Tortoise/doc/概要设计.md
2026-06-17 15:17:34 +08:00

280 lines
18 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 + WebSocketTS 原生 |
| UI 库 | Element Plus + ECharts 5 | 表格、表单、K 线、净值/概率曲线 |
| 状态管理 | Pinia | 与 Nuxt 3 集成最佳 |
| 后端运行时 | Node.js ≥ 20Nitro | |
| **存储** | **DuckDB单文件** | 同一个 `tortoise.duckdb` 同时承载行情、因子、元数据、任务、策略与 AI 结果;通过 schema 分区(`md.* / meta.* / run.*` |
| 缓存 | 进程内 LRU | 热点行情/快照短期缓存,避免穿透 DuckDB |
| 任务队列 | 内置内存队列(默认) / BullMQ + Redis可选 | 检测到 `REDIS_URL` 自动切换 |
| 数据源桥接 | 本地 Python sidecarFastAPI + 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.duckdbschema 分区 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 文件**。
> 理由:
> 1. DuckDB 同时支持列存大表(行情/因子)与小事务表(元数据),单连接即可覆盖。
> 2. 备份/迁移 = 拷贝单文件,避免三套生命周期不一致。
> 3. Parquet 仅作为"导出/外部分析"格式,不再作为运行期主存储;需要时用
> `COPY ... TO 'xxx.parquet'` 一行导出即可。
> 4. 单机场景下事务并发压力低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 SQL `window`)。
- `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 自选股查看
1. 前端 `/watchlist``meta.watchlist + watchlist_group_map`
2. `WatchlistService.enrich()`
- 当日涨跌、PE/PB → `TencentAdapter` 实时(带 LRU 缓存 5s
- 周/月涨跌 → DuckDB 对 `md.kline_daily` 做 5/20 日窗口聚合。
3. 返回合并 DTO点击单只 → `/quote/:symbol``MarketService` 拉 K 线。
### 6.2 策略运行(含自动保存与复用)
1. 用户在 `/strategy` 选择内置策略 + 选股策略universe+ 参数。
2. 服务端计算 `params_hash = sha1(json(params))`、`universe_hash = sha1(symbols)`、
`as_of_ts = max(md.kline_daily.date)`
3.`run.strategy_runs``(strategy_id, universe_hash, params_hash, as_of_ts)`
- 命中且状态=success → 直接返回历史 `run_id`**自动复用**)。
- 否则新建 run参数变更 → 新 run覆盖语义通过 UI 保留最新一条同参数 run
4. Job Worker 拉起策略:从 DuckDB 流式读 bar按事件驱动撮合
`strategy_curves``strategy_metrics`
5. WebSocket 推送净值完成跳到结果页ECharts 渲染)。
6. 多策略对比:在 `/strategy/compare` 选多条 `run_id` → 单图叠加曲线。
### 6.3 AI 短期上涨概率
1. `/ai/predict` 选标的、提示词模板、模型 provider、预测窗口如 5 日)。
2. `AIService` 拼上下文:最近 60 日 K 线摘要、最新估值、最近一期财报关键指标、F10 摘要。
3. 调模型OpenAI 兼容/Anthropic/Ollama解析输出概率要求模型按指定 JSON 格式返回)。
4.`run.ai_runs`,前端图表展示概率 + 置信区间 + 模型/Prompt 版本。
5. 支持批量:自选股分组一键预测。
### 6.4 数据增量更新
1. 调度器触发 `daily-update`
2. `DataService``meta.instruments` → 比对 `md.kline_daily` 最新日期 → 计算缺口。
3. `MootdxAdapter` 拉缺口 K 线≤800 根/次自动分页)+ 季报;`TencentAdapter` 拉当日快照。
4. 写入 DuckDB事务更新 `meta.data_versions`、`meta.data_jobs`。
5. 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 WorkerWeb 请求走只读连接;高频读热表加 LRU。
- **DuckDB 单文件损坏风险** → 每日 `EXPORT DATABASE` 备份,启动时校验。
- **策略代码安全** → `isolated-vm` 沙箱禁用 fs/net。
- **AI 输出不稳定** → 强制 JSON Schema 输出 + 重试 + 失败留 `raw_response` 供排查。
- **数据一致性 / 结果可复现** → 每次写入登记 `meta.data_versions`,策略与 AI 运行绑定 `as_of_ts`