听见笔的回声:缠论分型确认信号的设计与实现

一笔之中,除了顶点,还有确认顶点的那一刻

听见笔的回声:缠论分型确认信号的设计与实现

一笔之中,除了顶点,还有确认顶点的那一刻

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

引言:笔的信号里缺了什么

缠论笔识别的核心函数 recognise_bi 输出一组信号值:1 代表笔顶,-1 代表笔底。这些信号标注在分型最高或最低的那根 K 线上,构成了量化策略最基础的"转折点"坐标。

但笔的成立不是一瞬间的事。一个顶分型由三根合并 K 线组成——左、中、右。中间那根的最高价构成了笔顶,信号 1 打在那里。但分型真正"确认"的时刻,是右侧那根 K 线形成的时候。这个确认时刻在现有信号体系中是隐形的。

对于实盘交易而言,知道"笔顶在哪里"和知道"笔顶什么时候被确认"是完全不同的两件事。前者是事后标记,后者是可操作的时机。

第一章:分型的确认时刻

在缠论的合并 K 线体系中,分型由连续三根标准化 K 线构成:

顶分型 底分型 1 11 bar0·左元素 bar1·顶点 bar2·确认 -1 -11 bar0·左元素 bar1·底点 bar2·确认

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 标记强分型确认。

这样的设计有几个考虑:

  • 符号一致111 同号,表达"顶"的语义;-11-1 同号,表达"底"的语义。
  • 量级区分111 在数值上差一个数量级,用 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 线最高价,说明多头直接突破了左侧压力,反转信号可靠。

强顶分型 强底分型 bar0.low close bar0 bar1·顶 bar2 close < bar0.low → 信号=12 bar0.high close bar0 bar1·底 bar2 close > bar0.high → 信号=-12

但这里有一个现实问题:recognise_bi 原本的参数只有 highlow,没有 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 参数的引入需要在每个平台上找到对应的数据来源。

recognise_bi(high, low, close, options) 核心:C++ 实现,close 可选 通达信 Func2: 第4参数=close BI: pData→m_fClose MT5 FQ_BI: 新增 close[] 导出 @20→@24 Python c=[] 默认空 Cython: vector→vector 所有平台都已传入 close 数据,强分型检测全覆盖

通达信Func2 函数有 4 个参数位(out, high, low, ignore),其中第 4 个从未使用。正好把 close 填进去。而 BI 函数通过 pData->m_pData[i].m_fClose 直接构建 close 向量。

MT5FQ_BI 函数需要显式新增 close 参数,同时更新 DLL 导出的调用约定(从 @20 改为 @24)。

Python 的 Cython 绑定新增 c=[] 可选参数,传空列表时不做强分型判断。

最终的效果是:所有有 close 数据的平台都传入了真实收盘价,强分型检测全部生效。少数确实没有 close 的旧接口保持传空 vector,退化到不区分强弱。

原则:多平台项目的接口变更应该像水一样——有渠道的地方就流过去,没渠道的地方保持原状,不强求统一。

总结

这次改动的核心是一组配对信号:每个笔端点(1/-1)后面跟着一个分型确认信号(11/-1112/-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].startclose ≥ left.low_low
12顶分型强确认同上close < left.low_low
-1笔底low_vertex_raw_pos
-0.5中间态笔底同上可能被推翻
-11底分型确认std_bars[i+1].startclose ≤ 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.cppFunc2/BI/FQ_BI/ZS 函数传入 close
cpp/TCalcFuncSets.hFQ_BI 声明和导出对齐
mt5/fqchan04.mq5MQ5 声明和调用传入 close
python/fqchan04.pxdCython 声明同步
python/fqchan04.pyxPython 接口加可选 c 参数
浙ICP备2026022231号-1      浙公网安备33011002019439号