Files

171 lines
8.8 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 历史选股代码规格
## 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])
```
## 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)
```