# 策略执行结果持久化、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": "", "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 和生产构建。