当行情推翻了既定的终点,旧信号该何去何从
「量化实战手记」— 记录从想法到落地的真实开发历程
下载更新:纷传圈子
摘要:缠论走势识别中,当行情不断创新高(或新低),走势的终点需要不断后移,旧代码一行赋值就把曾经的转折信号彻底抹掉。本次改动引入 former_ends 字段记录所有被推翻的历史终点,并在输出阶段统一降级为方向感知的 ±0.5 半强度信号,与当前终点的 ±1.0 形成两级信号体系。一次二十行的小改动,保住了被行情抹去的历史。
引言:那个消失的高点
缠论引擎识别走势类型时,终点是最值钱的信号。一个上涨走势在哪里见顶、一个下跌走势在哪里见底,直接决定了下游所有买卖判断的锚点。
但行情从不会老老实实停在某个点上。价格创了新高,原来的"顶"就不再是顶了——终点必须往后挪。
旧代码处理这件事的方式简单粗暴:
// 走势结束点后移trend_segments.back().end = i;一行赋值,干净利落。但问题也藏在这一行里:原来的那个终点,被直接覆盖了。它曾经在某个时刻被确认为转折点,承载过一次"见顶"的判断,现在却连一点痕迹都没留下。
这不是一个 bug,而是一个设计上的盲点:我们只关心"终点现在在哪",却忘了问"曾经以为的终点,难道一文不值吗?"
第一章:走势终点为什么总在"后移"
要先理解一个事实:终点后移不是异常,是缠论识别的常态。
缠论里的"走势类型"由至少一个中枢构成,方向分上涨和下跌。以一个上涨走势为例,引擎在识别过程中,会沿着价格逐根扫描。只要出现比当前终点更高的高点,就意味着这段上涨还在延续,终点要顺延到新的高点上。
换句话说,一个走势在最终定型之前,可能要经历好几次"我以为到顶了——结果没有"的过程。
一个上涨走势在生长过程中,终点从 A 后移到 B,再后移到 C。A 和 B 虽不再是终点,但它们曾是真实的转折判断。
每一次后移,旧代码都做同样的事:把 end 指向新位置,旧值丢弃。问题不在于"终点变了"——这本就是对的;问题在于旧的那个位置,在历史回看时彻底成了空白。
对于事后分析、信号统计、回测复现来说,这种丢失是隐性的:你不会看到报错,只会看到一段本该有标记的走势,悄无声息地少了几笔。
原则:覆盖操作的危险不在于写错了值,而在于它让旧值"像从未存在过一样"。任何"更新"动作,都值得问一句——被覆盖的那个值,真的没有信息吗?
第二章:从覆盖到记录
解决思路很朴素:不要覆盖,要追加。
给走势段加一个字段,专门存放所有被推翻的历史终点:
structTrendSegment{int start = -1; // 走势类型起点int end = -1; // 走势类型终点float direction = 0; // 走势方向 std::vector<Pivot> pivots; // 中枢 std::vector<int> former_ends; // 被后移覆盖的历史终点};然后把那四处在不同分支里重复出现的"后移"动作,收拢成一个函数:
// ============================================================// 走势终点后移:旧终点记入历史,避免覆盖丢失// 历史终点在输出阶段统一降级为 ±0.5 半强度信号// 注意:仅用于"后移"场景,不可用于首次赋值// ============================================================static inline voidextend_end(TrendSegment &seg, int new_end){ seg.former_ends.push_back(seg.end); seg.end = new_end;}这个函数只做两件事:先把当前 end 推进历史档案,再把 end 更新为新位置。原先的四调用点全部改成 extend_end(...),行为统一,意图清晰。
这步改动的价值有两层。表层是 DRY——四段一样的赋值收敛成一处。深层是语义对齐:所有"后移"动作现在共用同一个名字,读代码的人一眼就知道这里发生了"带历史保留的延长",而不是普通的覆盖。
同一个走势生长过程,旧方案丢失全部中间终点,新方案完整归档并最终分级输出。
原则:当一处逻辑在四个分支里重复出现,先抽函数;当被覆盖的值带有信息,先想归档。覆盖是信息的死亡,记录是信息的重生。
第三章:±0.5,给旧信号一个体面的位置
历史终点留下来了,但新的问题来了:它该怎么输出?
它不能和当前终点一样标 ±1.0——那个位置已经被行情证伪,不再是真正的转折。它也不能标 0——0 代表"无事发生",可它分明曾是一个被认真确认过的转折判断。
答案是一个中间值:±0.5,半强度信号。
// 当前终点:全强度 ±1.0if (e.direction == 1) { trend[e.start] = -1; trend[e.end] = 1; }else { trend[e.start] = 1; trend[e.end] = -1; }// 历史终点降级:被后移覆盖的旧终点标记为半强度信号(方向感知 ±0.5)for (int fe : e.former_ends){ trend[fe] = (e.direction == 1) ? 0.5f : -0.5f;}这里的方向感值得细看。缠论里,上涨走势的终点是顶(见顶信号),下跌走势的终点是底(见底信号)。所以历史终点要继承所在走势的方向:
上涨走势里被推翻的旧高点 → +0.5,一个"曾经的顶"下跌走势里被推翻的旧低点 → -0.5,一个"曾经的底"
方向符号与全强度的 ±1.0 完全一致,只是强度减半。
四级信号体系:方向决定正负,被推翻与否决定强度。
这是一个值得品味的设计。它没有用"标记位 + 状态字段"那种重型方案(比如 is_historical=true 加 value=1.0),而是把"是否被推翻"这一维信息,直接编码进了数值的大小本身。下游拿到一个浮点数,正负号告诉你方向,绝对值告诉你确信度——一个数承载两个维度。
对调用方而言,几乎所有现成的逻辑都不用改。原来按 abs(trend[i]) >= 1 筛强信号的地方,天然不会误伤 0.5;而想做"包含次级转折"分析的地方,只要把阈值放宽到 0.5 即可。信号的语义扩展了,接口却没有膨胀。
原则:能用数值大小表达的东西,就不要另开一个布尔字段。信号的本质是强度,不是有无——布尔丢掉了"有多确定"这一整维信息。
第四章:一个走势能留下多少个旧终点
注意 former_ends 的类型:std::vector<int>,不是单个 int。
这不是过度设计。回到第一章的那张图——一个上涨走势在生长过程中,终点可能从 A 后移到 B,再后移到 C,理论上还能继续到 D、E。每一次后移,extend_end 都会把当时的 end 推进 former_ends。所以一段走势定型后,former_ends 里可能躺着好几个历史终点,end 只指向最终确认的那一个。
// 走势结束点后移(上涨创新高场景)if (high[i] > high[trend_segments.back().end]){extend_end(trend_segments.back(), i);// 终点变了,中枢也要跟着重算 trend_segments.back().pivots =locate_pivots(duan, high, low, 1, trend_segments.back().start, i);}输出阶段的那个循环 for (int fe : e.former_ends) 自然地把这一串历史终点全部标成 ±0.5,一个不落。
这背后是一个容易被忽视的事实:历史是层叠的,不是扁平的。一个走势的"最终形态"只是最后一帧,而它生长途中的每一次自我推翻,都是有价值的中间态。用 vector 而不是单值,本质上是在承认——走势不是一步到位的,它是被行情一笔一笔"试探"出来的。
原则:当一个量在生命周期里可能多次变化,就用容器装它的全部历史,而不是只留最后一帧。最终值回答"现在是什么",历史值回答"是怎么走到这一步的"。
总结
整个改动只有 21 行新增、4 行替换,但它解决了一个真正的问题:让走势识别引擎不再"遗忘"自己曾经做出的转折判断。
三件事串成了一条清晰的设计链:
- 不覆盖,要归档
——新增 former_ends,被推翻的终点入档而非丢弃; - 不丢弃,要分级
——历史终点降级为 ±0.5,与当前终点的 ±1.0 形成两级体系; - 不分叉,要自洽
——±0.5 继承所在走势的方向,与 ±1.0 共用同一套正负号语义。
下游的信号消费方几乎零改动就能享受到这套扩展:要做严格转折分析,只认 ±1.0;要做包含"次级转折"的统计,把阈值放到 0.5。一个浮点数,同时承载了方向与确信度。
核心原则:好信号体系从不把世界切成"有"和"没有"。它用强度的连续变化,诚实地区分"确定发生了"和"曾经以为发生了"。被行情推翻的判断不是错误,而是次级证据——给它一个 ±0.5 的位置,比假装它从未出现要诚实得多。
附录:信号速查表
| +1.0 | end | |
| -1.0 | end | |
| +0.5 | former_ends | |
| -0.5 | former_ends | |
| -5 |
如果想再深入一点实现细节:
// 核心不变量:former_ends 仅由 extend_end 写入// 首次赋值 end 时(end = i)不走 extend_end,// 因此 former_ends 永远不含"从未被确认"的伪终点。// 每个历史终点都真实经历过一次"曾是 end"的状态。关键数据结构:TrendSegment 增加 former_ends 字段(vector<int>),仅追加不删改,输出阶段统一消费。
性能考虑:后移在一个走势的生命周期内发生次数有限(通常个位数),vector 追加与最终遍历的开销可忽略;输出阶段的额外循环复杂度仍为 O(n)。
量化实战手记
本系列记录作者用代码理解市场的真实历程——每个想法如何变成设计,每个设计如何变成可运行的系统。不谈理论,只聊实战。
夜雨聆风