188 lines
9.7 KiB
Markdown
188 lines
9.7 KiB
Markdown
|
|
# 策略执行结果持久化、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 和生产构建。
|