Files
zhixing-system/.trellis/tasks/archive/2026-08/08-08-strategy-execution-results/design.md
T
2026-08-09 12:34:02 +08:00

9.7 KiB
Raw Blame History

策略执行结果持久化、HTTP 触发与查询设计

1. 设计目标

在现有 selection bounded context 上补齐一条可重跑的每日策略结果链路:用户在 Web 页面选择目标交易日并触发 zhixing_b1,HTTP 快速返回执行批次标识,服务端在 当前进程的异步批次中完成全股票池评估并持久化结果,页面轮询状态后展示信号明细。

一次重跑必须在同一数据库事务中清除指定“策略 + 目标交易日”的旧结果并创建新的 运行记录,避免旧信号和新信号混在一起。执行中的重复请求只返回冲突,不得并发清理 或重复计算同一批次。

本期不引入独立任务队列、定时器或其他策略;HTTP 是触发入口,FastAPI 进程内的 BackgroundTasks 是异步执行机制。

2. 上下文边界与模块分工

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

请求:

{
  "strategy": "zhixing_b1",
  "target_trade_date": "2026-08-08",
  "rerun": false
}

成功返回 202:

{
  "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 和生产构建。