9.7 KiB
策略执行结果持久化、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) 在一个短事务中完成:
- 使用按策略和日期派生的 PostgreSQL advisory transaction lock,串行化同一业务键。
- 查询当前
selection_run。 running时拒绝请求,返回409 run_in_progress。- 已有终态且
rerun=false时返回409 rerun_confirmation_required;页面只有在 用户确认弹窗后才发送rerun=true。 rerun=true时删除旧 run(子表使用ON DELETE CASCADE),再插入新的runningrun;删除与创建同事务提交。- 没有旧 run 时直接插入新的
runningrun。
事务提交后才注册 BackgroundTasks。后台执行异常会把 run 收敛为 failed;单只
股票异常记录到 selection_run_item,其余股票继续执行,最后根据失败数量和命中
结果写入 success、partial_success 或 failed。
进程在批次运行中崩溃会留下 running 状态;本期将其作为可见的执行中状态,并在
后续恢复机制中再增加超时接管。该限制必须在运维风险中保留,不能伪装成成功结果。
4. 执行数据流
- HTTP 收到策略、目标交易日和
rerun,边界只允许当前支持的zhixing_b1。 - application 通过 selection 端口读取目标日最新的
market_sync_batch,只允许strategy_eligible=true的同步批次作为输入;没有可用批次则在任何清理/创建 run 事务之前返回422 market_data_not_ready,不使用当前最新日期猜测目标日,也不破坏 已有的成功结果。 - 读取当前
market_stock.is_active=true且目标日同时拥有 bar/basic 的有效股票 集合;target_count和coverage来自同步批次,eligible_count来自实际输入。 - 对每只股票调用已有
PostgresMarketDataReader.load_history和EvaluateZhixingB1.execute,将 item 状态和全部独立 signals 写入当前 run。 - 完成后一次更新 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 和生产构建。