同一个信号,两套算法,如何让行为一致?
「量化实战手记」— 记录从想法到落地的真实开发历程
引言:两个插件,一笔信号
fqchan04 和 fqchan06 是缠论分析插件的两个版本。04 是主力版本,算法经过长期实盘验证;06 是实验版本,用一套全新的笔识别算法——跳过 StdBar 中间层,直接在原始 K 线上增量扫描,复杂度从 O(N²) 降到 O(N)。
实验版本速度快,但缺少一组信号:分型确认信号。
这组信号在 04 中用于标记笔端点的分型被确认的时刻——即分型形成后,下一根 K 线的位置打上 11(顶确认)、-11(底确认)、12(强顶确认)、-12(强底确认)。
实盘交易中,确认信号是进出场的重要参考。04 有而 06 没有,意味着用 06 做策略时会丢失这个维度的信息,也无法与 04 在同等条件下对比效果。
第一章:信号是什么
缠论中,分型是 K 线形态的基本单元。三根 K 线构成一个分型——中间那根最高的叫顶分型,最低的叫底分型。
分型出现不代表有效。它可能被后续行情推翻。只有当分型右侧的 K 线走出来,确认分型结构没有破坏,这个分型才算「成立」。
04 的信号输出中,每个笔端点之后都会出现一个确认值。交易策略可以据此判断:这个分型有多「确凿」。
其中 12/-12 是强分型确认——确认 bar 的收盘价直接击穿了分型左侧 K 线的极值,信号更强烈。
11 | ||
12 | ||
-11 | ||
-12 |
原则:信号的语义必须跨版本一致——同一个值,在不同插件中代表同一个含义。
第二章:两套算法的差异
问题比「复制粘贴」复杂得多。04 和 06 的笔识别算法完全不同。
04 使用 StdBar 架构:先合并包含关系的 K 线,再在合并后的序列上检测分型。分型信息(factor)作为中间产物,可以自然地用于生成确认信号。这套架构经过长期实盘验证,是主力版本。
06 是一套实验性的 直接扫描架构:跳过 StdBar 中间层,直接在原始 K 线上用增量统计找极值点。速度更快,但分型信息不在主流程中产生。
这意味着确认信号不能简单嵌入 06 的主循环。它需要在笔端点计算完成后,额外获取一次分型信息。
原则:当两个系统算法不同但输出需要对齐时,在输出端做补偿——不改变核心算法,只在结果上补齐信号。
第三章:核心实现
思路:在 recognise_bi 内部补齐信号。加 close 参数,笔端点算完后用 std_bars 做一次分型检测,叠加确认值。交易软件只需调一个指标,与 04 用法一致。
改动集中在三个地方:函数签名、确认逻辑、调用方更新。
第一步:加 close 参数
recognise_bi 从只接受 high/low,变成也接受 close。用 const std::vector<float>& 避免拷贝。
// 之前std::vector<float> recognise_bi(int length, std::vector<float>& high, std::vector<float>& low, ChanOptions& options);// 之后std::vector<float> recognise_bi(int length, std::vector<float>& high, std::vector<float>& low, const std::vector<float>& close, // 新增 ChanOptions& options);调用方如果不传 close(传空向量),确认信号就不会生成——向后兼容。
第二步:在笔计算之后、标记之前插入确认逻辑
这是最关键的部分。在 06 的 recognise_bi 中,笔端点(1/-1)已经算好,-3 标记还没写入。这个间隙就是插入点。
// 笔端点分型确认信号:11/12(顶) -11/-12(底)if (!close.empty()) { std::vector<StdBar> std_bars = recognise_std_bars(length, high, low); for (size_t i = 1; i + 1 < std_bars.size(); i++) { StdBar& mid = std_bars.at(i); if (mid.factor == 0) continue; // 跳过非分型 int confirm_pos = std_bars.at(i + 1).start; // 确认位置 if (bi[confirm_pos] != 0) continue; // 已有信号不覆盖 // 检查分型极值处是否有笔端点 int vertex_pos = (mid.factor == 1) ? mid.high_vertex_raw_pos : mid.low_vertex_raw_pos; float sig = bi[vertex_pos]; // 强分型:确认bar收盘价突破左bar极值 bool strong = false; StdBar& left = std_bars.at(i - 1); if (mid.factor == 1) { strong = (close[confirm_pos] < left.low_low); } else { strong = (close[confirm_pos] > left.high_high); } if (mid.factor == 1 && (sig == 1 || sig == 0.5)) bi[confirm_pos] = strong ? 12 : 11; else if (mid.factor == -1 && (sig == -1 || sig == -0.5)) bi[confirm_pos] = strong ? -12 : -11; }}逻辑的要点:
1. 确认位置怎么找?std_bars[i+1].start——分型中间 bar 之后的合并 K 线,它的起始原始 K 线位置就是确认 bar。
2. 为什么检查 bi[vertex_pos]? 只有在分型处确实存在笔端点时,才输出确认信号。不是所有分型都是笔端点。
3. 强分型怎么判断? 确认 bar 的收盘价直接击穿了左侧 K 线区间的极值。顶分型强确认要求收盘价低于左 bar 的最低价——跌得够深,分型才够「扎实」。
第三步:更新所有调用方
Func2 和大智慧 BI 函数传入真实 close 数据。Func5-11 和大智慧 ZS 系列函数不需要确认信号,传入空向量——确认逻辑检测到 close.empty() 直接跳过。
第四章:踩坑与边界
坑 1:插入顺序
确认信号必须在笔端点(1/-1)之后、-3 标记之前写入。-3 标记的逻辑是找第一个 bi[i] == 0 的位置。如果先写 -3,确认信号可能被覆盖。
坑 2:不要覆盖已有信号
确认位置 confirm_pos 可能与某个笔端点重合。if (bi[confirm_pos] != 0) continue 确保不覆盖笔端点信号——笔端点比确认信号更重要。
坑 3:Python 绑定也要改
fqchan06 有 Cython 绑定(.pxd + .pyx 文件),C++ 签名变了,Python 侧必须同步更新,否则编译失败。新增 c 参数设默认空向量,不影响现有调用方。
原则:改函数签名时,所有调用方(C++ 导出函数 + Python 绑定)必须一次扫完,漏一个就编译不过。
总结
整个改动涉及 5 个文件、64 行变更:
czsc.h | |
bi.cpp | |
TCalcFuncSets.cpp | |
fqchan06.pxd | |
fqchan06.pyx |
核心思路:不改笔识别算法,在输出端补齐信号。06 的实验性高速算法不受影响,只在最后一步用 std_bars 做一次分型检测,叠加确认值。当实验算法的输出与主力版本对齐后,就可以在同等条件下对比两者的实盘表现。
核心原则:实验版本与主力版本对齐信号时,不要试图让两套算法变得一样。保持各自的优势,只在输出接口上统一语义——这样才具备对比的条件。
附录:技术速查
确认信号生成条件:
分型中间 bar 的 factor 不为 0(顶分型 factor=1,底分型 factor=-1) 确认位置 = std_bars[i+1].start(分型后第一根原始 K 线)确认位置未被笔信号占据( bi[confirm_pos] == 0)分型极值处存在笔端点( bi[vertex_pos] != 0)
强分型判定:
顶分型强确认:确认 bar 收盘价 < 左 bar 的 low_low底分型强确认:确认 bar 收盘价 > 左 bar 的 high_high
通达信公式调用:FQBI06(HIGH, LOW, CLOSE),第 3 个参数传 CLOSE 即可获得确认信号。
量化实战手记
本系列记录作者用代码理解市场的真实历程——每个想法如何变成设计,每个设计如何变成可运行的系统。不谈理论,只聊实战。
夜雨聆风