从「切太碎」到「克制划分」——缠论子浪算法的三轮进化
「量化实战手记」— 记录从想法到落地的真实开发历程
摘要:缠论子浪会把跨周期的大笔拆细,但拆太碎会淹没主趋势。这次让算法在 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 的选择策略 + 一步前置的碎片合并。没有发明新策略。
| v3 递归贪心 + 合并 | 合并碎片后选最大反向 |
原则:找到真正的根因,比换一个新策略更省事。v3 没有发明新策略,只是给 v1 加了一步预处理。
第三章:核心实现
整个算法落在一个递归函数 split_long_bi 里,三步走:
第一步:合并碎片
连续同向的极值,顶取 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 浪。每一段都是有意义的折返,没有噪音。
第五章:34 这个数字
threshold 默认 34,不是拍脑袋——34 = 13 + 21,斐波那契数列的相邻两项。
它的克制语义是:一根笔如果在 34 根 K 线以内,就认为它内部没有「值得单独成浪」的折返,保持原样。只有跨过这条线,才进去找最大的反弹。
34 同时是刹车和门槛:它限制了递归深度(每段切完都更短,必然收敛),也定义了「什么算够短」。
zilang_threshold | ||
zilang_window | ||
zilang_min_bars |
原则:阈值不是调出来的,是有语义的。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 → 原样返回。
量化实战手记
本系列记录作者用代码理解市场的真实历程——每个想法如何变成设计,每个设计如何变成可运行的系统。不谈理论,只聊实战。
夜雨聆风