Files
zhixing-system/.trellis/spec/backend/selection.md
T

8.8 KiB
Raw Blame History

历史选股代码规格

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

# 会产生历史前视数据,并丢弃同日的其他分类。
target = history.bars[-1]
first = next(category for category in categories if masks[category].iloc[-1])

Correct

# 用显式交易日定位,并保留所有独立 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

# 先清空旧结果,再去确认目标日输入是否可执行;预检失败会造成数据丢失。
store.delete_current(strategy, target_trade_date)
source = reader.load_execution_source(strategy, target_trade_date)

Correct

# 先读取并验证来源快照,只有成功 claim 后才允许重跑清理事务。
source = reader.load_execution_source(strategy, target_trade_date)
run = store.prepare_run(strategy, target_trade_date, source, rerun=rerun)