feat(selection): 集成 B1 FastDTW 图形评分
This commit is contained in:
@@ -0,0 +1,162 @@
|
||||
# 知行 B1 图形相似度评分集成设计
|
||||
|
||||
## 目标与边界
|
||||
|
||||
在不改变知行 B1 七个子信号公式、命中状态和稳定身份的前提下,为每只已命中的股票计算一次 0–100 完美图形相似度。评分使用原项目实际代码中的十个案例、25 日窗口、四维特征、权重、容忍参数和 60 分阈值,并修正为真正生效的 FastDTW 曲线对齐,在选股结果页展示匹配案例与分项。
|
||||
|
||||
本设计不包含图片生成、视觉模型、1–5 主观评分、`PASS/WATCH/FAIL`、自动交易、评分独立重跑、多评分器并存或跨策略通用评分平台。评分只属于 `selection` bounded context。
|
||||
|
||||
## 当前与目标数据流
|
||||
|
||||
当前执行链:
|
||||
|
||||
```text
|
||||
POST selection run
|
||||
-> load qfq histories in batches
|
||||
-> evaluate zhixing_b1 masks
|
||||
-> SelectionRunItem + category signals
|
||||
-> PostgreSQL
|
||||
-> GET results
|
||||
-> stocks[].signals[]
|
||||
```
|
||||
|
||||
目标执行链:
|
||||
|
||||
```text
|
||||
POST selection run
|
||||
-> load the ten versioned case windows once for this run
|
||||
-> build an immutable in-memory case library
|
||||
-> load candidate qfq histories in existing batches
|
||||
-> evaluate zhixing_b1 masks
|
||||
-> if selected: score the stock once against all cases
|
||||
-> SelectionRunItem(score) + unchanged category signals
|
||||
-> PostgreSQL
|
||||
-> GET results with stocks[].score + stocks[].signals[]
|
||||
```
|
||||
|
||||
评分在公式评估之后执行。`no_signal`、`insufficient_history`、`missing_target_bar` 和 `data_error` 不运行评分;评分异常只影响该股票的评分状态,不改变 `SelectionRunItem.status`、signals 或批次的选股成功状态。
|
||||
|
||||
## 领域模型与模块边界
|
||||
|
||||
在 `modules/selection/domain/` 增加纯领域评分模块,负责案例定义、特征提取、四维匹配和结果值对象。该模块只依赖 NumPy/Pandas 与显式注入的评分配置,不导入 FastAPI、PostgreSQL 或 infrastructure。
|
||||
|
||||
建议领域类型包括:
|
||||
|
||||
- `PatternCaseDefinition`:案例 ID、名称、规范化 `ts_code`、突破日和窗口长度。
|
||||
- `PatternFeatures`:趋势、KDJ、量能和价格形态四组不可变特征。
|
||||
- `PatternScoreBreakdown`:四个 0–100 有限分项。
|
||||
- `PatternScore`:状态、原始总分、阈值、最佳案例、breakdown、版本和安全原因。
|
||||
- `ZhixingB1PatternScorer`:对一个 `StockHistory` 与不可变案例库执行确定性评分。
|
||||
|
||||
评分状态与选股状态分离,使用 `not_executed`、`matched`、`below_threshold` 和 `failed`。`matched` 表示最高分大于等于 60;`below_threshold` 表示计算成功但原 pipeline 不会 enrichment;`failed` 表示评分实际执行但输入、案例库或算法失败。选股失败仍只使用已有 evaluation status。
|
||||
|
||||
应用层增加评分用例或端口,由 `RunZhixingB1` 注入。每次 run 开始时加载一次案例库,每批复用已有候选 `StockHistory`,只给 `selected` 股票评分。相同股票命中的多个 category 共享一个股票级评分,不重复计算。
|
||||
|
||||
infrastructure 负责从 PostgreSQL 读取十个案例在各自 `breakout_date` 之前的 qfq 行情。查询必须参数化、升序、严格 `< breakout_date`,每个案例取最后 25 条。生产运行不访问旧项目 CSV、旧缓存或 Tushare。
|
||||
|
||||
## 算法兼容契约
|
||||
|
||||
版本一使用固定标识 `zhixing_b1_pattern_fastdtw_v1`。以下任何变化都必须升级版本:案例集合或突破日、窗口长度、特征公式、权重、容忍参数、FastDTW 半径或距离函数、阈值或非有限值处理。
|
||||
|
||||
版本一保留原运行代码的事实值:
|
||||
|
||||
| 项目 | 契约 |
|
||||
| --- | --- |
|
||||
| 案例数 | 10,保持缺少 `case_005` 的既有定义 |
|
||||
| 候选/案例窗口 | 25 个升序交易日;案例不包含突破日 |
|
||||
| 分项 | `trend_structure`、`kdj_state`、`volume_pattern`、`price_shape` |
|
||||
| 权重 | 0.10、0.20、0.25、0.45 |
|
||||
| 总分 | `round(weighted_sum * 100, 2)` |
|
||||
| 阈值 | 60.0,比较使用 `>=` |
|
||||
| 曲线距离 | 真正生效的 FastDTW;一维曲线使用标量欧氏距离,显式 `radius=1` |
|
||||
| 最佳案例 | 十个案例中总分最高者;稳定同分时按案例定义顺序 |
|
||||
|
||||
原文档中的 30% 趋势/25% 价格权重和 YAML 中未生效的动态权重不进入 v1。实现应把实际生效常量集中在版本化配置中,不能继续保留“配置看似可改但运行时忽略”的状态。
|
||||
|
||||
实施门禁已验证原代码的 `fastdtw(one_dimensional_curve, ..., dist=scipy.spatial.distance.euclidean)` 稳定抛出 `AxisError`,随后由 `_shape()` 回退 `_simple_dtw`。用户明确选择修正为真正生效的 FastDTW,因为允许局部时间对齐更符合评分要求。实现使用适配一维标量的欧氏距离并显式固定 `radius=1`,不依赖 SciPy 的向量函数;这会改变旧历史分数和阈值命中集合,因此必须使用新的 `zhixing_b1_pattern_fastdtw_v1` 版本,并以新 golden 锁定结果。
|
||||
|
||||
所有领域输出必须是有限数。对原 25 日窗口造成的非有限中间特征,通过 compatibility helper 复现旧 matcher 的最终比较结果,但不允许 `NaN`/`Infinity` 进入 dataclass、JSONB 或 HTTP。固定 fixture 必须覆盖该路径;没有证据证明兼容时,评分返回 `failed`,不伪造分数。
|
||||
|
||||
## 案例库构建与一致性
|
||||
|
||||
只迁移十条案例定义,不迁移原 `data/cache/b1_pattern_library_cache.json`。该缓存未被 Git 跟踪、没有失效协议且已与行情漂移,不能作为部署事实源。
|
||||
|
||||
每次 selection run 从 PostgreSQL 构建一次小型内存案例库,读取规模约为 250 行,避免跨 run 的磁盘缓存失效问题。十个案例必须全部成功、各有 25 条有效 qfq OHLCV,才将案例库标记为 ready;缺任一案例时本 run 的评分统一不可用,但选股照常执行。这个原子完整性检查是对旧项目“静默使用部分案例库”的有意收紧,避免同一个版本标识对应不同分母和结果。
|
||||
|
||||
案例特征使用数据库中当前最新修订的 qfq,符合现有历史分析语义。已落盘评分不会因后续 qfq 修订自动改变;用户显式重跑后允许得到基于最新修订数据的新分数。`market_sync_batch_id`、评分版本和案例定义共同提供解释上下文。
|
||||
|
||||
固定案例是评分模板,而不是目标交易日当时可知的市场事实。历史日期可能使用后来定义的案例,因此该分数解释为“使用 `zhixing_b1_pattern_fastdtw_v1` 模板对历史候选做相似度评价”,不能解释为无前视偏差的历史交易信号。
|
||||
|
||||
## 持久化设计
|
||||
|
||||
评分是每股一次的结果,存入 `selection_run_item`,不复制到 `selection_signal.details`。新增列建议为:
|
||||
|
||||
- `score_status VARCHAR(32) NOT NULL DEFAULT 'not_executed'`
|
||||
- `score_value NUMERIC(5,2) NULL`
|
||||
- `score_threshold NUMERIC(5,2) NULL`
|
||||
- `score_version VARCHAR(64) NULL`
|
||||
- `match_case_id VARCHAR(32) NULL`
|
||||
- `match_case_name VARCHAR(128) NULL`
|
||||
- `match_case_breakout_date DATE NULL`
|
||||
- `match_breakdown JSONB NULL`
|
||||
- `score_reason TEXT NULL`
|
||||
|
||||
约束保证总分和四个分项位于 0–100,`matched` 必须具备完整分数、案例、breakdown、阈值和版本;`below_threshold` 可以保留内部原始分数用于审计,但 HTTP 默认只表达“未达到 60”而不把它当作匹配结果;`failed` 不保存数值或案例,只保存去敏后的有限长度原因。旧 run 通过默认 `not_executed` 与空字段保持兼容。
|
||||
|
||||
增加 `(run_id, score_value DESC, ts_code)` 索引,为数据库级评分排序提供稳定分页。重跑仍删除旧 `selection_run` 并依赖级联清除 item/signal;不新增独立评分表,也不双写 signal details。
|
||||
|
||||
若未来需要同股多评分器、多个评分版本同时存在或评分独立重跑,再把 item 上的单份结果迁移到 `(run_id, ts_code, scorer, version)` 的独立表;当前需求不提前引入该复杂度。
|
||||
|
||||
## HTTP 与前端契约
|
||||
|
||||
`SelectionStockResponse` 增加可空的股票级 `score`:
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "matched",
|
||||
"value": 86.4,
|
||||
"threshold": 60.0,
|
||||
"version": "zhixing_b1_pattern_fastdtw_v1",
|
||||
"case": {
|
||||
"id": "case_001",
|
||||
"name": "华纳药厂",
|
||||
"breakout_date": "2025-05-12"
|
||||
},
|
||||
"breakdown": {
|
||||
"trend_structure": 71.2,
|
||||
"kdj_state": 83.0,
|
||||
"volume_pattern": 88.0,
|
||||
"price_shape": 90.1
|
||||
},
|
||||
"reason": null
|
||||
}
|
||||
```
|
||||
|
||||
旧 run、未执行评分或字段全空时返回 `score: null`。评分失败返回 `status: failed` 与安全原因,但现有 signals 仍完整显示;不得把评分失败放入顶层 `failures[]`,该列表继续只表示选股评估失败。
|
||||
|
||||
结果查询增加可选 `sort=code|score_desc|score_asc`,默认 `code` 保持当前行为。排序和分页必须在 PostgreSQL 完成,稳定次级键为 `ts_code`;前端不能只排序当前页。评分筛选、只导出高分代码和独立排名暂不纳入 MVP。
|
||||
|
||||
前端在每只股票卡片/行的股票级区域展示总分、最佳案例和四个分项,七个 signal 继续展示各自原有 details。`below_threshold` 显示“未匹配到 60 分以上案例”,`failed` 显示“评分暂不可用”,两者都不能遮挡选股信号。页面提供按评分升降序的可访问控件,并保留默认代码排序。
|
||||
|
||||
## 失败、性能与并发
|
||||
|
||||
评分复用已加载的候选历史,只额外读取一次十个案例窗口。复杂度约为 `命中股票数 × 10 × 25` 的特征比较,且只对 selected 股票执行;不得为每个 category 或每个候选单独查询案例数据。
|
||||
|
||||
案例库初始化失败是 run 级评分不可用,不是选股批次失败。单股评分异常只将该股 `score_status` 置为 `failed`,其他股票继续。边界日志只记录 run ID、股票代码、评分版本和异常类型,不输出数据库连接、凭据或原始异常对象。
|
||||
|
||||
现有 FastAPI 进程内 background task 仍是执行边界;本任务不引入队列。实现必须测量新增评分耗时并写入结构化 run 日志,确认没有显著放大现有批次时长或连接池使用。
|
||||
|
||||
## 发布与回滚
|
||||
|
||||
迁移为向后兼容的可空列与索引。增加 `ZHIXING_SELECTION_PATTERN_SCORING_ENABLED` 配置,默认启用;紧急情况下可关闭评分,选股链恢复原行为,新 run 的 score 为 `not_executed`。
|
||||
|
||||
发布顺序为先执行数据库 upgrade,再发布同时理解新列的后端,最后发布前端。旧前端会忽略新增 JSON 字段;新前端对 `score: null` 安全降级。回滚应用时保留新增列不会影响旧代码,只有确认不再需要已保存评分时才执行 destructive downgrade。
|
||||
|
||||
## 主要风险与控制
|
||||
|
||||
- 原项目没有数值 golden:先冻结最小旧数据 fixture 和期望值,再实现迁移。
|
||||
- 原缓存漂移:不迁移缓存,每次 run 从 PostgreSQL 构建完整案例库。
|
||||
- 25 日窗口与 114 日指标产生非有限中间值:兼容 helper + 有限值断言 + golden 覆盖。
|
||||
- FastDTW 语义:使用标量欧氏距离与固定 `radius=1`,通过新 golden 锁定;不得把旧 `_simple_dtw` 期望值当作兼容目标,也不能在异常时静默退回另一种算法。
|
||||
- 同股多 category:评分只存 item 并在股票级响应展示,signal 身份和详情不变。
|
||||
- 历史模板前视解释:在 UI/文档中明确分数是当前版本模板相似度,不是历史收益承诺。
|
||||
Reference in New Issue
Block a user