11 KiB
知行 B1 图形相似度评分集成设计
目标与边界
在不改变知行 B1 七个子信号公式、命中状态和稳定身份的前提下,为每只已命中的股票计算一次 0–100 完美图形相似度。评分使用原项目实际代码中的十个案例、25 日窗口、四维特征、权重、容忍参数和 60 分阈值,并修正为真正生效的 FastDTW 曲线对齐,在选股结果页展示匹配案例与分项。
本设计不包含图片生成、视觉模型、1–5 主观评分、PASS/WATCH/FAIL、自动交易、评分独立重跑、多评分器并存或跨策略通用评分平台。评分只属于 selection bounded context。
当前与目标数据流
当前执行链:
POST selection run
-> load qfq histories in batches
-> evaluate zhixing_b1 masks
-> SelectionRunItem + category signals
-> PostgreSQL
-> GET results
-> stocks[].signals[]
目标执行链:
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) NULLscore_threshold NUMERIC(5,2) NULLscore_version VARCHAR(64) NULLmatch_case_id VARCHAR(32) NULLmatch_case_name VARCHAR(128) NULLmatch_case_breakout_date DATE NULLmatch_breakdown JSONB NULLscore_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:
{
"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/文档中明确分数是当前版本模板相似度,不是历史收益承诺。