信号身份错位:缠论模型编码缺陷的发现与修复

信号身份错位:缠论模型编码缺陷的发现与修复

信号身份错位:缠论模型编码缺陷的发现与修复

fqcopilot 中两个模型输出了错误的信号身份——一次全量排查与修复记录

「量化实战手记」— 记录从想法到落地的真实开发历程

引言:信号值里的身份信息

fqcopilot 是一个缠论量化信号引擎,内置 18 个策略模型(S0000 ~ S0017)。每个模型对行情数据做独立分析后,输出一个整型信号值——这个值不仅包含买卖方向,还携带了模型编号、触发次数、入场类型三层信息。

信号编码公式如下:

signal_value = direction × (model_id × 1000 + occurrence × 100 + entrypoint)

比如信号值 14107 可以解码为:买入方向、14 号模型、第 1 次触发、MACD 交叉入场。

这套编码规则是下游选股系统、回测框架、实盘交易信号路由的基础。如果信号值里的模型编号不正确,下游就无法判断信号的真实来源。

+1 方向 14 × 1000 模型编号 1 × 100 触发次数 7 入场类型 = 14107 S0014, 第1次, MACD买入

第一章:问题发现

排查从 S0014 模型开始。S0014 是「中枢上方/下方」策略,它的设计逻辑是:先调用 S0008(盘整/趋势背驰)得到信号,再额外验证收盘价与最后一个中枢的位置关系,过滤掉不满足条件的信号。

问题出在过滤通过后的赋值:

// S0014.cpp 第 97 行(修复前)
inner_result[signal_pos] = original_signal;

original_signal 来自 S0008,它的值是 8107-8207 这样的编码——模型编号是 8,不是 14。直接赋值意味着 S0014 输出的信号,在下游看来跟 S0008 完全一样。

这就好比一个医生看了 X 光片给出了诊断意见,但签名用的是放射科医生的工号——下游系统无法区分这个信号到底是背驰策略原始触发,还是经过中枢位置验证后的精选信号。

S0008 盘整/趋势背驰 信号 过滤 中枢位置验证 输出 8107 model_id = 8 ✗ 身份错误 应输出 14107 model_id = 14 ✓ 正确身份
原则:当一个模型在另一个模型的输出上做过滤时,过滤后信号的「身份」必须更新为过滤模型的编号,否则下游无法区分信号的真实来源。

第二章:根因分析

在 fqcopilot 的 18 个模型中,有 3 个模型采用了「继承过滤」的设计模式——它们调用上游模型获取原始信号,然后通过额外条件过滤:

模型策略上游过滤条件
S0013二买二卖S0008trend/stretch 重合 + 收盘价验证
S0014中枢上方/下方S0008收盘价在中枢 zg 上方 / zd 下方
S0008盘整/趋势背驰自身MACD 背驰判定

S0008 是自身计算,信号由 encode_signal(8, ...) 直接编码,没有问题。但 S0013 和 S0014 在过滤 S0008 信号时,直接将 original_signal 赋值给结果数组。

从代码意图看,注释写的是「保留 S0008 原信号」——开发者在写这段代码时,认为「继承 S0008」意味着「透传 S0008 的信号值」。这是一个语义理解偏差:「继承」指的是继承 S0008 的背驰逻辑和 occurrence 含义,而非透传编码值。

// S0013.cpp 第 91 行(修复前)——注释暴露了设计意图的误解
inner_result[signal_pos] = original_signal;  // 满足条件,保留S0008原信号
原则:「继承」上游逻辑 ≠ 「透传」上游输出。在信号系统中,每个模型的输出必须携带自己的身份标识。

第三章:修复方案

修复思路很清晰:从上游信号中解码出 occurrence 和 entrypoint,再用当前模型的 model_id 重编码

为 S0014 新增 reencode() 方法:

// 将 S0008 信号重编码为 S0014(model_id 8 → 14,保留 occurrence 和 entrypoint)
int reencode(int original_signal) {
  int abs_val = std::abs(original_signal);
  int occurrence = (abs_val % 1000) / 100;   // 提取触发次数
  int ep_abs = abs_val % 100;                  // 提取入场类型
  auto ep = (original_signal > 0)
                ? static_cast<EntrypointType>(ep_abs)
                : static_cast<EntrypointType>(-ep_abs);
  return encode_signal(14, occurrence, ep);    // 用 14 号模型身份重新编码
}

然后将两处赋值替换为重编码调用:

// 修复前:直接透传,输出 8107
inner_result[signal_pos] = original_signal;

// 修复后:重编码,输出 14107
inner_result[signal_pos] = reencode(original_signal);

这段代码的精巧之处在于:它没有假设 S0008 的 entrypoint 固定为 7(MACD 交叉),而是从原始信号中动态提取。即使未来 S0008 的编码规则变化,S0014 的重编码逻辑依然正确。

1 解码 从 original_signal 提取 occurrence 和 entrypoint 2 换身份 将 model_id 从 8 替换为 14(或 13) 3 重编码 调用 encode_signal(14, occurrence, ep) 生成新信号值 4 输出 8107 → 14107,信号身份正确

第四章:全量排查

修复 S0014 后,需要对全部 18 个模型做一次系统性排查,确认是否存在同类问题。

排查方法很简单:搜索所有 inner_result 的赋值语句,检查是否都经过了 encode_signal() 或等价的正确编码。排除赋值为 0(取消信号)的情况,聚焦非零赋值。

模型赋值方式状态
S0000 ~ S0012encode_signal(N, ...)✓ 正确
S0013= original_signal(透传 S0008)✗ 缺陷
S0014= original_signal(透传 S0008)✗ 缺陷
S0015硬编码 15101 / -15101✓ 正确
S0016 ~ S0017encode_signal(16/17, ...)✓ 正确
S0101encode_signal(101, ...)✓ 正确

排查结果:只有 S0013 和 S0014 存在信号透传缺陷。其余 16 个模型均使用 encode_signal() 正确编码。

S0013 使用与 S0014 相同的 reencode() 方法修复,只需将 model_id 参数从 14 改为 13。

原则:「继承过滤」模式天然容易遗漏重编码。当新增基于上游模型过滤的策略时,必须将信号身份重编码作为 checklist 的一项。

第五章:设计反思

为什么这个缺陷容易被忽视

S0013 和 S0014 在功能上是正确的——它们确实完成了信号过滤,输出的买入/卖出方向、触发位置、入场类型都正确。唯一的问题是信号值里的模型编号字段不匹配。

在单元测试中,如果只验证「是否产生了买入信号」「信号方向是否正确」,这个缺陷不会被发现。只有当测试断言具体的信号值时,才会暴露问题。这提醒我们:信号系统的测试必须包含编码完整性校验

如何防止同类问题

有两个方向:

编译期约束:让「继承过滤」模型的基类提供专门的过滤接口,内部自动处理重编码,子类只负责判断「保留或丢弃」。

运行时校验:在 REGISTER_CALC 注册机制中增加输出校验——信号值的 model_id 必须与注册编号一致,不一致则断言失败。

// 伪代码:运行时校验信号身份
for (auto sig : results) {
  if (sig == 0) continue;
  int actual_model = std::abs(sig) / 1000;
  assert(actual_model == registered_model_id);
}
原则:信号身份是系统级约束,不应该依赖开发者的自觉。要么通过框架自动处理,要么通过断言强制校验。

总结

本次修复涉及 2 个文件、4 行赋值语句的变更。改动虽小,影响深远——如果 S0013 和 S0014 的信号继续以 8 开头输出,下游的信号归因分析、策略绩效统计、模型权重调整都会产生数据偏差。

提炼三条可复用的设计原则:

  • 身份不可透传:模型 A 过滤模型 B 的输出时,结果必须携带模型 A 的身份标识
  • 继承逻辑 ≠ 继承编码:「继承」上游策略的判定逻辑,不等于透传上游的编码值
  • 系统约束需要自动化:信号身份的正确性不应依赖人工检查,应通过框架或断言保障

附录:信号值解码速查

信号值方向模型触发入场类型
8107买入S0008 背驰1个中枢MACD交叉
14107买入S0014 中枢上方1个中枢MACD交叉
-14207卖出S0014 中枢下方2个中枢MACD交叉
13107买入S0013 二买1个中枢MACD交叉
5101买入S0005 中枢震荡第1次回踩结构信号
15101买入S0015 MA250确认信号结构信号

Entrypoint 触发类型对照

  • 1 = 结构信号 | 2 = Pin Bar | 3 = 吞没形态 | 4 = 强分型
  • 5 = MA5 拐头 | 6 = 量价配合 | 7 = MACD 交叉
浙ICP备2026022231号-1      浙公网安备33011002019439号