听见笔的回声:缠论分型确认信号的设计与实现
一笔之中,除了顶点,还有确认顶点的那一刻
一笔之中,除了顶点,还有确认顶点的那一刻
「量化实战手记」— 记录从想法到落地的真实开发历程
引言:笔的信号里缺了什么
缠论笔识别的核心函数 recognise_bi 输出一组信号值:1 代表笔顶,-1 代表笔底。这些信号标注在分型最高或最低的那根 K 线上,构成了量化策略最基础的"转折点"坐标。
但笔的成立不是一瞬间的事。一个顶分型由三根合并 K 线组成——左、中、右。中间那根的最高价构成了笔顶,信号 1 打在那里。但分型真正"确认"的时刻,是右侧那根 K 线形成的时候。这个确认时刻在现有信号体系中是隐形的。
对于实盘交易而言,知道"笔顶在哪里"和知道"笔顶什么时候被确认"是完全不同的两件事。前者是事后标记,后者是可操作的时机。
第一章:分型的确认时刻
在缠论的合并 K 线体系中,分型由连续三根标准化 K 线构成:
bar0(左)、bar1(中间/顶点)、bar2(右/确认)。
在 fqchan04 的 C++ 实现中,分型的判定发生在 recognise_std_bars 函数里。当 bar2 形成且方向与 bar1 不同时,bar1 被标记为分型——factor = 1(顶)或 factor = -1(底)。
分型被确认的那一刻,对应的是 bar2 的第一根原始 K 线。这就是"确认时刻"的精确位置。
原则:信号的时间精度决定了策略的可操作性。把"确认时刻"从"顶点时刻"中分离出来,相当于给每个转折点加上了时间戳。
第二章:信号值的设计
现有的笔信号体系已经形成了一套语义清晰的编码:
| 信号值 | 含义 | 位置 |
|---|---|---|
1 / -1 | 笔顶 / 笔底 | 分型最高/最低K线 |
0.5 / -0.5 | 中间态(可能被推翻) | 同上 |
-3 | 序列标识 | 首个非零位 |
我们需要在同一个数组中增加分型确认信号,且不与现有信号冲突。自然的选择是:用 11/-11 标记确认信号,用 12/-12 标记强分型确认。
这样的设计有几个考虑:
- 符号一致:
11跟1同号,表达"顶"的语义;-11跟-1同号,表达"底"的语义。 - 量级区分:
1和11在数值上差一个数量级,用abs(x) >= 10就能快速筛选确认信号。 - 强弱分型:
12/-12标记强分型,为后续策略提供额外的判断依据。
最终的信号全景:
| 信号 | 含义 | 位置 |
|---|---|---|
1 | 笔顶 | 顶分型最高K线 |
11 | 顶分型确认 | 确认bar的第一根K线 |
12 | 顶分型强确认 | 同上(强分型时) |
-1 | 笔底 | 底分型最低K线 |
-11 | 底分型确认 | 确认bar的第一根K线 |
-12 | 底分型强确认 | 同上(强分型时) |
原则:信号编码应保持"符号即语义、量级即分类"的特性,让下游消费者用最简单的数值判断就能区分信号类型。
第三章:核心实现
信号的计算发生在 recognise_bi 函数末尾。在现有的最终遍历(为所有笔端点赋值 1/-1)之后、序列标识 -3 之前,插入一段确认信号的遍历。
逻辑很直接:遍历所有标准化 K 线,找到已确认的分型(factor != 0),在确认位置放置信号。
// 笔端点分型确认信号:11/12(顶) -11/-12(底)
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; // 确认K线
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]; // 笔端点信号
bool strong = false;
if (!close.empty() && confirm_pos >= 0
&& confirm_pos < static_cast<int>(close.size())) {
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;
}
这段代码做了几件事:
第一,确认位置用 std_bars[i+1].start——这是导致分型确认的那根原始 K 线的索引,也就是"分型刚成立的那个时刻"。
第二,守卫条件:分型未确认(factor == 0)的不处理;确认位置已有信号的不覆盖;收盘价越界的不判断强分型。
第三,中间态也配对:0.5/-0.5(中间态笔端点)同样需要确认信号。这些中间态可能最终被推翻,也可能成为正式笔端点,但在它们存在的时刻,对应的分型确认同样有意义。
原则:新增信号不应破坏已有信号的语义。通过位置守卫和条件判断,确保新旧信号和平共处。
第四章:强分型的判断
强分型是这次设计中需要额外思考的部分。最初的设计是检查中间 K 线是否只包含一根原始 K 线(start == end),即没有发生过合并。这个判断虽然简单,但不够准确。
实际使用中,强分型的核心特征是:确认K线的收盘价直接突破了左侧K线的极值。
对于顶分型,如果确认 K 线的收盘价低于左侧 K 线的最低价,说明空头直接砸穿了左侧支撑,反转力度强。对底分型同理——确认 K 线收盘价高于左侧 K 线最高价,说明多头直接突破了左侧压力,反转信号可靠。
但这里有一个现实问题:recognise_bi 原本的参数只有 high 和 low,没有 close。收盘价数据从哪里来?
接口设计:close 作为可选参数
解决方案是将 close 作为 recognise_bi 的新参数,但设计为可选——不传就不做强分型判断,所有确认信号统一为 11/-11。
// 新签名:close 在 options 前面
std::vector<float> recognise_bi(
int length,
std::vector<float> &high,
std::vector<float> &low,
const std::vector<float> &close, // 可选:空=不强分型判断
ChanOptions &options);
用 const std::vector<float>& 的好处是:调用方可以传空 vector(std::vector<float>()),函数内通过 close.empty() 判断是否需要做强分型检测。不需要指针,不需要 std::optional,干净自然。
原则:可选参数应该让"不关心"的调用方完全无感——不传就退化到旧行为,传了就获得增量能力。
第五章:多平台适配
fqchan04 是一个多平台项目:通达信 DLL、MT5 指标、Python 包,它们的数据接口各不相同。close 参数的引入需要在每个平台上找到对应的数据来源。
通达信的 Func2 函数有 4 个参数位(out, high, low, ignore),其中第 4 个从未使用。正好把 close 填进去。而 BI 函数通过 pData->m_pData[i].m_fClose 直接构建 close 向量。
MT5 的 FQ_BI 函数需要显式新增 close 参数,同时更新 DLL 导出的调用约定(从 @20 改为 @24)。
Python 的 Cython 绑定新增 c=[] 可选参数,传空列表时不做强分型判断。
最终的效果是:所有有 close 数据的平台都传入了真实收盘价,强分型检测全部生效。少数确实没有 close 的旧接口保持传空 vector,退化到不区分强弱。
原则:多平台项目的接口变更应该像水一样——有渠道的地方就流过去,没渠道的地方保持原状,不强求统一。
总结
这次改动的核心是一组配对信号:每个笔端点(1/-1)后面跟着一个分型确认信号(11/-11 或 12/-12),标记的是"分型刚确认成立的那根 K 线"。
设计过程中有几个值得记住的决策:
信号放在确认 K 线而非顶点 K 线。顶点 K 线已经有了笔端点信号,再放一个信号会冲突。确认 K 线的位置天然不同,而且它的语义就是"确认时刻"——刚好是我们想要的。
close 参数从 float* 改为 const std::vector<float>&。用空 vector 代替 nullptr 表示"未提供",与项目中其他参数的风格统一,也避免了指针安全性的顾虑。
强分型用收盘价判断而非合并K线数量。收盘价直接反映市场意愿——如果确认 K 线的收盘价突破了左侧极值,说明市场选择了方向,这比检查"是否发生过合并"更有交易含义。
核心原则:好的信号设计是成对的——每一个"是什么"信号(笔端点)都应该伴随一个"什么时候确认"信号(分型确认)。单独看一个是静态标注,两个一起看才是可操作的交易时机。
附录:技术速查表
信号值速查:
| 值 | 类型 | 位置 | 条件 |
|---|---|---|---|
1 | 笔顶 | high_vertex_raw_pos | — |
0.5 | 中间态笔顶 | 同上 | 可能被推翻 |
11 | 顶分型确认 | std_bars[i+1].start | close ≥ left.low_low |
12 | 顶分型强确认 | 同上 | close < left.low_low |
-1 | 笔底 | low_vertex_raw_pos | — |
-0.5 | 中间态笔底 | 同上 | 可能被推翻 |
-11 | 底分型确认 | std_bars[i+1].start | close ≤ left.high_high |
-12 | 底分型强确认 | 同上 | close > left.high_high |
-3 | 序列标识 | 首个非零位 | — |
筛选技巧:
# Python 中快速筛选确认信号
confirm_sigs = [x for x in bi if abs(x) >= 10]
# 区分强弱
strong_sigs = [x for x in bi if abs(x) == 12]
normal_sigs = [x for x in bi if abs(x) == 11]
改动文件清单:
| 文件 | 变更 |
|---|---|
cpp/chanlun/bi.cpp | 核心:新增确认信号循环 |
cpp/chanlun/czsc.h | 签名加 close 参数 |
cpp/TCalcFuncSets.cpp | Func2/BI/FQ_BI/ZS 函数传入 close |
cpp/TCalcFuncSets.h | FQ_BI 声明和导出对齐 |
mt5/fqchan04.mq5 | MQ5 声明和调用传入 close |
python/fqchan04.pxd | Cython 声明同步 |
python/fqchan04.pyx | Python 接口加可选 c 参数 |