DSAWiki
03系统 · 全景

系统全景:一台分析机器长什么样

把 36 万行代码拆成六层,用「工厂流水线」的方式理解每个模块的职责与边界。

约 7 分钟 进阶 第 03 章 · 系统

36 万行代码听起来吓人,但它并不是 36 万行「同时需要理解」的代码。真正的主干只有一条:入口 → 流水线 → 数据 / 指标 / LLM → 守门 → 交付。其余模块都挂在这条主干的某个节点上。

类比:一家餐厅

入口层是前台,决定今天开门做午市还是晚市;编排层是后厨的传菜单,把每道菜分给对应工位;数据层是供应链,菜市场关门了还有备选供应商;智能层是主厨,负责调味与装盘;交付层是服务员,把同一道菜按不同客人的忌口重新介绍。你不需要认识每个厨师,但要知道传菜单怎么走。

六层架构:先建立一张地图

DSA 是一个 Python 单体后端 + React 单页前端。它没有拆成微服务,而是用清晰的目录边界把职责分开。下面这张表就是全书反复使用的「地图」。

入口与调度ENTRY
main.pyserver.pywebui.pysrc/scheduler.py.github/workflows

解析命令行参数、加载配置、判断交易日、决定运行模式(单次 / 定时 / Web / Bot / 回测 / ETF 轮动),然后把任务交给编排层。它不负责「怎么分析」。

编排层ORCHESTRATION
src/core/pipeline.pysrc/services/*src/core/trading_calendar.pysrc/services/task_queue.py

核心是 StockAnalysisPipeline:并发调度股票、按顺序完成抓数 → 指标 → 新闻 → LLM → 守卫 → 落库 → 推送。服务层负责把同一套能力开放给 Web/API/Bot,而不是复制第二套分析逻辑。

能力层CAPABILITY
src/analyzer.pysrc/stock_analyzer.pysrc/market_analyzer.pysrc/search_service.pysrc/core/backtest_engine.py

真正干活的算法与业务能力:技术指标、Prompt 组装、LLM 调用与解析、新闻检索、大盘复盘、回测、选股、ETF 轮动。它们不关心自己被 CLI 还是 Web 调用。

数据层DATA
data_provider/*src/storage.pysrc/repositories/*src/data/*

14 个数据源适配器 + SQLite/SQLAlchemy 持久化。对外提供统一的行情、K 线、筹码、基本面、新闻接口;对内负责优先级、市场过滤、熔断、缓存与自愈式迁移。

智能层INTELLIGENCE
src/llm/*src/agent/*strategies/*.yaml

模型接入抽象(LiteLLM / 本地 CLI)、多模型回退、Agent 工具循环、多智能体编排、策略路由与聚合。它把「会说话」变成「会按格式交付结论」。

交付层DELIVERY
src/notification.pysrc/notification_sender/*templates/*.j2api/*apps/dsa-web/*bot/*

把同一份分析变成不同形态:Markdown 报告、微信精简版、飞书文档、分享海报、Web 页面、IM 机器人回复。渠道能力差异在这一层被吸收。

读代码时的「单一真源」原则

分析算法只有一套:pipeline + analyzer + stock_analyzer。CLI、API、Bot、桌面端都只是调用方。看到 src/services/analysis_service.py 这类文件时,先假设它是薄封装,不要期待第二套实现——这是理解大型仓库非常省时间的习惯。

入口层:今天以什么方式上班

main.py 的 main() 先解析参数,再按模式分派。模式之间不是互相包含的,而是并列的入口:

模式触发方式它做什么不做什么
单次分析python main.py个股 + 大盘复盘,跑完退出不常驻、不提供 Web
定时任务--schedule按配置时间反复触发分析不做交易日以外的补跑
Web / API--serve / --serve-only启动 FastAPI、静态站点、任务队列与调度--serve-only 不立即分析
Bot随 --serve* 启用的 Stream 长连接在飞书/钉钉里接收命令不是独立进程;Webhook 入口已不是主路径
回测--backtest用历史报告与价格评估建议不生成当日新分析
ETF 轮动--etf-rotation规则化双动量,不调用 LLM不参与个股 LLM 分析
GitHub Actionscron / 手动无参数运行,环境变量来自 Secrets不启动 Web

入口层还有两个容易被忽视的职责:交易日过滤与运行锁。前者按每只股票所属市场判断今天是否开市;后者保证同一时间只有一个分析任务( Actions 用 concurrency group,本地用文件锁,定时任务用 SQLite 原子认领)。

四个关键设计模式

适配器 + 注册表

每个数据源实现同一组方法,DataFetcherManager 按市场和优先级排序、逐个尝试。新增数据源只需实现 _fetch_raw_data / _normalize_data,上层代码不用改。

降级 + 熔断

单源失败不是异常,而是常态。失败后切下一条链;同一源连续失败 3 次,冷却 5 分钟。系统宁可少一个字段,也不让整轮分析崩掉。

LLM 出稿,规则定稿

模型负责归纳、解释与表达;评分刻度、支撑压力、资金流、市场阶段等确定性规则负责二次校准。避免「模型说买就买」。

配置优于代码

模型、数据源优先级、通知渠道、并发、报告语言都由环境变量驱动。同一套代码可以在 Actions、Docker、本地以不同策略运行。

数据层:稳定性的地基

数据层由三部分组成:多源适配(data_provider/)、持久化(src/storage.py)、缓存与索引(SQLite 日线表、实时行情 TTL、股票自动补全索引)。

它最重要的设计不是「支持很多源」,而是把「源不可用」变成一种正常状态。上层拿到的数据带有来源与质量标记;缺失的字段会明确写 not_supported,而不是用 0 或假数据填补。这直接决定了 LLM 会不会在错误前提上编故事。

存储使用 SQLite + SQLAlchemy。选它不是为了高并发,而是为了「一个人也能部署」:单文件、零服务、可备份。代价是写入并发有限,因此多实例部署时要特别小心调度重复——这一点在部署与运维里展开。

智能层:把数据变成结论

智能层可以继续拆成三个同心圆:

LLM 接入backend
Prompt + 上下文prompt
结构化解析parse
规则守卫guard
决策信号signal

src/llm/ 解决「跟谁说话」:统一后端协议、错误分类、重试与多模型回退。src/analyzer.py 解决「怎么问」:Prompt、JSON 契约、解析与兜底。src/agent/ 解决「先查什么」:工具循环、多智能体、策略路由。strategies/*.yaml 解决「用什么方法」:可读、可改、无需写代码的策略包。

关键认知:LLM 不是数据库,也不是预言机。它只能基于你给它的上下文做归纳。上下文缺失、数据过期或 Prompt 约束不清,模型就会「合理地编」。所以系统把大量工程放在上下文组织与后置校验上。

配置中心:一个巨大的、但有边界的开关面板

src/config.py 接近 4000 行,.env.example 超过 1000 行。第一次看很容易窒息,但它只做三件事:

  1. 从系统环境变量和 .env 读取配置,系统环境变量优先;
  2. 做类型、范围、互斥关系的解析与兜底;
  3. 把结果收进一个 Config 数据类,供全系统读取。

配置虽然多,但绝大多数有默认值,且遵循「不配置也能跑,配置后增强」的思路。真正需要关心的只有几组:模型渠道、自选股、通知渠道、数据源 Token、Web 认证与调度。其余属于调优项。

一个例外:Web 认证

ADMIN_AUTH_ENABLED 不在 Config 里,而由 src/auth.py 直接读取 .env。因此只 export 环境变量不会开启认证。部署公网 Web 时尤其要注意。

同一套主干,三种调用方式

main.py parse_args()
  → run_full_analysis()
    → 交易日过滤
    → StockAnalysisPipeline.run()
      → process_single_stock()
        → 数据 / 指标 / 新闻 / LLM / 守卫
      → 汇总报告
      → NotificationService
React 页面
  → POST /api/v1/analysis/analyze
    → AnalysisService
      → StockAnalysisPipeline.process_single_stock()
    → 任务状态写入 task queue
  → GET /api/v1/analysis/tasks/stream   (SSE 进度)
  → GET /api/v1/history/{id}            (读取报告)
飞书 / 钉钉 Stream 长连接
  → CommandDispatcher 解析命令
    → /analyze /ask /chat /market ...
      → 同一套 Pipeline / AgentExecutor
  → 结果通过通知渠道回复

为什么是单体 + SPA,而不是微服务?

这是一个个人/小团队工具,不是高并发 SaaS。单体的收益非常具体:

  • 部署简单:一个容器、一个进程、一个 SQLite 文件,NAS 或 1G 云主机就能跑。
  • 调试直接:一次分析调用栈在同一进程里,日志、断点、数据传递都容易追。
  • 事务简单:报告、决策信号、用量写同一个库,不需要分布式事务。
  • 共享配置与缓存:模型客户端、股票索引、熔断状态都在内存里。

代价是:水平扩展困难、SQLite 写入串行、单进程内的降噪状态不共享。项目通过「单调度入口 + 原子认领 + 进程内熔断」来接受这些代价,并在文档里明确边界。理解这种取舍,比背架构图更重要。

几个能建立体感的数字

657Python 文件
32SQLite 表
131API 路由
15YAML 策略

数字大,不等于都要读。按「入口 → 流水线 → 一个具体能力 → 一个数据源」的顺序,你可以在几小时内建立足够的工作理解。

源码与文档锚点