2026-08-08 22:41:45 +08:00
|
|
|
|
# 历史选股代码规格
|
|
|
|
|
|
|
|
|
|
|
|
## Scenario: `zhixing_b1` 历史公式评估
|
|
|
|
|
|
|
|
|
|
|
|
### 1. Scope / Trigger
|
|
|
|
|
|
|
|
|
|
|
|
- 触发:新增 `modules/selection` bounded context,基于 PostgreSQL 已保存的 qfq
|
|
|
|
|
|
日线执行历史知行 B1 公式。
|
|
|
|
|
|
- 边界:selection 只读市场事实并返回领域评估结果;不负责市场数据同步、信号
|
|
|
|
|
|
持久化、批次调度、HTTP 路由或前端展示。
|
|
|
|
|
|
|
|
|
|
|
|
### 2. Signatures
|
|
|
|
|
|
|
|
|
|
|
|
- `MarketDataReader.load_history(ts_code: str, target_trade_date: date) -> StockHistory`
|
|
|
|
|
|
- `ZhixingB1Strategy.evaluate(history: StockHistory, target_trade_date: date) -> SelectionEvaluation`
|
|
|
|
|
|
- `EvaluateZhixingB1.execute(ts_code: str, target_trade_date: date) -> SelectionEvaluation`
|
|
|
|
|
|
- `SelectionSignal.identity -> tuple[str, date, str, str]`
|
|
|
|
|
|
|
|
|
|
|
|
### 3. Contracts
|
|
|
|
|
|
|
|
|
|
|
|
- `StockHistory.bars` 必须是升序、去重的 qfq 行情,且不得包含目标交易日之后的
|
|
|
|
|
|
数据;`daily_basic` 按交易日保存同日可空指标。
|
|
|
|
|
|
- `SelectionBar` 使用有限的 `float | None` 表示 OHLCV;目标日的 open/high/low/
|
|
|
|
|
|
close/volume 任一缺失时不得生成信号。
|
|
|
|
|
|
- 策略名称固定为 `zhixing_b1`;7 个分类固定为
|
|
|
|
|
|
`zhixing_b1_oversold_turn`、`zhixing_b1_oversold_volume`、
|
|
|
|
|
|
`zhixing_b1_original_b1`、`zhixing_b1_extreme_volume`、
|
|
|
|
|
|
`zhixing_b1_pullback_white`、`zhixing_b1_pullback_super`、
|
|
|
|
|
|
`zhixing_b1_pullback_yellow`。
|
|
|
|
|
|
- 同一股票同一交易日可以返回多个分类;唯一身份是
|
|
|
|
|
|
`(ts_code, target_trade_date, strategy, category)`,返回顺序遵循
|
|
|
|
|
|
`ZHIXING_B1_SIGNAL_ORDER`。
|
|
|
|
|
|
- PostgreSQL reader 必须参数化查询 `source_adj = 'qfq'` 且
|
|
|
|
|
|
`trade_date <= target_trade_date`,左连接同日 `market_daily_basic`;不得回退到
|
|
|
|
|
|
CSV、Tushare 或当前最后一行。
|
|
|
|
|
|
- 评估状态区分 `selected`、`no_signal`、`insufficient_history`、
|
|
|
|
|
|
`missing_target_bar` 和 `data_error`。业务状态不是异常,数据库读取失败才映射为
|
|
|
|
|
|
`data_error`。
|
|
|
|
|
|
|
|
|
|
|
|
### 4. Validation & Error Matrix
|
|
|
|
|
|
|
|
|
|
|
|
| 条件 | 行为 |
|
|
|
|
|
|
| --- | --- |
|
|
|
|
|
|
| 少于公式最小暖机长度 | 返回 `insufficient_history`,不返回信号 |
|
|
|
|
|
|
| 目标日没有 qfq bar 或 OHLCV 不完整 | 返回 `missing_target_bar` |
|
|
|
|
|
|
| 目标日数据完整但无分类命中 | 返回 `no_signal` |
|
|
|
|
|
|
| PostgreSQL 读取失败 | 返回 `data_error`,保留股票和目标日上下文 |
|
|
|
|
|
|
| rolling 窗口不足 | 只使用已到达的交易行;`EVERY` 等需要完整窗口的条件不命中 |
|
|
|
|
|
|
| 除零、NaN 或无穷中间值 | 转为 NaN/False,不得静默制造命中 |
|
|
|
|
|
|
|
|
|
|
|
|
### 5. Good / Base / Bad Cases
|
|
|
|
|
|
|
|
|
|
|
|
- Good:给定历史目标日,reader 只返回该日及之前的 qfq 行,策略返回可序列化详情
|
|
|
|
|
|
和全部命中分类。
|
|
|
|
|
|
- Base:同日多分类命中时每类都保留稳定身份;同日 `daily_basic` 缺失只影响实际
|
|
|
|
|
|
依赖该指标的条件。
|
|
|
|
|
|
- Bad:用当前最新市值覆盖历史 K 线、取 bars 最后一行代替显式目标日,或把 7 类
|
|
|
|
|
|
mask OR 成一个结果后丢失分类。
|
|
|
|
|
|
|
|
|
|
|
|
### 6. Tests Required
|
|
|
|
|
|
|
|
|
|
|
|
- 指标单元测试:rolling 暖机、交易日 `REF`、窗口边界、除零、NaN、宽幅代码参数。
|
|
|
|
|
|
- 策略单元测试:目标日截断、暖机/缺失状态、7 个 mask 独立存在和同日多分类身份。
|
|
|
|
|
|
- reader 单元测试:参数化 SQL、qfq 过滤、目标日截断、升序映射和 left join 可空值。
|
|
|
|
|
|
- golden 测试:离线固定 fixture 可重复运行;不得在测试运行时导入旧项目或访问生产库。
|
|
|
|
|
|
|
|
|
|
|
|
### 7. Wrong vs Correct
|
|
|
|
|
|
|
|
|
|
|
|
#### Wrong
|
|
|
|
|
|
|
|
|
|
|
|
```python
|
|
|
|
|
|
# 会产生历史前视数据,并丢弃同日的其他分类。
|
|
|
|
|
|
target = history.bars[-1]
|
|
|
|
|
|
first = next(category for category in categories if masks[category].iloc[-1])
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
#### Correct
|
|
|
|
|
|
|
|
|
|
|
|
```python
|
|
|
|
|
|
# 用显式交易日定位,并保留所有独立 mask 的命中。
|
|
|
|
|
|
target_index = frame.index[frame["trade_date"] == target_trade_date][0]
|
|
|
|
|
|
matched = tuple(category for category in signal_order if masks[category].iloc[target_index])
|
|
|
|
|
|
```
|
2026-08-09 09:34:46 +08:00
|
|
|
|
|
|
|
|
|
|
## Scenario: 持久化策略执行结果与 HTTP 重跑
|
|
|
|
|
|
|
|
|
|
|
|
### 1. Scope / Trigger
|
|
|
|
|
|
|
|
|
|
|
|
- Trigger:为已有历史选股公式增加每日批次持久化、HTTP 触发/查询和 Web 轮询时,沿用
|
|
|
|
|
|
`selection` bounded context;不要让查询请求重新计算公式。
|
|
|
|
|
|
- 触发入口是 `POST /api/v1/selection/runs`,结果读取入口是
|
|
|
|
|
|
`GET /api/v1/selection/runs/{run_id}` 和
|
|
|
|
|
|
`GET /api/v1/selection/results?strategy=...&target_trade_date=...`。
|
|
|
|
|
|
|
|
|
|
|
|
### 2. Signatures
|
|
|
|
|
|
|
|
|
|
|
|
- `SelectionUniverseReader.load_execution_source(strategy: str, target_trade_date: date) -> SelectionExecutionSource`
|
|
|
|
|
|
- `SelectionRunStore.prepare_run(strategy, target_trade_date, source, *, rerun: bool) -> SelectionRun`
|
|
|
|
|
|
- `POST /api/v1/selection/runs` 请求:
|
|
|
|
|
|
`{"strategy": "zhixing_b1", "target_trade_date": "YYYY-MM-DD", "rerun": false}`;成功返回
|
|
|
|
|
|
`202` 和 `{run_id, strategy, target_trade_date, status: "running"}`。
|
|
|
|
|
|
- `selection_run` 的业务唯一键是 `(strategy, target_trade_date)`;
|
|
|
|
|
|
`selection_run_item` 的唯一键是 `(run_id, ts_code)`;
|
|
|
|
|
|
`selection_signal` 的唯一键是 `(run_id, ts_code, category)`。
|
|
|
|
|
|
|
|
|
|
|
|
### 3. Contracts
|
|
|
|
|
|
|
|
|
|
|
|
- `load_execution_source` 必须先确认目标日存在 `strategy_eligible=true` 且已完成的
|
|
|
|
|
|
`success`/`partial_success` 市场同步批次,并且股票为 active 且目标日 qfq bar/basic
|
|
|
|
|
|
完整;来源批次 ID、目标数、实际参与数和 coverage 写入 run。
|
|
|
|
|
|
- 市场数据预检在删除旧结果之前执行。预检失败不得创建新 run,也不得破坏已有终态结果。
|
|
|
|
|
|
- 同一策略同一目标日的重跑在一个短事务内使用 advisory transaction lock,删除旧 run
|
|
|
|
|
|
(依赖子表 `ON DELETE CASCADE`)并创建唯一的新 `running` run;长时间的逐股计算在
|
|
|
|
|
|
事务外执行。
|
|
|
|
|
|
- 查询响应必须返回 run 状态、批次统计、coverage、失败股票和全部独立 signals;同一
|
|
|
|
|
|
股票同日的多 category 不能合并,并按 `ZHIXING_B1_SIGNAL_ORDER` 稳定排序。
|
|
|
|
|
|
- 前端仅在首次无结果时直接触发;已有终态结果或失败重试必须先确认,再传
|
|
|
|
|
|
`rerun=true`。运行中重复请求返回冲突,不能创建第二个当前 run。
|
|
|
|
|
|
|
|
|
|
|
|
### 4. Validation & Error Matrix
|
|
|
|
|
|
|
|
|
|
|
|
| 条件 | 行为 |
|
|
|
|
|
|
| --- | --- |
|
|
|
|
|
|
| 不支持的策略、非法日期或缺少合格市场数据 | `422`,错误码 `market_data_not_ready`(输入校验仍使用 FastAPI 默认 `422`) |
|
|
|
|
|
|
| 同策略同日已有 `running` run | `409`,错误码 `run_in_progress` |
|
|
|
|
|
|
| 已有终态 run 且 `rerun=false` | `409`,错误码 `rerun_confirmation_required` |
|
|
|
|
|
|
| PostgreSQL 读写失败 | `503`,错误码 `selection_storage_unavailable` |
|
|
|
|
|
|
| run ID 不存在 | `404`,错误码 `run_not_found` |
|
|
|
|
|
|
| 单只股票评估抛出异常 | 记录该 item 为 `data_error`,继续其他股票;批次最终为 `partial_success` 或 `failed` |
|
|
|
|
|
|
| 全股票评估完成但没有命中 | 批次为 `success`、`signal_count=0`;前端显示“没有命中信号”,不是“无数据” |
|
|
|
|
|
|
|
|
|
|
|
|
### 5. Good / Base / Bad Cases
|
|
|
|
|
|
|
|
|
|
|
|
- Good:目标日已有合格同步批次,首次 POST 返回 `202`,轮询最终结果保留同一股票的
|
|
|
|
|
|
两个独立 category;确认重跑后旧 signals 随旧 run 级联清除。
|
|
|
|
|
|
- Base:目标日没有结果时查询返回 `200 status=no_data`;查询不触发公式计算,用户可在
|
|
|
|
|
|
页面选择日期后发起首次执行。
|
|
|
|
|
|
- Bad:在市场数据预检前删除旧 run;把多个 category OR 成一条 signal;用浏览器长连接
|
|
|
|
|
|
等待全股票池计算;或把进程异常留下的 `running` 伪装成成功。
|
|
|
|
|
|
|
|
|
|
|
|
### 6. Tests Required
|
|
|
|
|
|
|
|
|
|
|
|
- Domain/application:断言 source 预检先于 `prepare_run`、首次执行、多分类落盘、单股异常
|
|
|
|
|
|
继续执行、全部 no-signal 和失败计数/终态聚合。
|
|
|
|
|
|
- PostgreSQL adapter:用 fake connection 断言 advisory lock、终态重跑删除后插入、运行中
|
|
|
|
|
|
冲突、JSONB details、级联删除契约和公式优先级排序。
|
|
|
|
|
|
- HTTP:用 `TestClient(create_app())` 断言 `202`、`409` 两类冲突、`422` 数据未就绪、
|
|
|
|
|
|
`GET` 无数据、run 轮询和终态 signals。
|
|
|
|
|
|
- Migration:在可用 PostgreSQL 中断言三张 selection 表、唯一键、索引、cascade 外键,
|
|
|
|
|
|
并验证 downgrade 顺序;无数据库时至少生成 offline upgrade/downgrade SQL。
|
|
|
|
|
|
- Frontend:断言加载、查询失败、无数据、执行中、失败、部分成功、无命中、多 category,
|
|
|
|
|
|
以及重跑/失败重试确认取消不发 POST、确认发送 `rerun=true`。
|
|
|
|
|
|
|
|
|
|
|
|
### 7. Wrong vs Correct
|
|
|
|
|
|
|
|
|
|
|
|
#### Wrong
|
|
|
|
|
|
|
|
|
|
|
|
```python
|
|
|
|
|
|
# 先清空旧结果,再去确认目标日输入是否可执行;预检失败会造成数据丢失。
|
|
|
|
|
|
store.delete_current(strategy, target_trade_date)
|
|
|
|
|
|
source = reader.load_execution_source(strategy, target_trade_date)
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
#### Correct
|
|
|
|
|
|
|
|
|
|
|
|
```python
|
|
|
|
|
|
# 先读取并验证来源快照,只有成功 claim 后才允许重跑清理事务。
|
|
|
|
|
|
source = reader.load_execution_source(strategy, target_trade_date)
|
|
|
|
|
|
run = store.prepare_run(strategy, target_trade_date, source, rerun=rerun)
|
|
|
|
|
|
```
|