docs: 同步操作说明/架构/路线图(归档操作、图表基座、数据库目标硬约束)
- 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 图表选型
This commit is contained in:
@@ -40,6 +40,17 @@ wget -e use_proxy=yes -e http_proxy=http://192.168.1.160:3128 <url>
|
||||
- 代理 IP 属于局域网配置,不写入 `.env` / `config.yaml` / 任何提交进 Git 的文件。
|
||||
- 直接下载成功时不要多此一举走代理。
|
||||
|
||||
### 0.1 数据库目标:只允许本机 MariaDB(硬约束)
|
||||
|
||||
- 数据库一律指向**本机 MariaDB 10.11**(`127.0.0.1:3306/qlib`)。
|
||||
- **禁止把 `192.168.1.10` 作为数据库目标**(该地址在本文档中仅作外网代理白名单/SSH 提及,
|
||||
不是数据库目标)。远端旧库只作为历史数据来源,已不再写入。
|
||||
- 该约束不是「靠自觉」:`app/core/config.py::assert_db_target_allowed` 在 `get_settings()`
|
||||
解析出 URL 后立刻校验,命中禁用主机**直接抛错**(应用起不来),不允许「起得来但连错库」。
|
||||
禁用列表默认 `192.168.1.10`,可用环境变量 `QLIB_FORBIDDEN_DB_HOSTS` 覆盖(空串=不限制)。
|
||||
- 理由:连错库属于最危险的静默错误 —— 不报错、界面正常,回测/归档/策略却写到了另一台机器上。
|
||||
- 新增脚本/文档/示例配置时,DB 主机一律写 `127.0.0.1`,禁止出现远端库地址作为**默认值**。
|
||||
|
||||
---
|
||||
|
||||
# 1. 项目定位
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
详见 [AGENT.md](./AGENT.md)(开发约束)与 [docs/ARCHITECTURE.md](./docs/ARCHITECTURE.md)(架构文档),**改代码前必须阅读**。
|
||||
|
||||
```text
|
||||
Web 前端 (frontend/web:总览 / 股票池 / 股票筛选 / 因子研究 / 因子组合 / 交易信号 / 选股回测 / 实验)
|
||||
Web 前端 (frontend/web:总览 / 股票池 / 股票筛选 / 因子研究 / 因子组合 / 交易信号 / 选股回测 / 实验 / 归档详情)
|
||||
↓ REST / SSE(异步 Job 状态机)
|
||||
FastAPI (backend:业务对象 API,见下「核心能力」)
|
||||
↓ Research Specification(统一研究契约)
|
||||
@@ -72,7 +72,12 @@ cd backend
|
||||
uv sync # 含 pyqlib(GitHub 源码依赖,固定 commit)。若网络下载困难/超时,按 AGENT.md §0 设置代理 192.168.1.160:3128 后重试
|
||||
|
||||
# 3. 运行测试
|
||||
uv run pytest
|
||||
uv run pytest # 全量 341 条
|
||||
uv run ruff check app tests
|
||||
|
||||
# 3b. 端到端契约自检(真实提交回测 Job,验证页面↔后端字段不漂移)
|
||||
PYTHONPATH=. .venv/bin/python ../scripts/verify_strategy_workspace.py # 策略库/说明/名称/选股直通/归档链路
|
||||
PYTHONPATH=. .venv/bin/python ../scripts/verify_backtest_page_contract.py # 回测结果结构契约
|
||||
|
||||
# 4. 启动开发服务
|
||||
uv run uvicorn app.main:app --reload --port 8000
|
||||
@@ -101,8 +106,14 @@ uv run alembic revision --autogenerate -m "add xxx table"
|
||||
| 因子研究 | Job 异步 IC / RankIC / 分层测试(`/api/jobs`) |
|
||||
| 交易信号 | `POST /api/signals`:评分排名 + 趋势规则 → BUY/WATCH/SELL + 理由;Web `/signals` |
|
||||
| 选股回测 | `POST /api/backtests`(成本/涨跌停近似;与当前选股共用同一评分引擎,v2 §25 一致性) |
|
||||
| 策略资产 | `strategy` 落库 + `/api/strategies`(命名配置,可展开为回测 spec) |
|
||||
| Experiment | 每次研究自动归档 + 一键复跑 + SSE 进度 |
|
||||
| 定期调仓回测 | **两级截断(候选池 n → 持仓 x)+ 双周期(每 m 月择股 / 每 y 月调仓)+ 复权口径(none/qfq/hfq)+ 组合条件过滤 + 最低佣金**;输出净值/个股曲线并标注买卖点;Web `/backtest`(含高股息案例预设)、`scripts/run_dividend_case.py` |
|
||||
| 每日指标 | `daily_basic`(股息率 `dv_ratio`/`dv_ttm`、PE/PB/市值)+ `dividend_yield` 因子;`sync daily_basic` 回补 |
|
||||
| 退市股与时点股票池 | `sync basic --include-delisted` 入库已退市/暂停上市(`status`/`delist_date`)+ 定向 `sync daily` 补行情;`filter_stocks` 按 `as_of` 正确纳入/排除(实测 338 只) |
|
||||
| 时点 ST / 名称历史 | `sync namechange` 入库名称生效区间(14,213 行);`exclude_st` 在**每个择股日**按当时名称判定(回测与 `/api/selections` 同口径),消除「曾高股息后 ST」的股息陷阱隐藏偏差(实测 3.70pp) |
|
||||
| 策略库 | `strategy` 落库 + `/api/strategies` CRUD/`PUT` 原地更新/展开为回测 spec;**每个策略有后端推导的「一句话说明 + 计算公式 + 执行步骤 + 注意事项」**(`describe_strategy`,与引擎实执行规则同源);一键回测;Web `/strategies` |
|
||||
| Experiment / 归档 | **完整存档**:每次回测(异步 Job 与同步接口都算)把结果 + spec + `code_version` + `data_version` 数据快照指纹写入 `experiment` 表,个股收益曲线默认**全量保存**(超出体积预算才裁剪并显式标注);列表支持 `kind`/`q` 过滤且总数经 `X-Total-Count` 暴露(不再静默截断);`DELETE /api/experiments/{id}` + `python -m app.cli.prune_experiments --keep N`(默认 dry-run,删除即失去结果,建议先导出)治理体积;**实验对比**:勾选 2~3 个 → 归一化净值曲线叠加 + 指标差值表 + `config_snapshot` 参数 diff;**归档详情页 `/experiments/{id}`**(Server Component)只读复看完整结果,并明确回答「**选股条件**」与「**交易执行依据**」;归档页可**导出完整 JSON / 以此参数再跑 / 删除**(确认框写明后果);体积用 `app.cli.prune_experiments`(默认 dry-run)治理,删错的历史归档可用 `app.cli.restore_experiment_from_job` 从 Job 副本按原 id 重建 |
|
||||
| 图表基座 | **统一 TradingView Lightweight Charts 4.2.3**:`components/charts/LwChart.tsx`(折线/面积/柱状 + 买卖点标记 + tooltip + 可点击图例)与 `components/StockChart/CandleChartLW.tsx`(个股 K 线 + 成交量 + MA);ECharts 已完全下线(`package.json`、`pnpm-lock.yaml`、`node_modules`、文档与架构图标注均已清除)。实测个股页图表根节点是 Lightweight Charts 自身的 `div.tv-lightweight-charts`,页面 canvas 无一来自其它图表库 |
|
||||
| 代码即名称 | 任何出现股票代码的位置都成对显示股票名称且可点击进入个股页(基本信息 + 走势图);后端在 `SelectionCandidate/SymbolCurve/ActionRecord/RankedPick/Position/Trade` 上填充 `name`,前端 `GET /api/stocks/names` 一次性缓存兜底 |
|
||||
| AI Agent | `POST /api/agent/chat`,**14 个受控 Tool**(v3 §25 全清单) |
|
||||
|
||||
## 里程碑(详见 [ROADMAP.md](./docs/ROADMAP.md) 与 [docs/DEV_PLAN_v2.md](./docs/DEV_PLAN_v2.md))
|
||||
|
||||
+43
-11
@@ -3,8 +3,10 @@
|
||||
> 版本:v1.0
|
||||
> 定位:个人开发、A股、中低频选股/回测、AI Agent 二次开发
|
||||
> 数据源优先级:Tushare > 新浪财经
|
||||
> 当前数据库:SQLite
|
||||
> 未来数据库:MySQL
|
||||
> 当前数据库:**本机 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
|
||||
@@ -269,6 +271,13 @@ 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 用于:
|
||||
@@ -385,11 +394,16 @@ index
|
||||
index_daily
|
||||
|
||||
trading_calendar
|
||||
suspend_data
|
||||
stock_st_data
|
||||
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
|
||||
|
||||
@@ -552,17 +566,35 @@ Qlib Adapter
|
||||
|
||||
# 12. 前端结构
|
||||
|
||||
第一阶段:
|
||||
第一阶段(**已实现**,侧栏按研究闭环分组):
|
||||
|
||||
```text
|
||||
Dashboard
|
||||
股票池
|
||||
因子研究
|
||||
选股
|
||||
回测
|
||||
Experiment
|
||||
研究 总览 / 股票池 / 股票筛选
|
||||
策略 策略库 / 选股回测 / 实验对比
|
||||
因子与信号 因子研究 / 因子组合 / 交易信号
|
||||
数据 数据同步 / 数据管理
|
||||
```
|
||||
|
||||
## 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`。
|
||||
|
||||
第二阶段:
|
||||
|
||||
```text
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -44,6 +44,7 @@ AI Agent(6 个受控 Tool)。**数据库已于 2026-09 从 SQLite 全量迁
|
||||
**已交付(2026-09)**:
|
||||
|
||||
- MySQL(192.168.1.10:3306,MariaDB 10.11,库 `qlib`,utf8mb4)建 8 张业务表 + alembic_version,
|
||||
> ⚠️ 历史记录:该地址已**禁止作为 DB 目标**,现为本机 MariaDB(`127.0.0.1:3306/qlib`),见 `AGENT.md` §0.1。
|
||||
全部经 `alembic upgrade head`(迁移 e4d18825…53113c80…91c4e27a…d3f6c9a2),结构由既有 ORM 模型保证。
|
||||
- `scripts/migrate_sqlite_to_mysql.py`:一次性工具 —— sqlite 直连 + pymysql chunk 多值 INSERT +
|
||||
keyset 分页续传 + 幂等(已迁则跳过)+ `--verify-only` 逐表 COUNT 与抽样比对;支持 `--only <表>` 并行大表、
|
||||
|
||||
@@ -9,6 +9,9 @@
|
||||
## 0. 现状 vs v3(已完成清单)
|
||||
|
||||
**数据库**:MySQL(qlib@192.168.1.10)默认,15 张业务表,Alembic head `e1f2a3b4c5d6`;
|
||||
> ⚠️ **历史记录,勿照抄**:当前 DB 目标为本机 MariaDB 10.11(`127.0.0.1:3306/qlib`),
|
||||
> 远端 `192.168.1.10` **已被禁止作为 DB 目标**(代码层 `assert_db_target_allowed` 硬拦截,
|
||||
> 见 `AGENT.md` §0.1)。以下涉及该地址的描述均为当时状态。
|
||||
SQLite→MySQL 迁移数据 16,057,265 行校验一致(M-DB)。
|
||||
|
||||
**研究/选股主线(M6–M8 已完成)**:
|
||||
|
||||
+2
-1
@@ -121,7 +121,8 @@
|
||||
|
||||
> 目标:业务导向 UI(股票池/因子/选股/回测/结果),不暴露 Qlib 内部概念。
|
||||
|
||||
- `frontend/web`:Next.js(TS) + ECharts(按 README 与架构 §12/§13/§26 初始化)
|
||||
- `frontend/web`:Next.js(TS) + **TradingView Lightweight Charts**(按 README 与架构 §12/§13/§26 初始化)
|
||||
> 注:M3 期曾用 ECharts,后已下线并移除依赖(含锁文件),全站唯一图表基座为 `components/charts/LwChart.tsx`。
|
||||
- 页面:Dashboard → 股票池 → 因子研究 → 选股 → 回测 → 结果(Experiment 视角)
|
||||
- API 对齐业务对象(AGENT.md §17):`/api/stocks /api/universes /api/factors /api/factor-tests /api/strategies /api/backtests /api/experiments /api/jobs`
|
||||
- 交互:参数清晰、结果可视化;耗时任务显示 job 进度(轮询起步,SSE 视需要)
|
||||
|
||||
+264
-23
@@ -25,7 +25,7 @@
|
||||
| 异步与归档 | Job 状态机 + Experiment 自动归档 + 一键复跑 + SSE 进度 |
|
||||
| AI Agent | **14 个受控工具**(v3 §25 全清单)+ LLM 编排 |
|
||||
|
||||
**技术栈**:Python 3.12(uv) · FastAPI · SQLAlchemy 2 · Alembic · **MySQL(pymysql)**(SQLite 兜底)· pandas · pyarrow(Parquet) · pyqlib 0.9.8.dev32(源码安装) · LightGBM · Next.js 15 · ECharts · Redis(本机 127.0.0.1:6379 已就绪,触发时接入)
|
||||
**技术栈**:Python 3.12(uv) · FastAPI · SQLAlchemy 2 · Alembic · **MySQL(pymysql)**(SQLite 兜底)· pandas · pyarrow(Parquet) · pyqlib 0.9.8.dev32(源码安装) · LightGBM · Next.js 15 · **TradingView Lightweight Charts 4.2.3**(唯一图表基座) · Redis(本机 127.0.0.1:6379 已就绪,触发时接入)
|
||||
|
||||
---
|
||||
|
||||
@@ -67,7 +67,7 @@ cp .env.example .env
|
||||
|---|---|---|
|
||||
| `TUSHARE_TOKEN` | 同步数据时必填 | Tushare Pro token |
|
||||
| `LLM_API_KEY` | 使用 Agent 时必填 | 大模型 API Key(URL/模型名在 config.yaml) |
|
||||
| `DATABASE_URL` | 否 | 默认库 = `config.yaml → database.mysql`(MySQL `192.168.1.10/qlib`);设此项可覆盖(如切回 SQLite) |
|
||||
| `DATABASE_URL` | 否 | 默认库 = `config.yaml → database.mysql`(本机 MariaDB `127.0.0.1/qlib`);设此项可覆盖(如切回 SQLite) |
|
||||
| `MYSQL_PASSWORD` | 使用 MySQL 默认库时必填 | MySQL 密码(`config.yaml database.mysql.password_env` 引用;host/db/user 在 config.yaml) |
|
||||
| `APP_SECRET_KEY` | 否 | 应用密钥(未接登录,可暂不改) |
|
||||
|
||||
@@ -83,7 +83,7 @@ api: {prefix: "/api"}
|
||||
database:
|
||||
url_env: "DATABASE_URL"
|
||||
migrations_dir: ...
|
||||
mysql: {enabled: true, host: "192.168.1.10", port: 3306,
|
||||
mysql: {enabled: true, host: "127.0.0.1", port: 3306,
|
||||
db: "qlib", user: "qlib", password_env: "MYSQL_PASSWORD", charset: "utf8mb4"}
|
||||
# URL 优先级:DATABASE_URL 环境变量 > database.mysql 组装 > sqlite:///./data/quant.db 兜底
|
||||
data_source: {primary: "tushare", fallback: "sina", tushare_token_env: "TUSHARE_TOKEN"}
|
||||
@@ -111,7 +111,14 @@ uv run alembic upgrade head
|
||||
DATABASE_URL='mysql+pymysql://user:pass@host/db' uv run alembic upgrade head
|
||||
```
|
||||
|
||||
> 2026-09 已把数据从 SQLite(`data/quant.db`)全量迁移至 MySQL(`192.168.1.10:3306/qlib`),
|
||||
> 2026-09 已把数据从 SQLite(`data/quant.db`)全量迁移至 MySQL;随后默认库切换到
|
||||
> **本机 MariaDB 10.11(`127.0.0.1:3306/qlib`)**(服务器主机名 `summer`,socket
|
||||
`/opt/local/var/run/mariadb-10.11/mysqld.sock`,账号 `qlib@localhost`;实测后端进程只有
|
||||
`127.0.0.1:3306` 连接),与远端 `192.168.1.10/qlib` 为逐表一致副本(**远端已禁止作为 DB 目标**:
|
||||
`assert_db_target_allowed` 硬拦截,见 `AGENT.md` §0.1)
|
||||
> (迁移当时 stock_daily 7,792,869 / adjust_factor 7,898,717;**当前 Alembic head
|
||||
> `a3f8c21d9b47`** = 迁移链 `f5e0d1c2b3a4 → 22d7380706f7(daily_basic) →
|
||||
> 7b1c4e9a52d8(result_json→MEDIUMTEXT) → a3f8c21d9b47(stock_name_history)`)。
|
||||
> 迁移工具与校验见 `scripts/migrate_sqlite_to_mysql.py`(`--verify-only` 可复查一致性)。
|
||||
|
||||
---
|
||||
@@ -120,16 +127,37 @@ DATABASE_URL='mysql+pymysql://user:pass@host/db' uv run alembic upgrade head
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
uv run python -m app.cli.sync basic # 股票基础信息(全市场)
|
||||
uv run python -m app.cli.sync basic # 股票基础信息(在市 L)
|
||||
uv run python -m app.cli.sync basic --include-delisted # 追加已退市 D / 暂停上市 P(含 delist_date)
|
||||
uv run python -m app.cli.sync namechange # 名称变更历史(时点 ST 判定,按年分片)
|
||||
uv run python -m app.cli.sync calendar --start 20240101 --end 20241231
|
||||
uv run python -m app.cli.sync daily --symbols 600519.SH,000858.SZ --start 20230101
|
||||
uv run python -m app.cli.sync daily --all --start 20240101 --resume # 全市场 + 断点续传
|
||||
uv run python -m app.cli.sync financial --all # 财务指标(默认增量)
|
||||
uv run python -m app.cli.sync financial --all --full # 财务指标(强制全量重拉)
|
||||
uv run python -m app.cli.sync daily_basic --start 20200101 # 每日指标(股息率/PE/PB/市值)
|
||||
uv run python -m app.cli.sync verify --symbol 600519.SH # 新浪交叉验证
|
||||
uv run python -m app.cli.sync export [--years 2023,2024] # 日线按年导出 Parquet
|
||||
```
|
||||
|
||||
- `basic --include-delisted`:Tushare `stock_basic` 默认只返回**在市**股票,因此
|
||||
`delist_date` 恒为空、退市股整体缺失(回测幸存者偏差的根因)。加该参数后按
|
||||
`list_status=L/P/D` 分别拉取并写入 `status`/`delist_date`(实测 2019-12 之后退市 230 只)。
|
||||
⚠️ **退市股的历史行情必须另行同步**,否则池子里有票、没行情:
|
||||
`sync daily --symbols <退市代码> --start 20190101`。时点股票池语义由
|
||||
`filter_stocks` 保证(`delist_date < as_of` 排除),无需额外配置。
|
||||
- `namechange`:Tushare `namechange` 的**名称生效区间**(实测 14,213 行,覆盖 1990-12 起)。
|
||||
用途:把 `universe.exclude_st` 从「最新名称快照」升级为**时点名称** ——
|
||||
`stock.name` 是最新快照,用它判定会把「曾为高股息、后来才变 ST/退市」的标的
|
||||
在整段历史里排除,而那正是**股息陷阱**样本(实测影响约 3.70pp 收益)。
|
||||
回测在**每个择股日**按当时名称重判,与 `/api/selections` 的单时点口径一致;
|
||||
未同步时自动回退最新名称,并在结果 `config_snapshot.price_basis.name_basis`
|
||||
标注 `point_in_time=false`(不静默)。
|
||||
- `daily_basic`:Tushare `daily_basic` 接口的每日指标(`dv_ratio` 股息率 / `dv_ttm` /
|
||||
`pe` / `pb` / `total_mv` 等),是高股息类策略的数据基础。**按缺失交易日续跑**,
|
||||
可中断重跑;每 20 个交易日提交一次进度。仅 Tushare 提供,新浪不支持
|
||||
(`DataSourceNotSupported`,不做假兜底)。
|
||||
|
||||
- 每次拉取写入 `sync_log` 审计(来源 / 成功与否 / 行数 / 区间),禁止静默切换数据源。
|
||||
- `daily` 同时写入日线与复权因子;`--resume` 从本地最新交易日续传。`financial`
|
||||
默认增量:本地已含「最新应披露报告期」(按 A 股披露节奏推算)的股票直接跳过;
|
||||
@@ -238,10 +266,58 @@ curl -X POST http://127.0.0.1:8000/api/backtests \
|
||||
}'
|
||||
```
|
||||
|
||||
**高股息案例(两级截断 + 双周期 + 复权 + 条件过滤)**——同一 spec 在页面/API/脚本一致:
|
||||
|
||||
```bash
|
||||
curl -X POST http://127.0.0.1:8000/api/jobs \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{
|
||||
"type": "backtest",
|
||||
"universe": {"exclude_st": true, "min_listing_days": 250},
|
||||
"price_adjustment": "hfq",
|
||||
"factors": [{"name": "dividend_yield", "weight": 1.0}],
|
||||
"conditions": [{"field": "dv_ratio", "op": "lte", "value": 30}],
|
||||
"selection": {"top_n": 20, "hold_top_x": 20,
|
||||
"allow_substitute": false, "defer_buy": true},
|
||||
"rebalance": "monthly",
|
||||
"selection_interval_months": 6,
|
||||
"rebalance_interval_months": 6,
|
||||
"costs": {"commission_rate": 0.0003, "stamp_tax_rate": 0.0005,
|
||||
"slippage_rate": 0.001, "min_commission": 5.0},
|
||||
"initial_capital": 1000000,
|
||||
"period": ["2020-01-01", "2026-09-04"]
|
||||
}'
|
||||
```
|
||||
|
||||
一键脚本(含指标打印与结果落盘,走真实 Job 链路):
|
||||
|
||||
```bash
|
||||
cd backend && PYTHONPATH=. .venv/bin/python ../scripts/run_dividend_case.py --help
|
||||
```
|
||||
|
||||
新增 spec 字段说明:
|
||||
|
||||
| 字段 | 语义 | 默认 |
|
||||
|---|---|---|
|
||||
| `selection.top_n`(n) | 择股条件/因子排序后**候选池**大小 | 30 |
|
||||
| `selection.hold_top_x`(x) | 实际持仓数,**必须 ≤ n**(超出直接校验失败) | = n |
|
||||
| `selection.allow_substitute` | 买不进时是否往 n 名之外替补 | `true`(向后兼容) |
|
||||
| `selection.defer_buy` | 买不进时**顺延到之后首个不涨停交易日**买入 | `false` |
|
||||
| `selection_interval_months`(m) | 每 m 个月择股一次(锚定起始月) | 空 = 每次调仓都择股 |
|
||||
| `rebalance_interval_months`(y) | 每 y 个月调仓一次 | = m |
|
||||
| `conditions[]` | 选股过滤条件(AND,universe 之后、排序之前) | 空 |
|
||||
| `price_adjustment` | `none` / `qfq` / `hfq`(回测与成交价同源折算) | `none` |
|
||||
| `costs.min_commission` | 单笔最低佣金(元) | `0` |
|
||||
|
||||
> `allow_substitute` 与 `defer_buy` **互斥**(同时为 `true` 会被拒绝):
|
||||
> 「换一只买」与「等它不涨停」是两种互斥的补位哲学。
|
||||
> 顺延单只在两次调仓之间有效,到下一次调仓仍未成交则作废(资金留作现金)。
|
||||
|
||||
| 端点 | 说明 |
|
||||
|---|---|
|
||||
| `GET /api/health` | 健康检查 |
|
||||
| `GET /api/stocks?q=600519&limit=20` | 股票列表/搜索 |
|
||||
| `GET /api/stocks?q=600519&limit=20` | 股票列表/搜索(单页上限 500) |
|
||||
| `GET /api/stocks/names` | **`{symbol: name}` 全市场名称映射**(5,900+ 条,前端启动一次性拉取并缓存,避免表格逐行查名称) |
|
||||
| `GET /api/stocks/{symbol}` | 单只股票详情 |
|
||||
| `GET /api/factors` | 因子目录(读 `factor_definition` 表;空表自动 seed) |
|
||||
| `POST /api/composites` / `GET /api/composites` | 因子组合保存 / 列表(方向由注册表填充) |
|
||||
@@ -249,16 +325,20 @@ curl -X POST http://127.0.0.1:8000/api/backtests \
|
||||
| `GET /api/selections/{id}` / `GET /api/selections` | 读回 / 历史选股(`as_of`、`method` 过滤) |
|
||||
| `POST /api/signals` | 生成 BUY/WATCH/SELL 信号(评分+规则)→ 落库 |
|
||||
| `GET /api/signals/{id}` / `GET /api/signals` | 读回 / 历史信号 |
|
||||
| `POST /api/strategies` / `GET /api/strategies` | 保存 / 列表命名策略 |
|
||||
| `POST /api/strategies` / `GET /api/strategies` | 保存(`description` 留空时自动用推导出的一句话说明补全)/ 列表命名策略 |
|
||||
| `GET /api/strategies/{id}` / `PUT /api/strategies/{id}` / `DELETE /api/strategies/{id}` | 读取 / **原地更新**(保留 `id` 与 `created_at`,重名 → 400,不存在 → 404)/ 删除 |
|
||||
| `POST /api/strategies/{id}/expand` | 展开为回测 ResearchSpec(period + 初始资金) |
|
||||
| `POST /api/factor-tests` | 同步单因子测试 → `FactorTestReport` |
|
||||
| `POST /api/backtests` | 同步回测 → `BacktestResult`(默认 LocalEngine) |
|
||||
| `GET /api/strategies/{id}/describe` | **策略说明**:`{summary, formula, steps, warnings}`(由已保存 spec 推导) |
|
||||
| `POST /api/strategies/describe` | 同上,但直接传 `ResearchSpec` —— 用于**未保存参数**的实时预览 |
|
||||
| `POST /api/factor-tests` | 同步单因子测试 → `FactorTestReport`;**结果同样写入归档**(kind=`factor_test`),归档 id 见响应头 `X-Experiment-Id` |
|
||||
| `POST /api/backtests` | 同步回测 → `BacktestResult`(默认 LocalEngine);**结果同样写入归档**,归档 id 见响应头 `X-Experiment-Id` |
|
||||
| `GET /api/backtests/last` | 最近一次同步回测 |
|
||||
| `POST /api/jobs` | 创建异步 Job(同 spec),返回 `{"job_id","status":"queued"}` |
|
||||
| `GET /api/jobs/{id}` | 查询状态;成功时内嵌 `result` |
|
||||
| `GET /api/jobs/{id}/events` | SSE 进度(`curl -N ...`) |
|
||||
| `GET /api/experiments` | 实验归档列表(收益摘要/代码版本) |
|
||||
| `GET /api/experiments/{id}` | 实验详情(spec + 完整结果) |
|
||||
| `GET /api/experiments` | **归档列表**:支持 `kind`(backtest/factor_test/selection)与 `q`(匹配 id/因子/摘要)过滤、`limit`/`offset`;**过滤后总数放在 `X-Total-Count` 响应头**(body 保持数组形状) |
|
||||
| `GET /api/experiments/{id}` | 归档详情:`spec`(复现依据)+ 完整 `result` + `code_version` / **`data_version`** / `job_id` / `result_bytes` |
|
||||
| `DELETE /api/experiments/{id}` | 删除一条归档(不存在 → 404)。**同时等于删掉这次回测的结果**:完整结果只存归档一份,Job 记录仍在但 `GET /api/jobs/{id}` 的 `result` 会变成 `null`(并给 `result_unavailable_reason` 如实说明)。要留底请先导出 JSON |
|
||||
| `POST /api/experiments/{id}/rerun` | 一键复跑 → 新 Job |
|
||||
| `GET /api/stocks/{symbol}/chart\|signals\|selections` | Chart API:K线/量/MA/复权显示 + 该股历史标记 |
|
||||
| `GET /api/backtests/{experiment_id}/stocks/{symbol}/chart\|trades\|positions` | 回测个股图(fills/成交)/明细 |
|
||||
@@ -272,6 +352,93 @@ curl -X POST http://127.0.0.1:8000/api/backtests \
|
||||
`announce_date` 之前已公告内容;成本(佣金/印花税/滑点)、涨跌停、停牌已建模;
|
||||
未建模约束(如开盘一字路径)会写入结果的 `unimplemented` 数组,前端如实展示。
|
||||
|
||||
### 6.3 Web 研究工作台:一条闭环走到底
|
||||
|
||||
侧栏「策略」分组把研究闭环的四个环节串成一条线,顺序即引导:
|
||||
|
||||
| 环节 | 页面 | 做什么 | 关键改进 |
|
||||
|---|---|---|---|
|
||||
| ① 出候选 | `/selection` | 因子评分 TopN / 条件选股(可指定历史时点) | 候选表**代码 + 名称**且可点击进个股页;结果卡带「**按此条件回测**」 |
|
||||
| ② 定规则 | `/strategies` | 命名保存选股规则,管理增删改查 | 每个策略展示**一句话说明 + 计算公式 + 执行步骤 + 注意事项**(后端由 spec 真实推导),列表内可直接**一键回测**,编辑为**原地更新** |
|
||||
| ③ 验规则 | `/backtest` | 两级截断(候选池 n → 持仓 x)+ 双周期(每 m 月择股 / 每 y 月调仓)回测 | 净值/个股曲线标买卖点;参数区与「策略说明」同屏实时联动;**保存为策略**;**载入上次结果**(免重跑);作业显示**真实阶段**与已用时间;结果分区锚点导航 |
|
||||
| ④ 复盘 | `/experiments` | 勾选 2~3 次回测对比;**搜索/按类型筛选**后「打开归档」 | 归一化净值曲线叠加 + 指标差值表(✅/⚠️ 标注改善方向)+ `config_snapshot` **参数 diff**(默认只显示有差异项)|
|
||||
| ⑤ 存档 | `/experiments/{id}` | **只读归档快照**:任意时候都能翻回来看 | 明确回答「**选股条件**」与「**交易执行依据**」+ 完整结果(与刚跑完时同一套图表)+ 归档元数据(代码版本/数据版本)+ 导出 JSON |
|
||||
|
||||
三条「直通」链路(都可分享 URL、刷新后仍生效):
|
||||
|
||||
```
|
||||
/selection ──「按此条件回测」──▶ /backtest?from_selection=SEL-xxxx 预填条件/因子/TopN/复权口径
|
||||
/strategies ──「一键回测」────▶ /backtest 展开 spec → 提交 Job(页内出指标)
|
||||
/experiments ──「以此参数再跑」─▶ /backtest?from_experiment=EXP-xxxx 复用该实验的参数快照
|
||||
回测跑完 ──「打开归档(完整快照)」─▶ /experiments/EXP-xxxx 直接查看这次结果的冻结快照
|
||||
```
|
||||
|
||||
> 归档是**只读**的:`/experiments/{id}` 用 URL 表达「这就是那次回测」,刷新/换设备/分享链接都能看。
|
||||
> 页面顶部两块表明确定义了这次回测的口径:
|
||||
> **① 选股条件**(股票池 / 因子与权重及方向 / 过滤条件 / 两级截断 n→x / 择股周期 m / 调仓周期 y / 买不进时怎么办)
|
||||
> **② 交易执行依据**(成交时点=调仓日收盘、复权口径、滑点后的买/卖价、佣金与印花税与最低佣金、
|
||||
> 涨停/跌停/停牌如何拦单、顺延规则、期末是否平仓、对照基准),
|
||||
> 并附后端按归档 `spec` 推导的**一句话说明 + 计算公式 + 执行步骤 + 注意事项**(与引擎实执行规则同源)。
|
||||
|
||||
> ⚠️ 口径提醒(页面上也有同样提示):从选股结果进入回测,预填的是**规则**(条件/因子/TopN/口径),
|
||||
> 回测会在**每个择股日按同一规则重新选股**,不是固定持有那一次选出的股票。
|
||||
|
||||
#### 6.3.1 归档的日常操作(查看 / 导出 / 删除 / 恢复)
|
||||
|
||||
| 想做什么 | 在哪做 | 说明 |
|
||||
|---|---|---|
|
||||
| **查看** | `/experiments` 每行「打开归档」,或回测跑完提示条「打开归档(完整快照)」 | 进入 `/experiments/{id}`:**选股条件** + **交易执行依据** + 完整结果 + 归档元数据。URL 可分享、刷新不丢 |
|
||||
| **筛选找归档** | `/experiments` 顶部搜索框 + 类型下拉 | 条件是 `q`(匹配 id/因子/摘要)与 `kind`;**筛选会回写 URL**(`/experiments?kind=backtest&q=dividend`),可分享、刷新保持;总数显示「显示 N 条 / 共 M 条」,不静默截断 |
|
||||
| **导出留底** | 归档页「**导出完整 JSON**」 | 把该归档的 `spec` + `result` 原样下载(字节级一致),是最可靠的留底方式 |
|
||||
| **看原始 spec** | 归档页「查看原始 spec(JSON)」 | 展开后端实际收到的请求体,逐字核对参数 |
|
||||
| **复用参数再跑** | 归档页「**以此参数再跑**」→ `/backtest?from_experiment=EXP-xxxx` | 参数预填但**不自动执行**(长区间一次 3~5 分钟,由你决定何时跑) |
|
||||
| **参与对比** | 归档页「加入对比」/ 列表勾选 | 最多 3 条,跳到 `/experiments` 的对比视图 |
|
||||
| **删除** | 归档页「**删除归档**」(带确认框) | 调 `DELETE /api/experiments/{id}`,成功后回到列表 |
|
||||
|
||||
**删除前必须知道的事**(确认框里也写了同样的内容):
|
||||
|
||||
- 删除后这份快照(结果 + spec)**无法再查看,也不可恢复**;
|
||||
- **完整结果只存归档一份**:删除后连该次执行的作业记录也读不回结果
|
||||
(`GET /api/jobs/{id}` 的 `result` 变 `null`,并给出 `result_unavailable_reason`);
|
||||
- 想留底请先点「导出完整 JSON」。
|
||||
|
||||
**但是历史归档能救回来**(`job` 表另有副本),边界如下:
|
||||
|
||||
| 归档 | `job.result_json` | 删除后能否重建 |
|
||||
|---|---|---|
|
||||
| 完整存档上线**前**的历史归档 | 有副本(双写遗留) | **能**,按原 id 重建 |
|
||||
| 完整存档上线**后**的新归档 | `NULL` | **不能**,工具明确拒绝,不假装能救 |
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
# 保留策略:默认 dry-run,只打印将要删的清单;确认后加 --apply
|
||||
PYTHONPATH=. .venv/bin/python -m app.cli.prune_experiments --keep 10
|
||||
PYTHONPATH=. .venv/bin/python -m app.cli.prune_experiments --keep 10 --kind backtest --apply
|
||||
|
||||
# 从 Job 副本重建某条已删除的历史归档(默认 dry-run;code-version 必须显式给,不猜)
|
||||
PYTHONPATH=. .venv/bin/python -m app.cli.restore_experiment_from_job \
|
||||
--job-id JOB-XXXXXXXX --code-version 92627f5 # 加 --apply 才写库
|
||||
```
|
||||
|
||||
**归档完整度会明示**:归档页顶部标注「归档完整 N/N」;若曲线因体积预算被裁剪,或该快照产生于
|
||||
完整存档上线之前(个股曲线只有 60 只),页面直接显示「**归档不完整**」并给出差异与「以此参数重跑」入口。
|
||||
|
||||
**图表**:全站统一使用 **TradingView Lightweight Charts**(`components/charts/LwChart.tsx`),
|
||||
ECharts 已从依赖中移除。买卖点标记(▲绿=买入 / ▼红=卖出)只落在该 series 真实存在的交易日上,
|
||||
并按时间升序提交(Lightweight Charts 的硬约束),因此不会出现标记丢失或错位。
|
||||
|
||||
**股票名称**:后端在 `SelectionCandidate` / `SymbolCurve` / `ActionRecord` / `RankedPick` /
|
||||
`Position` / `Trade` 上都填充 `name`(由 `GET /api/stocks/names` 一次性映射),
|
||||
前端 `SymbolLink` 保证「有代码必有名称、且可点击进入个股页(基本信息 + 走势图)」;
|
||||
名称确实缺失时显示灰色「—」而**不猜测、不臆造**。
|
||||
|
||||
**自检脚本**(接口 + 页面契约,约 2 分钟含一次真实回测):
|
||||
|
||||
```bash
|
||||
cd backend && PYTHONPATH=. .venv/bin/python ../scripts/verify_strategy_workspace.py
|
||||
# 只验接口与页面(跳过回测 Job):加 --skip-job
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. AI Agent
|
||||
@@ -317,17 +484,28 @@ Agent 能力边界(10 个内置受控工具,只读 + 受控写库):
|
||||
```bash
|
||||
cd backend
|
||||
uv run ruff check app tests && uv run ruff format --check app tests
|
||||
uv run pytest # 全量测试(每个里程碑提交前均须通过)
|
||||
uv run pytest # 全量测试(每个里程碑提交前均须通过,当前 388 条)
|
||||
|
||||
cd frontend/web
|
||||
pnpm run typecheck
|
||||
pnpm run build
|
||||
pnpm run typecheck # tsc --noEmit,0 error
|
||||
pnpm run test:charts # 图表标记逻辑单测(7 条,node --test,无需额外依赖)
|
||||
pnpm run build # 13 条路由,含 /strategies /backtest /experiments /experiments/[id]
|
||||
```
|
||||
|
||||
**端到端契约脚本**(会真实提交回测 Job,用于验证「页面实现」与「后端字段」不漂移):
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
PYTHONPATH=. .venv/bin/python ../scripts/verify_strategy_workspace.py # 策略库/说明/名称/选股直通/归档链路(56 项,约 4 分钟)
|
||||
PYTHONPATH=. .venv/bin/python ../scripts/verify_backtest_page_contract.py # 回测结果结构契约(约 4 分钟)
|
||||
```
|
||||
|
||||
覆盖重点:Provider 归一化与 Failover 审计、Repository 幂等与「未来函数阻断」
|
||||
(`as_of_date`/`announce_date`)、Alembic 迁移、因子/IC/分层、回测记账与成本/涨跌停、
|
||||
**数据库目标守卫**(`192.168.1.10` 被拒 / 本机放行 / 列表可用环境变量覆盖)、
|
||||
**QlibEngine 数据管线**(bin 落盘格式/roundtrip/端到端回测)、Job 状态机与 Experiment
|
||||
归档、Agent 工具白名单与编排、LLM 配置加载、API 端到端。
|
||||
归档、Agent 工具白名单与编排、LLM 配置加载、API 端到端、**策略说明推导**
|
||||
(`describe_strategy` 的分支/互斥/缺失值语义)、**股票名称回填**(回测与选股两侧)。
|
||||
|
||||
---
|
||||
|
||||
@@ -338,17 +516,78 @@ pnpm run build
|
||||
(见 docs/DEV_PLAN_v2.md §6.4)。
|
||||
2. **全市场选股为同步请求**:无白名单的全市场选股约需 60s+;Agent 工具要求传 `symbols`
|
||||
白名单,全市场请在 Web 页执行(后续可迁异步 Job)。
|
||||
3. **数据规模**:当前 MySQL 已同步全市场(stock 5556 只、stock_daily 约 780 万行、
|
||||
adjust_factor 约 790 万行,2026-09 由 SQLite 迁移并校验一致)。
|
||||
3. **数据规模**:当前 MySQL 已同步全市场 + 退市股(stock 5903 只 = 在市 5565 + 退市 338、
|
||||
stock_daily 约 805 万行/5786 只、adjust_factor 约 819 万行、daily_basic 约 800 万行
|
||||
/1629 个交易日,2026-09 由 SQLite 迁移并校验一致)。
|
||||
4. **回测为近似建模**:涨跌停按收盘相对上一有效收盘判定、成交假设调仓日收盘,
|
||||
Portfolio v1 仅等权(单股/行业上限约束字段已预留但未建模,设置后会在结果
|
||||
`unimplemented` 如实标注)(详见结果 `unimplemented`)。
|
||||
5. **Agent 结论质量取决于模型**:编排只保证「经受控工具 + 归档留痕」,研究有效性判断
|
||||
5. **幸存者偏差(已修复)**:
|
||||
- 退市股:`sync basic --include-delisted` + 定向 `sync daily`,实测 338 只、24.0 万根;
|
||||
时点股票池按 `delist_date` 正确纳入/排除(实测 `000005.SZ` 2024-04-26 退市:
|
||||
2024-01-02 纳入 / 2024-06-03 排除)。
|
||||
- ST/风险警示:`sync namechange`(14,213 行名称生效区间,覆盖 1990-12 起),
|
||||
`exclude_st` 在**每个择股日**按当时名称重判,与 `/api/selections` 同口径。
|
||||
实测 2020-01-02 时点口径比快照口径**多纳入 236 只**(当时尚未 ST 的标的)。
|
||||
案例全区间收益因此经历三次修正:+35.71%(最新名称快照)→ +32.01%(仅在池子基准日过滤)
|
||||
→ **+24.86%(逐择股日时点 ST,最终)**;即初始口径高估 **10.85pp**。
|
||||
窗口内真实反例:`000961.SZ` 阳光城 2023-05-05 起 ST、`600466.SH` 中南建设
|
||||
2024-04-24 起 ST,只按起始日过滤会让它们在变 ST 后仍被选中。
|
||||
- 残余(如实标注):股票池的 `min_listing_days`/`delist_date` 仍以**回测起始日**为基准
|
||||
(池子不逐日重算),故「起始日为 ST、之后摘帽」的标的不会进入该次回测候选池;
|
||||
名称历史未同步时自动回退最新名称并在 `name_basis` 标注 `point_in_time=false`。
|
||||
|
||||
6. **老牌蓝筹缺 2020–2022 行情(已修复)**:实测缺口 **20 只**(含 `600028.SH` 中石化、
|
||||
`600900.SH` 长江电力、`601398.SH` 工商银行等高股息主力),本地日线原自 2023-01-03 起;
|
||||
已回补 19,419 根、数据现自 2019-01-02 起。另有 **5 只**(盈方微/盐湖股份/皇台酒业/
|
||||
深深房A/中毅达)是该区间的**真实长期停牌**(停牌期本就无行情),非数据缺失。
|
||||
7. **停牌无独立数据表**:以「当日无行情」近似停牌(不可买不可卖),未接入
|
||||
tushare `suspend_d` 明细(`ARCHITECTURE.md` §8 的 `suspend_data` 尚未落地),
|
||||
`exclude_suspended` 仍未实现(结果 `unimplemented` 已标注)。
|
||||
8. **结果体积与完整存档**:全市场多年回测的结果 JSON 可达数 MB,
|
||||
`experiment.result_json` 为 `MEDIUMTEXT`(16MB)以容纳。
|
||||
- **个股收益曲线默认全量保存**(此前只存收益绝对值最大的 60 只,是个真实缺陷):
|
||||
实测案例 2020-01-01~2026-09-04 回测期内持有 **135 只**,归档 `symbol_curves` 为
|
||||
**135 条**、结果 **1.86 MB**,`archive_meta` 记录
|
||||
`{curves_stored:135, curves_total:135, truncated:false, budget_chars:12000000}`。
|
||||
- 上限可用 `research.archive_curve_limit` 配置(默认不限制);只有当结果超出
|
||||
体积预算(12,000,000 字符,为 MEDIUMTEXT 16MB 留余量)时才会按收益绝对值裁剪,
|
||||
并**同时**写入 `archive_meta.truncated=true` 与 `unimplemented` 说明 ——
|
||||
归档页会把「归档完整 / 归档不完整」直接标出来,**不静默丢数据**。
|
||||
- **老归档(完整存档上线前)确实不完整**:归档页对这类快照显示
|
||||
「个股曲线只有 60 只(期内共持有 N 只)」并给出「以此参数重跑」入口。
|
||||
- 归档列表保留策略:`python -m app.cli.prune_experiments --keep N`(**默认 dry-run**,
|
||||
加 `--apply` 才删除)。注意删除即失去结果(结果只存归档一份),建议先导出要紧的归档;
|
||||
早期归档在 `job` 表另有副本,但新记录没有(`job.result_json` 为 NULL)。
|
||||
- 删除后的恢复路径:历史归档可用
|
||||
`python -m app.cli.restore_experiment_from_job --job-id <JOB> --code-version <rev> --apply`
|
||||
按原 id 重建(spec/result 逐字复制、摘要按归档同口径重算、`data_version` 留空不伪造);
|
||||
新归档没有副本,工具会明确拒绝。操作细节见 §6.3.1。
|
||||
9. **Agent 结论质量取决于模型**:编排只保证「经受控工具 + 归档留痕」,研究有效性判断
|
||||
需要人复核;接不同厂商模型请核对 `config.yaml` 的 `base_url` 与 `model` 命名。
|
||||
6. **SSE 与 Job 为单进程本地执行**:重启进程后未完成 Job 需重新提交(第一阶段刻意
|
||||
10. **SSE 与 Job 为单进程本地执行**:重启进程后未完成 Job 需重新提交(第一阶段刻意
|
||||
不引入 Redis/Celery;本机 Redis 已就绪,出现排队/长任务需求时按 DEV_PLAN §7 接入)。
|
||||
7. **Qlib 数据为进程内单例初始化**:`qlib.init` 以 provider_uri 为键幂等,切换不同
|
||||
qlib_dir 会重新初始化;测试环境建议注入独立临时目录。
|
||||
11. **Qlib 数据为进程内单例初始化**:`qlib.init` 以 provider_uri 为键幂等,切换不同
|
||||
qlib_dir 会重新初始化;测试环境建议注入独立临时目录。
|
||||
12. **策略说明列宽 300(未加迁移)**:`strategy.description` 列是 `String(300)`,而
|
||||
`describe_strategy` 生成的自动说明最长可达 313 字符 → 后端按 300 截断并加显式 `…`,
|
||||
前端表单同步 `maxLength=300` 并实时计数。要让长说明完整落库,需要单独做一次
|
||||
`Model → Alembic`(把列改 `Text`)的 schema 变更。
|
||||
13. **异步选股不落库**:`POST /api/selections/jobs` 只建 Job、不写 `selection_snapshot`,
|
||||
因此异步路径下 `/selection` 不渲染「按此条件回测」按钮(不给坏链接);同步
|
||||
`POST /api/selections` 会落库并返回 `selection_id`,可正常直通回测。
|
||||
14. **同步接口也会写归档**:`POST /api/backtests` 与 `POST /api/factor-tests` 每次调用都会
|
||||
新增一条归档(归档 id 见响应头 `X-Experiment-Id`)。批量试参数会在库里留下大量归档,
|
||||
用 `prune_experiments` 按 `--keep` 清理。归档失败不会吞掉计算结果(接口仍返回结果,
|
||||
失败原因经响应头 `X-Archive-Error` 与日志如实暴露)。
|
||||
15. **归档的「数据版本」只对上线后产生的归档有效**:老归档 `data_version` 为空,
|
||||
归档页显示「未记录(该归档早于数据指纹上线)」,并提示不要当作可严格复现的依据;
|
||||
指纹形如 `d20260904;n≈7688k;a≈7922k;b≈7718k`(`d`=最新交易日、`n`=stock_daily、
|
||||
`a`=adjust_factor、`b`=daily_basic,`≈` 表示取自 `information_schema` 的近似行数)。
|
||||
16. **个股页「回测成交与信号」卡的数据边界**:`GET /api/stocks/{symbol}/chart` 的
|
||||
`fills/signals` 来自信号表与选股命中;**回测成交**要经
|
||||
`/api/backtests/{experiment_id}/stocks/{symbol}/chart` 才有,因此在没有信号/命中记录时
|
||||
该卡不渲染(个股页的 K 线、基本信息、复权切换不受影响)。
|
||||
|
||||
---
|
||||
|
||||
@@ -364,11 +603,13 @@ pnpm run build
|
||||
`pd.read_parquet(...)` 直接读取(pyarrow 已随依赖安装)。
|
||||
- **前端连不上后端 / NetworkError**:前端默认经 Next **同源代理**访问 `/api/*`
|
||||
(next.config.ts rewrites → `127.0.0.1:8000`,可用 `BACKEND_API_URL` 覆盖),
|
||||
任意 IP 访问 `:3000` 都不需要 CORS 或硬编码后端地址;后端 CORS 开发期为 `*`。
|
||||
后端 CORS 开发期为 `*`。前端 dev server 监听 `0.0.0.0:3000`(`dev.sh` 传 `-H 0.0.0.0`),
|
||||
但**访问来源必须在 `next.config.ts` 的 `allowedDevOrigins` 白名单内**,否则其
|
||||
`/_next/*` 请求会被 Next 15.5+ 拒绝(表现为页面白屏/资源 403);换机器访问时按需追加来源 IP。
|
||||
若使用 SQLite 兜底库且日志出现 `database is locked`,多半是正在跑全市场数据
|
||||
同步(长写事务),同步结束后自动恢复(SQLite 连接已加 busy_timeout);
|
||||
默认 MySQL 库无此问题。
|
||||
- **数据库被改动想重置**:默认 MySQL(qlib@192.168.1.10)时在远端重建后
|
||||
- **数据库被改动想重置**:默认库为本机 MariaDB(qlib@127.0.0.1)时重建库后
|
||||
`uv run alembic upgrade head`(行情需重新同步或从备份恢复);若切回 SQLite 兜底库则删除
|
||||
`data/quant.db` 后重建。
|
||||
- **默认库已是 MySQL**(config.yaml `database.mysql`,密码在 `.env` 的 `MYSQL_PASSWORD`);
|
||||
|
||||
@@ -4924,8 +4924,8 @@
|
||||
<path data-edge-from="agent" data-edge-to="services" data-edge-label="受控 Tool(Phase 5)" data-edge-key="9" data-edge-id="agent-tools" data-composition-points="270,71;335,71;335,279;400,279" d="M 270 71 L 327 71 Q 335 71 335 79 L 335 271 Q 335 279 343 279 L 400 279" class="a-dashed" stroke-width="1.5" marker-end="url(#arrowhead-dashed)"/>
|
||||
|
||||
<!-- Components -->
|
||||
<g id="node-web-ui" data-node-id="web-ui" data-node-label="Web UI" tabindex="0" role="button" aria-label="Focus Web UI, Next.js · ECharts, Architecture component" aria-pressed="false" data-node-kind="frontend" data-node-sublabel="Next.js · ECharts" data-node-context="Architecture component">
|
||||
<title>Web UI · Next.js · ECharts · Architecture component</title>
|
||||
<g id="node-web-ui" data-node-id="web-ui" data-node-label="Web UI" tabindex="0" role="button" aria-label="Focus Web UI, Next.js · Lightweight Charts, Architecture component" aria-pressed="false" data-node-kind="frontend" data-node-sublabel="Next.js · Lightweight Charts" data-node-context="Architecture component">
|
||||
<title>Web UI · Next.js · Lightweight Charts · Architecture component</title>
|
||||
<rect x="460" y="16" width="320" height="64" rx="6" class="c-mask"/>
|
||||
<rect x="460" y="16" width="320" height="64" rx="6" class="c-frontend" stroke-width="1.5"/>
|
||||
<g aria-hidden="true" data-semantic-sigil="frontend" class="semantic-sigil s-frontend" transform="translate(466 22) scale(0.6875)">
|
||||
@@ -4935,7 +4935,7 @@
|
||||
<circle cx="6.3" cy="4.8" r=".7" class="sigil-fill"/>
|
||||
</g>
|
||||
<text data-detail-anchor x="620" y="46" class="t-primary" font-size="11" font-weight="600" text-anchor="middle">Web UI</text>
|
||||
<text data-detail="context" x="620" y="62" class="t-muted" font-size="9" text-anchor="middle">Next.js · ECharts</text>
|
||||
<text data-detail="context" x="620" y="62" class="t-muted" font-size="9" text-anchor="middle">Next.js · Lightweight Charts</text>
|
||||
</g>
|
||||
|
||||
<g id="node-api" data-node-id="api" data-node-label="FastAPI API" tabindex="0" role="button" aria-label="Focus FastAPI API, Pydantic DTO · REST / SSE, Architecture component" aria-pressed="false" data-node-kind="backend" data-node-sublabel="Pydantic DTO · REST / SSE" data-node-context="Architecture component">
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
]
|
||||
},
|
||||
"components": [
|
||||
{ "id": "web-ui", "type": "frontend", "label": "Web UI", "sublabel": "Next.js · ECharts", "pos": [460, 16], "size": [320, 64] },
|
||||
{ "id": "web-ui", "type": "frontend", "label": "Web UI", "sublabel": "Next.js · Lightweight Charts", "pos": [460, 16], "size": [320, 64] },
|
||||
{ "id": "api", "type": "backend", "label": "FastAPI API", "sublabel": "Pydantic DTO · REST / SSE", "pos": [460, 118], "size": [320, 64] },
|
||||
{ "id": "services", "type": "backend", "label": "Application Services", "sublabel": "数据 / 因子 / 策略 / 回测 / 实验", "pos": [400, 232], "size": [440, 108] },
|
||||
{ "id": "domain", "type": "backend", "label": "Domain · Repository", "sublabel": "实体 + Protocol(禁 SQL)", "pos": [460, 392], "size": [320, 80] },
|
||||
|
||||
@@ -5079,8 +5079,8 @@
|
||||
<text data-detail="fine" x="960" y="493" class="t-backend" font-size="7" text-anchor="middle">REST / SSE</text>
|
||||
</g>
|
||||
|
||||
<g id="node-web" data-node-id="web" data-node-label="Web UI" tabindex="0" role="button" aria-label="Focus Web UI, Next.js · ECharts, 05 / 回测 · 实验 · 消费" aria-pressed="false" data-node-kind="frontend" data-node-sublabel="Next.js · ECharts" data-node-tag="结果可视化" data-node-context="05 / 回测 · 实验 · 消费">
|
||||
<title>Web UI · Next.js · ECharts · 05 / 回测 · 实验 · 消费 · 结果可视化</title>
|
||||
<g id="node-web" data-node-id="web" data-node-label="Web UI" tabindex="0" role="button" aria-label="Focus Web UI, Next.js · Lightweight Charts, 05 / 回测 · 实验 · 消费" aria-pressed="false" data-node-kind="frontend" data-node-sublabel="Next.js · Lightweight Charts" data-node-tag="结果可视化" data-node-context="05 / 回测 · 实验 · 消费">
|
||||
<title>Web UI · Next.js · Lightweight Charts · 05 / 回测 · 实验 · 消费 · 结果可视化</title>
|
||||
<rect x="904" y="582" width="112" height="36" rx="6" class="c-mask"/>
|
||||
<rect x="904" y="582" width="112" height="36" rx="6" class="c-frontend" stroke-width="1.5"/>
|
||||
<g aria-hidden="true" data-semantic-sigil="frontend" class="semantic-sigil s-frontend" transform="translate(910 588) scale(0.6875)">
|
||||
@@ -5090,7 +5090,7 @@
|
||||
<circle cx="6.3" cy="4.8" r=".7" class="sigil-fill"/>
|
||||
</g>
|
||||
<text data-detail-anchor x="960" y="603" class="t-primary" font-size="10" font-weight="600" text-anchor="middle">Web UI</text>
|
||||
<text data-detail="context" x="960" y="619" class="t-muted" font-size="7" text-anchor="middle">Next.js · ECharts</text>
|
||||
<text data-detail="context" x="960" y="619" class="t-muted" font-size="7" text-anchor="middle">Next.js · Lightweight Charts</text>
|
||||
<text data-detail="fine" x="960" y="607" class="t-frontend" font-size="7" text-anchor="middle">结果可视化</text>
|
||||
</g>
|
||||
|
||||
|
||||
@@ -192,7 +192,7 @@
|
||||
"id": "web",
|
||||
"type": "frontend",
|
||||
"label": "Web UI",
|
||||
"sublabel": "Next.js · ECharts",
|
||||
"sublabel": "Next.js · Lightweight Charts",
|
||||
"tag": "结果可视化",
|
||||
"stage": 4,
|
||||
"row": 4,
|
||||
|
||||
Reference in New Issue
Block a user