18 KiB
18 KiB
Tortoise 投研回测系统 概要设计
1. 项目背景与目标
Tortoise 定位为个人/小团队使用的轻量级 A 股投研回测平台。基于
doc/数据源选型.md 中确定的免费、无 Key 的数据组合(mootdx + 腾讯财经),
对照 doc/需求.md 的功能清单,向上构建:自选股管理、行情查看、内置策略
回测、AI 短期上涨概率推算的一体化工作台。
核心目标(与 doc/需求.md 对齐):
- 自选股:增删改查、多分组、当前行情(当日/周/月涨跌、PE/PB)、历史 K 线。
- 策略:内置主要策略,按"选股策略 + 执行参数"运行;结果图表化、可叠加多策略、 自动持久化(同参数复用,参数变更则覆盖)。
- AI 接入:可配置多模型与提示词,输出短期上涨概率。
- 暂不开发:用户权限(仅保留扩展位)。
非功能目标:
- 单机即可部署,前后端同进程,开箱即用。
- 数据可复现:策略结果与"数据快照版本"绑定。
- 预留分布式扩展点(任务队列、独立计算节点)。
2. 设计原则
- B/S 架构:浏览器即客户端,服务端集中调度数据与计算。
- JS 一体式框架:前后端共享同一个 Node.js 进程与同一套 TS 类型定义。
- 数据源解耦:以
DataAdapter抽象隔离 mootdx / 腾讯财经接口,未来可加 akshare、tushare 等。 - 存储极简:行情/因子/元数据/任务/策略结果全部收敛到 单一 DuckDB 文件, 不再引入 SQLite 与独立 Parquet 目录(详见第 5.2 节)。
- 任务化:所有耗时操作(拉数据、回测、AI 调用)走任务队列,UI 实时订阅进度。
- 可复现:策略代码版本 + 参数 + 数据快照版本号 三者绑定。
- 遵循
doc/开发规范.md:函数中文注释(功能/参数/返回/注意事项);需求变更 同步更新文档+代码+测试;测试用例使用唯一编号(章-序)。
3. 技术选型
| 层 | 选型 | 说明 |
|---|---|---|
| 一体式框架 | Nuxt 3 (Nitro + Vue 3) | 同进程托管 SSR 前端 + server/api REST + WebSocket;TS 原生 |
| UI 库 | Element Plus + ECharts 5 | 表格、表单、K 线、净值/概率曲线 |
| 状态管理 | Pinia | 与 Nuxt 3 集成最佳 |
| 后端运行时 | Node.js ≥ 20(Nitro) | |
| 存储 | DuckDB(单文件) | 同一个 tortoise.duckdb 同时承载行情、因子、元数据、任务、策略与 AI 结果;通过 schema 分区(md.* / meta.* / run.*) |
| 缓存 | 进程内 LRU | 热点行情/快照短期缓存,避免穿透 DuckDB |
| 任务队列 | 内置内存队列(默认) / BullMQ + Redis(可选) | 检测到 REDIS_URL 自动切换 |
| 数据源桥接 | 本地 Python sidecar(FastAPI + mootdx) + Node 端 HTTP(腾讯财经) | mootdx 仅 Python 实现 |
| AI 接入 | OpenAI 兼容协议为主,扩展 Anthropic/本地 Ollama | 模型与 Prompt 配置存 DuckDB |
| 策略沙箱 | isolated-vm |
隔离用户/内置策略代码,禁用 fs/net |
| 测试 | Vitest(单测/接口) + Playwright(端到端) | 用例编号遵循 章-序 |
| 包管理 | pnpm | |
| 部署 | 单机 node .output/server/index.mjs 或 Docker Compose(含 sidecar) |
关于 mootdx:因仅有 Python 实现,Tortoise 用一个常驻 Python FastAPI sidecar 暴露
/kline /snapshot /f10 /finance等接口,Node 端通过DataAdapter调用。 腾讯财经字段解析全部在 Node 端实现(已校准索引:39=PE_TTM、43=振幅、46=PB、52=PE 静态)。
4. 总体架构
┌───────────────────────────────────────────────────────────────┐
│ Browser (Vue 3 SSR/SPA) │
│ 自选股 │ 行情/K 线 │ 策略运行台 │ 结果对比 │ AI 概率 │ 任务 │
└───────────────▲───────────────────────────▲───────────────────┘
│ HTTPS / WebSocket │
┌───────────────┴───────────────────────────┴───────────────────┐
│ Nuxt 3 一体式服务(Node.js 20) │
│ ┌──────────┐ ┌────────────┐ ┌─────────────────────────────┐ │
│ │ Pages │ │ server/api │ │ WebSocket / SSE 推送 │ │
│ └──────────┘ └─────┬──────┘ └──────────────┬──────────────┘ │
│ │ │ │
│ ┌───────────────────┴───────────────────────┴───────────────┐ │
│ │ Service 层 │ │
│ │ Watchlist │ Market │ Strategy │ AI │ Job │ Data │ │
│ └───┬────────┬─────────┬─────────┬─────┬─────────┬──────────┘ │
│ │ │ │ │ │ │ │
│ ┌───┴───┐ ┌──┴──────┐ ┌┴────────┐ ┌┴──┐ ┌──────┴─────────┐ │
│ │Adapter│ │ Indicator│ │Strategy │ │AI │ │ Job Queue │ │
│ │mootdx │ │ Library │ │Registry │ │Hub│ │ memory/BullMQ │ │
│ │腾讯 │ │ │ │(内置) │ │ │ │ │ │
│ └─┬───┬─┘ └──────────┘ └─────────┘ └─┬─┘ └────────────────┘ │
│ │ │ │ │
│ │ └─ HTTP ─► 腾讯财经 qt.gtimg.cn │ │
│ └──── HTTP ─► Python sidecar (mootdx, FastAPI) │
│ │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ 单一存储:tortoise.duckdb(schema 分区 md/meta/run) │ │
│ └────────────────────────────────────────────────────────────┘ │
└───────────────────────────────────────────────────────────────┘
5. 模块划分
5.1 数据采集层(DataAdapter)
MootdxAdapter:通过 sidecar 拉取 K 线(category 4=日、5=周、6=月、7=1m…11=60m)、 五档、逐笔、季报、F10。内置坑位规避:drop(columns=["datetime"])后reset_index; 屏蔽block()/minute()空接口。TencentAdapter:HTTP 调qt.gtimg.cn,GBK 解码,按校准后的索引解析 PE/PB/振幅/ 市值/涨跌停。带断言(PE/PB 合理性),异常告警。- 统一输出标准 DTO:
Kline、Snapshot、Tick、FinanceSnapshot、F10Section, 共享于shared/dto/,前后端复用 Zod 校验。
5.2 数据存储层(单一 DuckDB)
决策:原方案"DuckDB + SQLite + Parquet"三件套合并为 一个 DuckDB 文件。 理由:
- DuckDB 同时支持列存大表(行情/因子)与小事务表(元数据),单连接即可覆盖。
- 备份/迁移 = 拷贝单文件,避免三套生命周期不一致。
- Parquet 仅作为"导出/外部分析"格式,不再作为运行期主存储;需要时用
COPY ... TO 'xxx.parquet'一行导出即可。- 单机场景下事务并发压力低,DuckDB 的 MVCC 足以承担元数据写入。
DuckDB 内部按 schema 分区,文件名 data/tortoise.duckdb:
| schema | 表 | 说明 |
|---|---|---|
md (market data) |
kline_daily(symbol,date,o,h,l,c,vol,amount,adj) |
日线,主键 (symbol,date) |
kline_min(symbol,ts,period,o,h,l,c,vol) |
多周期分钟线,period∈{1,5,15,30,60} | |
snapshot_daily(symbol,date,pe_ttm,pe_static,pb,mcap,float_mcap,turnover,limit_up,limit_down,amplitude) |
估值快照(腾讯校准字段) | |
finance_quarter(symbol,report_date,...37 字段) |
mootdx 季报快照 | |
f10(symbol,section,content,fetched_at) |
F10 9 大类文本 | |
meta |
instruments(symbol,name,exchange,board,listed_at,delisted_at,...) |
标的元数据 |
watchlist(id,symbol,note,created_at) |
自选股 | |
watchlist_group(id,name) |
分组 | |
watchlist_group_map(watchlist_id,group_id) |
多对多映射(满足"一个自选股属多个分组") | |
data_jobs(id,kind,params,status,error,started_at,finished_at) |
数据/策略/AI 任务 | |
ai_providers(id,name,base_url,api_key_enc,model,enabled) |
AI 模型配置 | |
ai_prompts(id,scene,template,version,active) |
提示词模板(按场景版本化) | |
data_versions(as_of_ts,scope,row_count,checksum) |
数据快照版本(供回测/AI 复现) | |
run |
strategies(id,code_id,name,kind,params_schema) |
内置策略注册表 |
strategy_runs(id,strategy_id,universe_hash,params_hash,as_of_ts,status,started_at,finished_at) |
策略运行记录;同 (strategy_id,universe_hash,params_hash) 复用,参数变化即新建 | |
strategy_curves(run_id,date,equity,position_json) |
策略每日净值/持仓 | |
strategy_metrics(run_id,key,value) |
年化、回撤、Sharpe 等指标 | |
ai_runs(id,provider_id,prompt_id,symbol,as_of_ts,prob_up,horizon_days,raw_response) |
AI 概率推算结果 |
复用规则(对应需求"自动保存策略执行结果"): 以
(strategy_id, universe_hash, params_hash, as_of_ts)作为幂等键,相同键复用历史结果; 参数或选股变化导致 hash 变化 → 新建运行,旧记录保留;同 hash 重跑则覆盖(按需求要求)。
并发与备份:
- 写入集中:仅 Job Worker 持有可写连接,Web 请求走只读连接。
- 备份:每日定时
EXPORT DATABASE 'backup/yyyymmdd'(DuckDB 原生命令,输出 Parquet+SQL)。 这是唯一保留的 Parquet 用途——离线备份/外部分析,不参与运行期。
5.3 业务服务层
WatchlistService:自选股 CRUD + 分组多对多 + 当前行情聚合(合并 mootdx 当日 + 腾讯快照)。MarketService:K 线/快照/F10 查询;周/月涨跌由日线即时聚合(DuckDB SQLwindow)。StrategyService:- 内置策略注册(
strategies表 + 代码集中在server/engine/strategies/*.ts)。 - 运行入口:传入
strategyId + 选股策略 + 参数;命中幂等键直接返回历史结果。 - 多策略对比:同一图表叠加多个
run_id的strategy_curves。
- 内置策略注册(
AIService:providers/prompts管理 API;predictUp(symbol, horizon):拼装提示词(含最近行情、估值、F10 摘要)→ 调模型 → 解析概率 → 落ai_runs。- 提示词模板支持变量:
{{symbol}} {{kline_summary}} {{valuation}} {{news}}。
JobService:任务登记/进度/取消;统一 WebSocket 频道/_ws/jobs。DataService:增量拉取调度、缺口检测、data_versions写入。
5.4 Web 应用层
页面(与需求一一对应):
| 路由 | 对应需求 | 关键组件 |
|---|---|---|
/watchlist |
自选股增删改查、分组、当前行情 | 分组侧栏 + 行情表(涨跌/PE/PB) |
/quote/:symbol |
历史 K 线/财务/F10 | ECharts K 线 + 切换周期 |
/strategy |
策略运行台 | 选择内置策略 → 选股策略 → 参数表单 → 运行 |
/strategy/compare |
多策略叠加 | 多 run_id 净值曲线叠加 |
/ai |
AI 模型与提示词配置、批量预测 | Provider/Prompt 管理 + 概率结果 |
/jobs |
任务监控 | 列表 + 实时日志 |
/data |
数据更新 | 触发增量、查看版本 |
API(统一 JSON + Zod 校验):
/api/watchlist/*、/api/quote/*、/api/strategy/*、/api/ai/*、/api/jobs/*、/api/data/*。- 实时通道:
/_ws(任务进度、策略增量净值、AI 流式 token 可选)。
5.5 任务调度
- 默认内存队列;检测到
REDIS_URL自动切 BullMQ。 - 内置任务:日终增量拉数、F10 月更、策略运行、AI 批量预测、每日备份。
- cron 表达式存
meta.data_jobs(kind='cron'子类)。
6. 关键流程
6.1 自选股查看
- 前端
/watchlist拉meta.watchlist + watchlist_group_map。 WatchlistService.enrich():- 当日涨跌、PE/PB →
TencentAdapter实时(带 LRU 缓存 5s)。 - 周/月涨跌 → DuckDB 对
md.kline_daily做 5/20 日窗口聚合。
- 当日涨跌、PE/PB →
- 返回合并 DTO;点击单只 →
/quote/:symbol走MarketService拉 K 线。
6.2 策略运行(含自动保存与复用)
- 用户在
/strategy选择内置策略 + 选股策略(universe)+ 参数。 - 服务端计算
params_hash = sha1(json(params))、universe_hash = sha1(symbols)、as_of_ts = max(md.kline_daily.date)。 - 在
run.strategy_runs查(strategy_id, universe_hash, params_hash, as_of_ts):- 命中且状态=success → 直接返回历史
run_id(自动复用)。 - 否则新建 run(参数变更 → 新 run,覆盖语义通过 UI 保留最新一条同参数 run)。
- 命中且状态=success → 直接返回历史
- Job Worker 拉起策略:从 DuckDB 流式读 bar,按事件驱动撮合,写
strategy_curves与strategy_metrics。 - WebSocket 推送净值;完成跳到结果页(ECharts 渲染)。
- 多策略对比:在
/strategy/compare选多条run_id→ 单图叠加曲线。
6.3 AI 短期上涨概率
/ai/predict选标的、提示词模板、模型 provider、预测窗口(如 5 日)。AIService拼上下文:最近 60 日 K 线摘要、最新估值、最近一期财报关键指标、F10 摘要。- 调模型(OpenAI 兼容/Anthropic/Ollama),解析输出概率(要求模型按指定 JSON 格式返回)。
- 落
run.ai_runs,前端图表展示概率 + 置信区间 + 模型/Prompt 版本。 - 支持批量:自选股分组一键预测。
6.4 数据增量更新
- 调度器触发
daily-update。 DataService读meta.instruments→ 比对md.kline_daily最新日期 → 计算缺口。MootdxAdapter拉缺口 K 线(≤800 根/次自动分页)+ 季报;TencentAdapter拉当日快照。- 写入 DuckDB(事务),更新
meta.data_versions、meta.data_jobs。 - WebSocket 推送进度。
7. 目录结构(建议)
tortoise/
├── app/ # Nuxt 前端
│ ├── pages/
│ │ ├── watchlist.vue
│ │ ├── quote/[symbol].vue
│ │ ├── strategy/index.vue
│ │ ├── strategy/compare.vue
│ │ ├── ai.vue
│ │ ├── jobs.vue
│ │ └── data.vue
│ ├── components/
│ └── composables/
├── server/
│ ├── api/ # REST 端点
│ ├── services/ # Watchlist/Market/Strategy/AI/Job/Data
│ ├── adapters/ # mootdx / tencent
│ ├── engine/
│ │ ├── indicator/ # 指标库
│ │ ├── strategies/ # 内置策略
│ │ └── backtest/ # 撮合/绩效
│ ├── ai/ # provider/prompt 引擎
│ ├── jobs/ # 队列与定时任务
│ └── db/ # duckdb 单连接封装、迁移脚本
├── sidecar/ # Python FastAPI + mootdx
├── shared/ # 前后端共享 TS 类型 + Zod schema
├── tests/ # Vitest + Playwright(用例编号 章-序)
├── data/ # tortoise.duckdb + backup/
├── doc/
└── nuxt.config.ts
8. 安全与运维
- 默认绑定
127.0.0.1,对外发布需走反向代理(用户权限按需求暂不开发,仅预留中间件位)。 - 数据源限速:mootdx 单连接串行 + 退避;腾讯每秒 ≤ 10 请求;AI 调用按 provider 配置 RPS。
- AI Key:
ai_providers.api_key_enc用本地随机主密钥 AES-GCM 加密;主密钥放data/.master.key(gitignore)。 - 日志:pino + 按日切分;任务失败落
meta.data_jobs.error。 - 备份:每日
EXPORT DATABASE到data/backup/yyyymmdd/,保留 N 天。 - 策略沙箱:
isolated-vm禁用 fs/net,仅暴露受控的指标/数据 API。
9. 里程碑(与需求一致的优先级)
| 阶段 | 范围 | 产出 |
|---|---|---|
| M1 数据底座 | DataAdapter + 单 DuckDB + 数据更新页 | A 股全市场日线/估值/季报入库 |
| M2 自选股与行情 | Watchlist CRUD + 分组 + 当前行情 + K 线页 | 覆盖需求"自选股"全部条目 |
| M3 策略 | 内置策略 + 运行台 + 自动保存/复用 + 多策略叠加 | 覆盖需求"策略"全部条目 |
| M4 AI 接入 | Provider/Prompt 管理 + 概率预测 + 批量 | 覆盖需求"AI 接入" |
| M5 调度与备份 | 定时任务 + 备份 + 任务监控完善 | 长期稳定运行 |
10. 风险与对策
- mootdx 海外不可用 → sidecar 必须部署在国内主机;UI 给出连通性自检。
- 腾讯字段索引漂移 →
TencentAdapter自带断言(PE_TTM、PB 范围),异常立即告警。 - DuckDB 单文件并发 → 写入仅 Job Worker;Web 请求走只读连接;高频读热表加 LRU。
- DuckDB 单文件损坏风险 → 每日
EXPORT DATABASE备份,启动时校验。 - 策略代码安全 →
isolated-vm沙箱禁用 fs/net。 - AI 输出不稳定 → 强制 JSON Schema 输出 + 重试 + 失败留
raw_response供排查。 - 数据一致性 / 结果可复现 → 每次写入登记
meta.data_versions,策略与 AI 运行绑定as_of_ts。