- USAGE 新增 §6.3.1「归档的日常操作」:查看 / 筛选(回写 URL)/ 导出完整 JSON / 以此参数再跑 / 删除(写明「删除即失去结果,结果只存归档一份」)/ 历史归档用 restore_experiment_from_job 按原 id 重建;并说明归档完整度如何标注 - USAGE/README 更正技术栈与图表基座:TradingView Lightweight Charts 4.2.3 为唯一 图表基座(ECharts 已从 package.json、pnpm-lock.yaml、node_modules、文档与 架构图标注中全部清除),并记录实测证据(个股页图表根节点为 div.tv-lightweight-charts,页面 canvas 无一来自其它图表库) - ARCHITECTURE 更正「当前数据库」(原写 SQLite/未来 MySQL):现为本机 MariaDB 10.11, §6 补目标库硬约束;USAGE 补服务器身份与实测连接证据 - AGENT.md 新增 §0.1:数据库目标只允许本机 MariaDB,禁止 192.168.1.10, 由 config.py::assert_db_target_allowed 硬拦截(命中直接抛错,不静默降级) - DEV_PLAN_DIVIDEND_BACKTEST 记录三轮实施与验证、归档恢复边界(§12.5)、已知限制 - DEV_PLAN v2/v3 标注为历史记录(避免把当时的远端库地址当现状照抄); ROADMAP 更正 M3 图表选型
20 KiB
A股个人量化选股与回测平台架构
版本:v1.0
定位:个人开发、A股、中低频选股/回测、AI Agent 二次开发
数据源优先级:Tushare > 新浪财经
当前数据库:本机 MariaDB 10.11(127.0.0.1:3306/qlib)
数据库目标约束:只允许本机;192.168.1.10已禁止作为 DB 目标,由app/core/config.py::assert_db_target_allowed在解析配置时硬拦截(见AGENT.md§0.1)
历史:SQLite(data/quant.db)→ 远端 MySQL → 本机 MariaDB(逐表一致副本)
核心量化引擎:Qlib
前端:React / Next.js
后端:FastAPI + Python
1. 项目目标
本项目不是简单给 Qlib 套一个 Web UI,而是构建一个独立的个人量化研究平台:
Web 前端
↓
FastAPI
↓
Research Specification
↓
业务服务层
├── 数据服务
├── 股票池服务
├── 因子服务
├── 选股服务
├── 模型服务
├── 回测服务
└── 实验服务
↓
Qlib / ML
↓
标准化研究结果
↓
SQLite / Parquet
↓
Web 前端
AI Agent 作为独立能力层,通过受控 Tool 调用上述服务,而不是直接操作数据库或随意修改 Qlib 内部代码。
2. 总体架构
┌──────────────────────┐
│ React / Next │
│ │
│ Dashboard │
│ 股票池 │
│ 因子研究 │
│ 选股 │
│ 回测 │
│ Experiment │
│ AI Research │
└──────────┬───────────┘
│
REST / SSE
│
▼
┌──────────────────────┐
│ FastAPI │
│ │
│ API / Auth / DTO │
│ Job / Validation │
└──────────┬───────────┘
│
┌────────────┼────────────┐
│ │ │
▼ ▼ ▼
Data Service Research Agent Service
│ Service │
│ │ │
▼ ▼ ▼
DAO / Repo Qlib / ML Agent Tools
│ │ │
└────────────┼──────────────┘
▼
┌──────────────────────┐
│ Persistence │
│ │
│ SQLite │
│ Parquet │
│ Qlib Dataset │
└──────────────────────┘
3. 核心设计原则
3.1 Qlib 不是系统数据库
Qlib 负责:
- Feature / Dataset
- 模型训练
- Prediction
- Portfolio
- Backtest
- 部分分析工具
Qlib 不作为业务系统唯一数据存储。
系统数据分为:
业务数据 → SQLite
历史/分析型大数据 → Parquet
Qlib计算数据 → Qlib Dataset
实验结果 → SQLite
3.2 前端不能直接调用 Qlib
必须经过:
Frontend
↓
FastAPI
↓
Application Service
↓
Qlib Adapter
↓
Qlib
这样未来可以替换 Qlib,而不影响前端。
3.3 DAO 必须与数据库实现解耦
业务代码禁止直接:
sqlite3.connect(...)
也禁止在 Service 中写 SQL。
必须:
Service
↓
Repository / DAO Interface
↓
SQLite Repository
未来:
Service
↓
Repository Interface
├── SQLite Repository
└── MySQL Repository
业务层不感知底层数据库。
4. 数据源架构
4.1 数据源优先级
Data Service
│
┌────────┴────────┐
▼ ▼
Tushare Sina
Primary Secondary
│ │
└────────┬────────┘
▼
Normalization
│
Validation
│
Storage
第一优先级:Tushare
用于:
- 股票基本信息
- 日线行情
- 复权因子
- 指数
- 财务数据
- 分红送转
- 停复牌
- 行业/概念
- 其他可用数据
第二优先级:新浪财经
主要作为:
- Tushare 数据缺失补充
- 行情数据校验
- 特定实时/准实时数据补充
新浪数据必须经过统一 Adapter,禁止业务代码直接调用新浪接口。
5. 数据采集流水线
Scheduler
↓
Data Source Adapter
↓
Raw Response
↓
Schema Validation
↓
Normalization
↓
Deduplication
↓
Business Validation
↓
DAO
↓
SQLite
↓
Parquet Export
↓
Qlib Dataset
每个数据源都必须有独立 Adapter:
data/
├── sources/
│ ├── base.py
│ ├── tushare.py
│ └── sina.py
├── normalizers/
├── validators/
└── service.py
业务层只依赖:
MarketDataProvider
而不是:
TushareClient
6. 数据库设计
目标库约束(硬性):一律
127.0.0.1:3306/qlib(本机 MariaDB 10.11)。192.168.1.10禁止作为数据库目标 —— 连错库不会报错、界面也正常,却会把回测/归档/策略 静默写到另一台机器上,属于最难发现的一类故障。守卫在get_settings()里执行: 命中禁用主机直接抛错(应用起不来),禁用列表可用QLIB_FORBIDDEN_DB_HOSTS覆盖。 启动日志会打印数据库目标:mysql qlib@127.0.0.1:3306/qlib(不含密码),便于随时确认。
6.1 SQLite 定位
SQLite 用于:
- 股票基础信息
- 数据源状态
- 数据同步记录
- 因子定义
- 策略定义
- 回测配置
- Experiment metadata
- Job
- 用户配置
- 系统配置
SQLite 不建议长期承载超大规模原始行情明细。
历史行情和大量 Feature 优先使用 Parquet。
7. DAO / Repository 设计
推荐使用:
SQLAlchemy 2.x
+
Repository Pattern
+
Pydantic DTO
目录:
backend/
├── domain/
│ ├── entities/
│ └── repositories/
│
├── infrastructure/
│ └── persistence/
│ ├── sqlalchemy/
│ │ ├── models/
│ │ ├── repositories/
│ │ └── session.py
│ └── migrations/
│
└── application/
└── services/
Repository 接口示例:
class StockRepository(Protocol):
def get_by_symbol(self, symbol: str) -> Stock | None: ...
def list(self, filters: StockFilter) -> list[Stock]: ...
def save(self, stock: Stock) -> Stock: ...
SQLite:
class SqlAlchemyStockRepository(StockRepository):
...
未来 MySQL:
class MySqlStockRepository(StockRepository):
...
推荐实际上让两者都基于 SQLAlchemy,而不是分别维护两套 SQL。
这样数据库切换主要通过:
DATABASE_URL
完成。
例如:
SQLite:
sqlite:///./data/quant.db
MySQL:
mysql+pymysql://user:password@host/quant
业务 Service 不需要修改。
8. 建议的核心数据库表
第一阶段至少包括:
stock
stock_daily
stock_adjust_factor
industry
stock_industry
index
index_daily
trading_calendar
suspend_data # ⚠️ 未实现(停牌以「当日无行情」近似,结果页如实标注)
stock_st_data # → 由 stock_name_history(tushare namechange)落地时点 ST 判定
financial_indicator
income_statement
# ---- 后续增量(已落地,见 docs/DEV_PLAN_DIVIDEND_BACKTEST.md §10)----
daily_basic # 每日指标:dv_ratio/dv_ttm 股息率、PE/PB、市值(高股息选股)
stock_name_history # 名称生效区间:exclude_st 的**时点**口径(股息陷阱可见)
balance_sheet
cashflow_statement
factor_definition
factor_test
strategy
strategy_factor
backtest
backtest_trade
backtest_position
backtest_metric
experiment
experiment_artifact
job
data_sync_log
注意:
stock_daily 等大量时序数据如果规模变大,应逐渐迁移到:
Parquet
SQLite 保留元数据和索引。
9. 时间与未来函数防护
所有研究数据必须明确:
trade_date
report_date
announce_date
effective_date
财务数据必须按照 announce_date 控制可见性。
禁止:
使用未来公告的财务数据回测过去
所有数据查询必须明确:
as_of_date
例如:
get_financial_data(
symbol="600519.SH",
as_of_date="2024-06-30"
)
只允许返回当时市场已经知道的数据。
10. Domain 层
建议核心领域对象:
Stock
Universe
Factor
FactorTest
Model
Strategy
Portfolio
Backtest
Experiment
Job
例如:
Universe
↓
Factor
↓
Model
↓
Strategy
↓
Backtest
↓
Experiment
11. Research Specification
前端、AI Agent 和后端统一使用自己的研究描述对象。
示例:
{
"type": "backtest",
"universe": {
"market": "CN_A",
"exclude_st": true,
"exclude_suspended": true,
"min_listing_days": 250
},
"factors": [
{
"name": "momentum_60",
"weight": 0.3
},
{
"name": "roe_ttm",
"weight": 0.3
},
{
"name": "low_volatility_60",
"weight": 0.4
}
],
"selection": {
"top_n": 30
},
"rebalance": "monthly",
"period": {
"start": "2015-01-01",
"end": "2026-08-31"
}
}
然后:
Research Specification
↓
Validator
↓
Strategy Builder
↓
Qlib Adapter
12. 前端结构
第一阶段(已实现,侧栏按研究闭环分组):
研究 总览 / 股票池 / 股票筛选
策略 策略库 / 选股回测 / 实验对比
因子与信号 因子研究 / 因子组合 / 交易信号
数据 数据同步 / 数据管理
12.1 表现层约定(本轮定型)
| 约定 | 取值 | 理由 |
|---|---|---|
| 图表库 | TradingView Lightweight Charts(唯一) | 轻量(~45KB)、原生支持「在 series 上打买卖点标记」,金融图表交互(十字光标/缩放)开箱可用;ECharts 已下线(依赖已移除),避免两套图表基座带来的样式与交互分裂 |
| 图表基座 | components/charts/LwChart.tsx |
折线/面积/柱状 + 标记 + tooltip + 可点击图例由一个组件承载;chart 实例只在结构变化时重建,数据变化只 setData,避免每次刷新重建 canvas |
| 主题 | components/charts/theme.ts |
颜色/数值格式集中定义(涨绿跌红按 A 股习惯),组件内不出现硬编码色值 |
| 标记约束 | 标记时间必须存在于对应 series 且升序 | Lightweight Charts 硬约束:不满足会整组标记丢失。LwChart 内统一做「过滤到已有时间点 + 排序」,页面只负责给语义(BUY/SELL) |
| 股票标识 | SymbolLink(lib/symbols.tsx) |
「有代码必有名称」是产品要求:后端填充 name(权威),前端 GET /api/stocks/names 缓存兜底;名称缺失显示「—」,不猜不造(§7 无静默行为) |
| 参数模型 | components/StrategyParamsForm.tsx 单一 StrategyParams |
策略库/回测/直通入口共用同一参数模型与校验,避免三处各自维护字段(§6 依赖抽象) |
| 策略说明 | StrategyDoc(后端 describe_strategy 推导) |
说明与公式由 spec 真实推导而非前端手写模板:参数一改,公式同步变;引擎未建模处进 warnings 如实暴露(§7/§24) |
| 结果视图 | components/BacktestResultView.tsx(回测页与归档页共用) |
「刚跑完」和「翻回来看」必须是同一套图表与表格;两处各写一套必然漂移成「归档里少一张图」 |
| 归档页 | /experiments/{id} 为 Server Component(app/experiments/[id]/page.tsx) |
归档是只读内容:选股条件/执行依据/元数据必须服务端直出(客户端渲染时 SSR HTML 里连「选股条件」都搜不到);交互(删除/导出/折叠 spec)隔离在 components/ArchiveActions.tsx |
| 归档口径说明 | 归档页顶部「选股条件」+「交易执行依据」两块,取自归档内 spec | 说明必须与那次执行一致,不能读当前页面状态(否则「看归档」会看到今天的参数);字段逐项对应引擎实执行语义,不写泛泛的模板话 |
| 归档列表分页 | body 保持 list[...],总数放 X-Total-Count 响应头 |
不破坏既有前端契约,同时消除「硬编码 limit=50 静默截断」(§7):前端据此显示「显示 N 条 / 共 M 条」 |
前端只依赖
/api/*的 Domain 结构(§3.2):name由后端填充而不是前端反查数据库, 图表库可替换而不影响领域层,/backtest的三种入口(策略/选股/实验)都只是把 URL 参数 映射成同一个StrategyParams。
第二阶段:
模型研究
Portfolio
AI Research
数据管理
13. 前端输入
股票池
支持:
- 市场
- 股票类型
- 行业
- 市值
- 流动性
- 上市时间
- ST
- 停牌
- 指数成分
因子
支持:
- 内置因子
- 自定义表达式
- 因子组合
- 权重
- 标准化
- 中性化
选股
支持:
- Top N
- Top %
- Score
- 等权
- Score 加权
- 行业约束
- 单股权重上限
回测
支持:
- 起止日期
- 调仓频率
- 初始资金
- 手续费
- 印花税
- 滑点
- 涨跌停限制
- 停牌限制
14. 前端输出
回测结果统一转换为:
BacktestResult
├── summary
├── equity_curve
├── drawdown
├── monthly_returns
├── yearly_returns
├── positions
├── trades
├── turnover
├── risk_metrics
└── factor_exposure
这样前端完全不需要理解 Qlib 内部对象。
15. 异步 Job
所有耗时任务必须异步:
POST /api/backtests
↓
Job Created
↓
Worker
↓
Qlib
↓
Result
前端通过:
SSE
接收:
queued
running
factor_calculation
model_training
backtesting
analysis
completed
failed
第一阶段可以使用:
FastAPI BackgroundTasks
或简单本地 Worker。
系统复杂后再引入:
Redis + Celery/RQ
不要第一版过度工程化。
16. Qlib Adapter
Qlib 必须被封装:
quant/
├── qlib_adapter/
│ ├── dataset.py
│ ├── feature.py
│ ├── model.py
│ ├── backtest.py
│ └── provider.py
业务代码:
backtest_service.run(spec)
而不是:
qlib.init(...)
qlib.workflow(...)
散落在项目各处。
17. AI Agent 架构
Agent 只能调用 Tool:
Agent
├── search_stocks
├── inspect_factor
├── test_factor
├── create_strategy
├── run_backtest
├── compare_experiments
├── get_backtest_result
└── create_experiment
Agent 禁止:
直接 SQL
直接修改数据库
直接删除数据
直接执行任意 shell
直接修改生产策略
Agent:
自然语言
↓
Research Plan
↓
Tool Calls
↓
Research Specification
↓
执行
↓
Experiment
↓
分析
18. AI Research 示例
用户:
帮我寻找适合A股月度调仓的低频选股因子。
Agent:
1. 获取A股股票池
2. 创建候选因子
3. IC测试
4. RankIC测试
5. 分层测试
6. 相关性分析
7. 删除冗余因子
8. 组合因子
9. Walk Forward
10. 回测
11. 保存 Experiment
12. 输出结论
Agent 不能因为一次回测表现好就直接认为策略有效。
必须关注:
样本外
Walk Forward
不同市场阶段
交易成本
换手率
因子稳定性
19. 推荐的开发目录
quant-platform/
│
├── frontend/
│ └── web/
│
├── backend/
│ ├── app/
│ │ ├── api/
│ │ ├── application/
│ │ ├── domain/
│ │ ├── infrastructure/
│ │ ├── quant/
│ │ ├── agent/
│ │ └── core/
│ │
│ └── tests/
│
├── data/
│ ├── raw/
│ ├── normalized/
│ ├── parquet/
│ └── qlib/
│
├── experiments/
│
├── scripts/
│
├── docker/
│
├── docs/
│
├── ARCHITECTURE.md
├── AGENT.md
└── README.md
20. 第一阶段 MVP
不要一开始做所有功能。
Phase 1:数据
Tushare
↓
标准化
↓
SQLite
↓
Parquet
完成:
- 股票列表
- 交易日历
- 日线
- 复权因子
- 基本财务指标
Phase 2:Qlib
Parquet
↓
Qlib Dataset
↓
Alpha158 / 自定义因子
↓
LightGBM
↓
Backtest
Phase 3:Web
完成:
股票池
因子
选股
回测
结果
Phase 4:Experiment
所有研究自动保存。
Phase 5:AI Agent
最后再接:
自然语言
↓
Research Plan
↓
Tool
↓
Experiment
21. 数据流总图
Tushare
│
▼
Tushare Adapter
│
│ 失败/缺失
▼
Sina Adapter
│
▼
Raw Data
│
▼
Data Validator
│
▼
Normalization
│
┌─────┴──────┐
▼ ▼
SQLite Parquet
│ │
│ ▼
│ Qlib Dataset
│ │
│ ┌─────┴─────┐
│ ▼ ▼
│ Factor Model
│ │ │
│ └─────┬─────┘
│ ▼
│ Strategy
│ │
│ ▼
│ Backtest
│ │
└────────────┤
▼
Experiment
│
┌─────────┴─────────┐
▼ ▼
Web Output AI Analysis
22. 最终原则
这个系统必须坚持:
- Tushare 第一,新浪第二
- 数据源通过 Adapter 隔离
- DAO / Repository 隔离数据库
- SQLite 当前使用,SQLAlchemy 保证未来 MySQL 可切换
- 大量历史时序数据逐渐转 Parquet
- Qlib 是计算引擎,不是整个系统
- 前端只面对自己的 Domain API
- 前后端通过 Research Specification 解耦
- 所有耗时任务异步化
- 所有研究产生 Experiment
- 严格防止未来函数
- AI Agent 只能通过 Tool 使用研究能力
- 第一阶段不做实盘
- 第一阶段不做复杂分布式架构
- 每个模块都必须可以被单元测试
最终目标不是:
Qlib + Web
而是:
一个以 Qlib 为量化研究引擎、以 Tushare 为主要数据源、具备清晰 DAO 抽象和 AI Agent 接口的个人 A 股量化研究平台。