Files
zhixing-system/.trellis/tasks/08-08-strategy-execution-results/design.md
T

188 lines
9.7 KiB
Markdown
Raw 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.
# 策略执行结果持久化、HTTP 触发与查询设计
## 1. 设计目标
在现有 `selection` bounded context 上补齐一条可重跑的每日策略结果链路:用户在
Web 页面选择目标交易日并触发 `zhixing_b1`,HTTP 快速返回执行批次标识,服务端在
当前进程的异步批次中完成全股票池评估并持久化结果,页面轮询状态后展示信号明细。
一次重跑必须在同一数据库事务中清除指定“策略 + 目标交易日”的旧结果并创建新的
运行记录,避免旧信号和新信号混在一起。执行中的重复请求只返回冲突,不得并发清理
或重复计算同一批次。
本期不引入独立任务队列、定时器或其他策略;HTTP 是触发入口,FastAPI 进程内的
`BackgroundTasks` 是异步执行机制。
## 2. 上下文边界与模块分工
```text
zhixing-server/src/zhixing_server/modules/selection/
├── domain/
│ ├── models.py # 已有行情、信号和单股评估模型
│ ├── ports.py # 市场数据读取端口
│ └── runs.py # 批次状态、持久化读写端口和执行结果模型
├── application/
│ ├── evaluate.py # 已有单股评估用例
│ └── run.py # 全股票池批次编排、重跑和失败收敛
├── infrastructure/
│ ├── postgres_reader.py # 已有单股 qfq 历史读取,补充执行股票池读取
│ └── postgres_runs.py # selection 批次、item、signal 的 PostgreSQL 适配器
└── presentation/
└── http.py # Pydantic 请求/响应和 HTTP 依赖
```
- `selection.domain` 不依赖 FastAPI、Psycopg 或 PostgreSQL JSON 类型。
- `selection.application` 只依赖端口;批次执行负责逐股调用已有
`EvaluateZhixingB1`,不复制公式逻辑。
- `selection.infrastructure` 负责事务、锁、SQL、JSONB 序列化和市场同步批次关联。
- `selection.presentation.http` 只做边界校验、HTTP 状态映射和领域模型转换;由
`interfaces/http/router.py` 在 `/api/v1/selection` 下挂载。
- 前端新增 `features/selection` 垂直切片;页面可以依赖 `HomeShell` 和 shared UI,
`shared` 不反向依赖 selection。
## 3. 持久化模型与重跑事务
新增 Alembic migration `0002_selection_results`,同时更新
`modules/market_data/infrastructure/schema.py` 的 metadata。使用三张表:
### 3.1 `selection_run`
一行代表某个策略、目标交易日的一次当前执行尝试。
- `id`:UUID 字符串主键,作为异步轮询的 `run_id`。
- `strategy`、`target_trade_date`:业务身份;建立唯一约束,保证同一时点只有一条
当前尝试。
- `market_sync_batch_id`:引用产生输入数据的市场同步批次标识;跨上下文先保存
稳定 ID,不改变市场数据 bounded context 的写入所有权。
- `status`:`running`、`success`、`partial_success`、`failed`。
- `target_count`、`eligible_count`、`evaluated_count`、`selected_stock_count`、
`signal_count`、`failed_count`:批次汇总计数。
- `coverage`:从市场同步批次复制的覆盖率,使用 Numeric 保存精度。
- `error_type`、`error_message`:批次级失败上下文,可空且不保存 traceback 或凭据。
- `created_at`、`finished_at`:审计时间。
`selection_run_item` 以 `(run_id, ts_code)` 为主键,保存每只参与股票的名称、
评估状态、信号数量和可读原因。状态沿用领域评估状态:`selected`、`no_signal`、
`insufficient_history`、`missing_target_bar`、`data_error`。
`selection_signal` 以 `(run_id, ts_code, category)` 为主键,保存股票、目标日、
策略、子信号分类、qfq 收盘价和 JSONB `details`。同一股票同日的多个 category
分别落行,查询时按代码和公式优先级稳定排序。
### 3.2 首次执行、重跑和并发
`prepare_run(strategy, target_trade_date, rerun)` 在一个短事务中完成:
1. 使用按策略和日期派生的 PostgreSQL advisory transaction lock,串行化同一业务键。
2. 查询当前 `selection_run`。
3. `running` 时拒绝请求,返回 `409 run_in_progress`。
4. 已有终态且 `rerun=false` 时返回 `409 rerun_confirmation_required`;页面只有在
用户确认弹窗后才发送 `rerun=true`。
5. `rerun=true` 时删除旧 run(子表使用 `ON DELETE CASCADE`),再插入新的 `running`
run;删除与创建同事务提交。
6. 没有旧 run 时直接插入新的 `running` run。
事务提交后才注册 `BackgroundTasks`。后台执行异常会把 run 收敛为 `failed`;单只
股票异常记录到 `selection_run_item`,其余股票继续执行,最后根据失败数量和命中
结果写入 `success`、`partial_success` 或 `failed`。
进程在批次运行中崩溃会留下 `running` 状态;本期将其作为可见的执行中状态,并在
后续恢复机制中再增加超时接管。该限制必须在运维风险中保留,不能伪装成成功结果。
## 4. 执行数据流
1. HTTP 收到策略、目标交易日和 `rerun`,边界只允许当前支持的 `zhixing_b1`。
2. application 通过 selection 端口读取目标日最新的 `market_sync_batch`,只允许
`strategy_eligible=true` 的同步批次作为输入;没有可用批次则在任何清理/创建 run
事务之前返回 `422 market_data_not_ready`,不使用当前最新日期猜测目标日,也不破坏
已有的成功结果。
3. 读取当前 `market_stock.is_active=true` 且目标日同时拥有 bar/basic 的有效股票
集合;`target_count` 和 `coverage` 来自同步批次,`eligible_count` 来自实际输入。
4. 对每只股票调用已有 `PostgresMarketDataReader.load_history` 和
`EvaluateZhixingB1.execute`,将 item 状态和全部独立 signals 写入当前 run。
5. 完成后一次更新 run 汇总和 `finished_at`;查询端只读取已提交的持久化状态。
全股票池执行先使用现有“逐股票读取”的正确性优先方案,不在本任务引入并行化或
缓存;若性能不足,后续再以批量历史读取为单独设计。
## 5. HTTP 契约
### 5.1 触发
`POST /api/v1/selection/runs`
请求:
```json
{
"strategy": "zhixing_b1",
"target_trade_date": "2026-08-08",
"rerun": false
}
```
成功返回 `202`:
```json
{
"run_id": "<uuid>",
"strategy": "zhixing_b1",
"target_trade_date": "2026-08-08",
"status": "running"
}
```
错误状态至少包括:
- `409 run_in_progress`:同一策略和目标日已有运行中的批次;
- `409 rerun_confirmation_required`:已有终态结果但请求没有 `rerun=true`;
- `422`:策略、日期或市场数据资格不满足请求契约;
- `503`:无法创建批次或数据库不可用。
### 5.2 轮询与结果查询
- `GET /api/v1/selection/runs/{run_id}`:按 run ID 返回批次状态;运行中返回汇总,
终态追加 item 失败列表和 signals。
- `GET /api/v1/selection/results?strategy=zhixing_b1&target_trade_date=...`:按业务
键查询当前结果。目标日省略时取该策略最近一条当前 run;没有结果返回 `200` 的
`status=no_data`,不把“没有执行”伪装成 HTTP 异常。
稳定响应包含策略、目标日、run ID、状态、市场同步批次、计数、coverage、错误/失败
列表和 signal 明细。日期使用 ISO `date`,时间使用带时区的 ISO `datetime`。字段不
直接暴露数据库列名以外的内部异常信息。
## 6. 前端交互
- 新增 `/selection` 路由,启用 `HomeShell` 的“选股策略”导航。
- 页面提供目标交易日选择,默认查询最近持久化结果;策略下拉首期只显示“知行 B1”。
- 首次无结果时显示“执行策略”;已有成功、部分成功或失败结果时显示“重新执行/
重试执行”,点击先打开确认 Dialog,取消不调用 POST,确认才发送 `rerun=true`。
- POST 成功后保存 `run_id` 到组件局部状态,使用 React Query 轮询 run;运行中展示
状态和刷新提示,终态失效业务键查询并显示结果。
- 页面明确区分加载中、查询错误、无数据、执行中、执行失败、无命中、部分成功和
成功;signals 以每个 category 一行或可辨认的标签展示,同一股票的多分类不能合并
成一条无分类记录。
- API 类型、query key、mutation 和轮询逻辑全部位于 `features/selection/api/`,
页面不直接调用 `fetch`,不把服务器结果复制到 Zustand。
## 7. 兼容性与回滚
- 不修改 `market_stock`、行情事实表或现有同步批次的语义;只读取其
`strategy_eligible`、coverage 和目标日输入。
- migration downgrade 按 signals → items → runs 删除新表;删除 selection 结果
不影响市场数据。
- 如果异步机制或全市场性能不满足,保留已提交的迁移和领域契约,后续替换执行器;
不回退到即时查询或删除持久化结果。
- 进程崩溃遗留 `running` 和当前实现逐股票读取是已知风险,作为后续任务候选记录。
## 8. 验证策略
- domain/application:Fake reader/repository 覆盖首次执行、重跑先清空、运行中冲突、
全部状态聚合、多分类落盘和单股失败继续执行。
- infrastructure:fake psycopg connection 覆盖参数化查询、事务顺序、级联清理、
JSONB details、市场同步资格和稳定排序。
- HTTP:`TestClient(create_app())` 覆盖 `202`、`409`、`422`、无数据查询、轮询和
终态响应;后台执行依赖通过 FastAPI override 或 fake service 注入。
- frontend:页面测试覆盖初次执行、确认弹窗取消/确认、轮询状态、失败重试、无命中、
多分类和查询错误;运行格式、lint、类型、Vitest 和生产构建。