乐于分享
好东西不私藏

FqChan05缠论插件(34K 内只留最大的反弹:子浪划分优化记)

FqChan05缠论插件(34K 内只留最大的反弹:子浪划分优化记)

从「切太碎」到「克制划分」——缠论子浪算法的三轮进化

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

摘要:缠论子浪会把跨周期的大笔拆细,但拆太碎会淹没主趋势。这次让算法在 34K 范围内只留 K 线最多的那段反弹——先合并碎片极值,再贪心选 span 最大的反向子浪切一刀,递归检查每段。同时修掉「漏掉 10.50% 大反弹」与「一笔切出 5 浪」两个极端,收敛到少即是多的克制划分。

引言:拆,有两个极端

缠论里,「笔」是两个相邻顶底之间的最小趋势单元。当一根笔跨过太长的周期——比如日线上一笔横跨两个多月——它的内部其实藏着反复折返,这些折返才是真正的交易机会。

子浪(zilang)模块的职责,就是把这些「超长笔」拆细,把内部结构露出来。拆的触发条件很简单:一根笔内部的 K 线数超过阈值(默认 34),才进去找折返。

但「拆」这件事有两个极端,都不对:

太碎,一根笔被切成 5 段,主趋势淹没在小噪音里;太粗,又漏掉中间 10% 的大反弹,信号严重失真。

这次优化要找的,是两个极端之间那个「克制」的点——在一根超长笔里,只保留 K 线数量最多的那段反弹

第一章:两个走极端的旧版本

v1 递归贪心:漏掉了 10.50% 的反弹

第一个版本用贪心策略:在笔的内部极值里,每次挑 span(K 线跨度)最大的反向对,切一刀。

300355 的日线暴露了问题。2025-08-21 顶 → 2025-10-17 底,35 根 K 线。中间藏着一根真正的反弹:2025-09-04 底 → 2025-09-15 顶,涨幅 10.50%

但这段反弹「跨」在中间的一个碎片顶之上,不是相邻极值对。算法没看见它,反而挑了 2025 年 8 月下旬的一对小噪音。

结果:10.50% 的大反弹,被一笔带过。

v2 标准笔划分:一笔切出了 5 浪

为了不再漏,第二个版本走向另一极端——把超长笔内所有「满足约束的真笔」全部保留。

000159 的日线 2025-12-17 底 → 2026-03-03 顶,46 根 K 线,内部有多段折返。v2 全保留,切出了 5 浪。

但交易者要的不是「所有折返」,而是「最有价值的那段」。5 浪里混着小噪音,反而看不清主结构。

原则
:拆分的目标不是「不遗漏」,而是「保留有信号价值的主折返」。多即是少。

第二章:根因不是「选最大」错了

调试 300355 时,很容易得出一个错误结论:「贪心选最大反向」这个策略本身有问题。

实际根因更微妙:v1 漏反弹,不是策略错,是它「在没合并的碎片极值上」贪心。

问题出在 2025 年 9 月上旬的一个碎片顶——它是个噪音,和真正的 2025-09-15 顶连续同向(都是顶)。如果不先合并,贪心就会被它干扰,看不见 2025-09-04 底 → 2025-09-15 顶 这段跨碎片的真反弹。

只要先把这个碎片顶合并进 2025-09-15 顶(取 high 更高者),真反弹就作为序列里唯一的反向段,清晰地显现出来——这时候再贪心,自然就选对了。

所以最终版 v3,等于 v1 的选择策略 + 一步前置的碎片合并。没有发明新策略。

版本
选择策略
300355
000159
v1 递归贪心
相邻极值选 span 最大
漏 10.50% 反弹
v2 标准笔划分
保留所有合格真笔
3 浪 ✓
切 5 浪 ✗
v3 递归贪心 + 合并合并碎片后选最大反向
3 浪 ✓
3 浪 ✓
原则
:找到真正的根因,比换一个新策略更省事。v3 没有发明新策略,只是给 v1 加了一步预处理。

第三章:核心实现

整个算法落在一个递归函数 split_long_bi 里,三步走:

1合并碎片极值连续同向取最极端,锁首尾2贪心选最大反向子浪span 须 ≥ min_bars3切一刀 → 递归左 / 中 / 右每段 ≤ 34 即停

第一步:合并碎片

连续同向的极值,顶取 high 最高的、底取 low 最低的,删掉弱的。首尾(笔的两端)锁定不动。一次调用就能消化任意长度的连续段。

// 合并中间连续同向极值:连续顶取 high 最高、连续底取 low 最低// 首尾(Va/Vb)锁定不动,一次调用消化任意长度连续段voidmerge_consecutive(seq, high, low) {size_t i = 1;while (i + 1 < seq.size() - 1) {if (seq[i].type != seq[i+1].type) { i++; continue; }// 同向:保留更极端者,删另一个bool take_next = (seq[i].type == 1)            ? high[seq[i+1].pos] > high[seq[i].pos]            : low [seq[i+1].pos] < low [seq[i].pos];if (take_next) seq[i] = seq[i+1];        seq.erase(seq.begin() + i + 1);    }}

第二步:贪心选最大反向

在合并后的交替序列里,找和笔方向相反、span 最大的那一段。向上笔找「顶→底」(下跌回撤),向下笔找「底→顶」(上涨反弹)。span 必须 ≥ min_bars(默认 3),太小的不算。

// 找 span 最大的反向子浪(与笔方向相反的相邻异向段)const int want_a = (Va.type < 0) ?  1 : -1;  // 反向段起点const int want_b = (Va.type < 0) ? -1 :  1;  // 反向段终点for (size_t i = 0; i + 1 < seq.size(); i++) {if (seq[i].type != want_a || seq[i+1].type != want_b) continue;int span = seq[i+1].pos - seq[i].pos;if (span >= min_bars && span > best_span) {  // ★ 贪心选最大        best_span = span; best_a = &seq[i]; best_b = &seq[i+1];    }}

第三步:切一刀,递归

切在最大反向子浪的两端,把原笔分成左、中、右三段,各自再递归。关键刹车:span ≤ 34 就直接返回,不再拆。相邻段共享切点,拼接时各去掉首端点避免重复。

if (Vb.pos - Va.pos <= threshold)return {Va, Vb};                 // ★ 足够短,不拆// ... 合并碎片 + 贪心选最大反向 best_a/best_b ...// 切 best_a → best_b,对左/中/右三段递归auto left   = split_long_bi(Va,    cut_a, ...);auto middle = split_long_bi(cut_a, cut_b, ...);auto right  = split_long_bi(cut_b, Vb,    ...);// 相邻段共享切点,拼接时去重left.insert(left.end(), middle.begin()+1, middle.end());left.insert(left.end(), right.begin()+1,  right.end());

第四章:克制划分的两个真实案例

300355:合并修好了反弹

把这个碎片顶合并进 2025-09-15 顶后,反向段只剩 2025-09-04 底 → 2025-09-15 顶(span 7)一个。贪心选中,切成 3 子浪

2025-08-21顶 → 2025-09-04底 / 2025-09-04底 → 2025-09-15顶(10.50%) / 2025-09-15顶 → 2025-10-17底。

那段 10.50% 的反弹,被正确地单独成浪。

000159:贪心守住了克制

内部反向段最大的是 2026-01-26 顶 → 2026-02-13 底(span 14)。贪心选中,切一刀。左段 span 26 ≤ 34、右段 span 6 ≤ 34,都不再拆。

最终 3 浪,不是 5 浪。每一段都是有意义的折返,没有噪音。

v2 标准笔划分:全保留 → 5 浪(碎)噪音折返混入,主趋势被淹没v3 递归贪心 + 合并:只留最大反弹 → 3 浪(克制)最大反弹每段 ≤ 34K,只有主折返成浪

第五章:34 这个数字

threshold 默认 34,不是拍脑袋——34 = 13 + 21,斐波那契数列的相邻两项。

它的克制语义是:一根笔如果在 34 根 K 线以内,就认为它内部没有「值得单独成浪」的折返,保持原样。只有跨过这条线,才进去找最大的反弹。

34 同时是刹车和门槛:它限制了递归深度(每段切完都更短,必然收敛),也定义了「什么算够短」。

参数
默认
含义
zilang_threshold
34
触发拆分的笔内 K 线数(13+21),≤ 此值不拆
zilang_window
5
5K 局部极值窗口([i-2, i+2])
zilang_min_bars
3
拆出小笔的最小 K 线数
原则
:阈值不是调出来的,是有语义的。34 = 13 + 21,把「什么算够短」固化成一个有据可依的边界。

总结:少即是多

这次优化改的,是「拆多少」的尺度感:

  • 不是拆得越多越好——5 浪里混着噪音;
  • 也不是越粗越好——会漏掉 10% 的反弹;
  • 而是在 34K 的尺度里,只留 K 线最多的那段

代码层面,v3 没有发明新东西——只是把 v2 的碎片合并,接回了 v1 的贪心选择。根因不在策略,在预处理。这大概是这次最值得记下的经验。

核心原则
:克制不是少做事,而是在正确的尺度上,只保留有信号价值的东西。

附录:技术速查(给技术读者的彩蛋)

核心算法 split_long_bi 的伪代码,完整呈现「合并 → 贪心 → 递归」三段式:

split_long_bi(Va, Vb, threshold) → 端点序列if Vb.pos - Va.pos <= threshold:return [Va, Vb]                       // 叶节点:足够短,不拆  inner = find_local_extrema(Va+1 .. Vb-1, window)  seq   = [Va] + inner + [Vb]merge_consecutive(seq)                // ① 合并连续同向碎片(锁首尾)// ② 找 span 最大的反向子浪  best = nullfor 相邻 (a, b) in seq where (a,b) == 反向对 and b.pos-a.pos >= min_bars:if best == null or span > best.span: best = (a, b)if best == null: return [Va, Vb]      // 无合格反向,保留原样// ③ 切 best.a → best.b,对左/中/右三段递归  left   = split_long_bi(Va,      best.a, threshold)  middle = split_long_bi(best.a,  best.b, threshold)  right  = split_long_bi(best.b,  Vb,     threshold)return left + middle[1:] + right[1:]     // 共享切点,去重拼接

收敛性:切点 best.a / best.b 严格落在 (Va, Vb) 内部,故左/中/右三段 span 都严格小于原 span,递归深度有界,必然收敛。

边界:内部无极值 / 无合格反向段 → 返回 [Va, Vb](宁缺毋滥);vertices.size() < 2 → 原样返回。

量化实战手记

本系列记录作者用代码理解市场的真实历程——每个想法如何变成设计,每个设计如何变成可运行的系统。不谈理论,只聊实战。

加入量化探索之旅