news 2026/10/31 4:49:45

顾比倒数线Python实现与回测落地:锚点、状态机与仓位管理

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
顾比倒数线Python实现与回测落地:锚点、状态机与仓位管理

1. 我为什么先把"倒数线"这一个工具啃透

去年有一段时间我在整理一套趋势跟随的入场规则,翻资料的时候翻到了顾比倒数线,第一反应是"这不就是个前低吗,能有多复杂"。真动手在图上画了几十张之后才发现,我一开始的理解偏差得相当离谱。它确实跟"前低"有关,但取哪一个前低、从哪根K线开始往左数、数到第几根停、什么情况下这条线要重算,每一步都藏着一堆口径分歧。而这些分歧,恰恰就是同一套策略在不同人手里跑出天差地别结果的根本原因。

顾比倒数线(Count Back Line)解决的其实是一个非常具体的问题:当趋势已经确立、价格出现回调之后,到底在哪个价位重新进场才算"技术上有依据"。绝大多数人的做法是拍脑袋——回撤5%买、跌到10日线买、破了前低就止损。这些数字是外生的,跟这只标的历史走势本身没有关系。倒数线的思路不一样,它是从最近的高点出发,往回数K线,用低点序列的连续性找出一个"多头结构被打断"的位置,把这个位置作为触发价。这个价位是从行情本身长出来的,不是人为设定的百分比。

我这篇想讲三件事:倒数线到底在数什么(原理和口径)、用Python怎么把它写成能跑的函数(含状态管理)、以及什么条件下这条线才算有效(这是最容易被跳过、也最容易亏钱的部分)。适合已经会写基础pandas、想把手上的技术指标从"能画出来"推进到"能回测"的朋友。文中所有代码我都按可直接复现的方式写,数据接口留成通用的,你把手上的行情DataFrame按open/high/low/close四列传进去就能跑。

2. 把"倒数"两个字拆开:它数的其实是低点序列的连续性

2.1 上升趋势里的倒数线:从最近高点往左数低点

先讲多头场景,这是用得最多的一种。假设价格走了一段上升趋势,创出一个近期高点,然后开始回调。这时候我要回答的问题是:回调到哪儿算"正常回撤",到哪儿算"结构破坏"。

倒数线的做法是这样的:找到最近那个高点所在的K线(我叫它锚点),从锚点开始往左(往历史方向)看,检查相邻两根K线的低点关系,要求是"右边的低点高于左边的低点",也就是低点在依次抬高。只要这个抬高关系还在延续,就继续往左走;一旦碰到第一根低点不再抬高(即它的低点小于或等于右边那根的低点)的K线,扫描就停下来。停下来的时候,最后一根满足"低点抬高"的K线,它的最低价就是倒数线的位置。

为什么取这个位置?因为它标记的是"多头结构最后一次有效抬升"的起点。价格如果跌破了这个点,说明从锚点高点往左数的这一整段抬升结构已经被破坏,多头逻辑不再成立。反过来,价格从这个位置上方重新起来,说明回调结束、抬升结构有望重建,这就是进场触发。

注意"倒数"这两个字在这里的含义:它不是从今天往后数,而是从锚点往前数。所以锚点一旦确定,这条线立刻就能算出来,不需要等后续K线验证。这也解释了一个新手常犯的错——把锚点当成"未来某天的高点"去找,结果永远对不上图。

2.2 反过来数高点:下降趋势中那条"卖出触发线"

空头场景完全对称。找最近的低点锚点,从它往左数,这次检查的是高点关系,要求"右边的高点低于左边的高点",也就是高点依次降低。扫到第一根高点不再降低的K线就停,取最后一根合规K线的最高价作为倒数线。价格反弹到这个价位上方,就是空头结构被破坏、可以考虑离场的信号。

我见过不少人只做多头,就跳过这一半不写。这个偷懒会带来一个实际问题:你的离场规则会变成"固定止盈+固定止损",而入场用的是结构化价位。两边不对称,回测出来的权益曲线会非常难看。哪怕你不做空,把下降趋势的倒数线算出来当多头持仓的离场参考,也是很有价值的用法。

2.3 三种流传最广的口径差异,以及我最后选了哪一种

真正动手写代码之前,必须先承认一件事:倒数线的具体算法在不同资料里有差异。我把见到过的归纳成三种,用一张表对比清楚。

口径停止条件取哪一根K线的价格特点
A:取"破坏者"扫到第一根低点不抬高的K线即停取这根破坏者本身的低点线位更低,触发更晚,信号少但更"钝"
B:取"最后一根合规"同上取破坏者右边那根的低点线位略高,触发更早,最常见的版本
C:固定数三根无条件往左数满3根就停取第3根的低点完全不看结构,只保证"有回撤空间",机械但抗噪

我这篇文章后面全部用口径B。理由有两个:一是它跟"结构破坏"这个原始逻辑最贴合,破坏者本身已经是无效K线了,拿它当触发位等于把噪音也算进去;二是口径A和B在绝大多数情况下只差一根K线的低点,但B在回测里的信号数量明显更合理,不会因为一个毛刺把触发价压得太低。

口径C我没用,但我理解它的存在意义——它相当于人为给策略加了一个"最小回撤幅度"的约束,防止你在几乎没回调的位置就追进去。如果你发现自己的策略信号太密集、交易成本吃掉太多利润,可以把它当成一个附加过滤器:先按口径B算,再要求锚点到倒数线之间至少有N根K线的距离。这个组合比单纯换成口径C可控得多。

3. Python落地:把"数K线"写成能复用的函数

3.1 先约定函数边界:只返回价位和锚点索引

写这种指标函数,最容易犯的错是"一把梭"——输入整段行情,输出一个长得离谱的DataFrame,想调试根本无从下手。我的习惯是先写一个只处理"某一天"的原子函数,返回值固定在三个:倒数线的价位、锚点的索引、实际数到的抬升次数。这三个值足够你后面做任何扩展,也足够你定位问题。

抬升次数这个返回值特别有用。它不只是个调试信息,本身就是一个质量指标——数到的次数越多,说明这个高点之前的抬升结构越扎实,倒数线越可信;只数到两三根就停的,往往是在震荡区间里硬抠出来的信号,含金量很低。后面第4章讲"三根起步"的硬条件,用的就是它。

先写找锚点的辅助函数。要注意:锚点用的是最高价(high)的最大值,倒数线取的是最低价(low),这两个不能混。我见过有人用收盘价找锚点、用收盘价算线,画出来跟书上的图差一大截,问题就出在这儿。

import numpy as np import pandas as pd def _swing_high_idx(high: np.ndarray, end_idx: int, lookback: int) -> int: """在 [end_idx-lookback+1, end_idx] 窗口内找最高价所在的索引""" start = max(0, end_idx - lookback + 1) window = high[start:end_idx + 1] return start + int(np.argmax(window)) def _swing_low_idx(low: np.ndarray, end_idx: int, lookback: int) -> int: start = max(0, end_idx - lookback + 1) window = low[start:end_idx + 1] return start + int(np.argmin(window))

3.2 核心扫描逻辑的完整代码

原子函数长这样。注意min_bars的语义我在注释里写清楚了——它数的是"相邻抬升的次数",设成3意味着至少4根K线参与。如果你按"三根K线"的口径来理解,把它设成2。这个约定必须在代码里写死并注释,否则半年后你自己回来看都会懵。

def cbl_long(high, low, end_idx, lookback=20, min_bars=3): """ 多头倒数线(口径B) 返回: (价位, 锚点索引, 抬升次数) 抬升次数 = 相邻K线满足 low[j-1] > low[j] 的对数 """ i0 = _swing_high_idx(high, end_idx, lookback) j = i0 cnt = 0 while j - 1 >= 0 and low[j - 1] > low[j]: j -= 1 cnt += 1 if cnt < min_bars: return np.nan, i0, cnt return float(low[j]), i0, cnt def cbl_short(high, low, end_idx, lookback=20, min_bars=3): """空头倒数线:从最近低点往左数高点降低""" i0 = _swing_low_idx(low, end_idx, lookback) j = i0 cnt = 0 while j - 1 >= 0 and high[j - 1] < high[j]: j -= 1 cnt += 1 if cnt < min_bars: return np.nan, i0, cnt return float(high[j]), i0, cnt

这个while循环有个隐含前提:它只检查相邻两根的低点关系,不做任何平滑。这意味着只要出现一根反常的低点,扫描立刻中断。这是设计意图,不是缺陷——倒数线本来就是要找"结构被打断"的那一下。但你要清楚后果:在低波动、K线密集纠缠的行情里,这个循环经常数不到两三根就断,导致大量NaN。我第一版跑出来的有效信号比例不到15%,差点以为代码写错了,后来发现是标的选错了。

3.3 用 pandas 铺成全序列,顺手解决锚点漂移

最直觉的写法是每天重算一次:

def add_cbl_naive(df, lookback=20, min_bars=3): high = df['high'].to_numpy(float) low = df['low'].to_numpy(float) n = len(df) line = np.full(n, np.nan) for t in range(n): if t < lookback: continue val, _, _ = cbl_long(high, low, t, lookback, min_bars) line[t] = val df['cbl'] = line return df

这段代码能跑,但直接用它会踩一个很隐蔽的坑:锚点漂移。因为argmax是在一个滚动窗口里找的,随着窗口右移,昨天那个高点可能今天还在窗口里,明天就滑出去了,锚点就换成了窗口里的另一个点。锚点一换,倒数线的价位跟着跳变,图上线会呈现出一段一段的平台,中间夹杂着莫名其妙的跳空。

解决办法是引入状态:只在锚点发生变化时才重算,触发之后清空等待下一个锚点。这样倒数线在触发前是锁定的,跟实际交易的逻辑一致——你不可能每天都盯着一个移动的目标价做决策。

def build_cbl(df, lookback=20, min_bars=3, mode='long'): high = df['high'].to_numpy(float) low = df['low'].to_numpy(float) close = df['close'].to_numpy(float) n = len(df) line = np.full(n, np.nan) anchor = np.full(n, -1, dtype=int) signal = np.zeros(n, dtype=int) cur_val, cur_anchor = np.nan, -1 for t in range(n): if t < lookback: continue if mode == 'long': i0 = _swing_high_idx(high, t, lookback) if i0 != cur_anchor: val, _, _ = cbl_long(high, low, t, lookback, min_bars) if not np.isnan(val): cur_val, cur_anchor = val, i0 else: i0 = _swing_low_idx(low, t, lookback) if i0 != cur_anchor: val, _, _ = cbl_short(high, low, t, lookback, min_bars) if not np.isnan(val): cur_val, cur_anchor = val, i0 if np.isnan(cur_val): continue triggered = (close[t] > cur_val) if mode == 'long' else (close[t] < cur_val) if triggered: signal[t] = 1 line[t] = cur_val cur_val, cur_anchor = np.nan, -1 # 触发后作废,等下一个锚点 else: line[t] = cur_val anchor[t] = cur_anchor df['cbl_line'] = line df['cbl_anchor'] = anchor df[f'cbl_sig_{mode}'] = signal return df

提示:这里i0 != cur_anchor的判断在边界情况下会有个小问题——如果旧锚点被窗口挤出去、新的 argmax 落在更早的位置,程序会把它当成新锚点并重算。我试过加i0 > cur_anchor的严格判断,结果是某些震荡行情里倒数线再也不更新,更难用。权衡下来还是保留!=,代价是偶尔多出一次重算,可以接受。

性能上这是我刻意偷懒的写法,复杂度 O(n × lookback),十万根K线大概几秒钟。真要上高频或者参数量大的场景,可以把扫描过程改成递增式的状态机,但那个复杂度对绝大多数日线策略完全不值当。先把逻辑跑对,再谈优化——这句话我在很多个项目里都吃过教训。

4. 使用条件:这四条不满足,倒数线就是一条废线

4.1 硬条件一:至少要有三根抬升的低点

这就是前面min_bars那个参数的来源。为什么要设这个门槛?因为如果只数到一两根就找到了触发位,说明锚点高点之前的走势根本没形成连续的抬升结构,这个"高点"可能只是一个反弹的毛刺。用这种锚点算出来的倒数线,位置往往离锚点特别近,稍微一回调就触发,本质上是在追高。

我把不同min_bars设置下的信号特征做了个对照,样本是几段不同波动特征的日线数据,这里只给相对结论,具体数值你换标的就会变:

min_bars信号数量平均触发延迟触发后假突破比例适用场景
0(不限制)极多1-2根很高基本不能用
2多2-4根较高短线、高波动标的
3中等4-8根中等日线趋势跟随,我用的值
5 以上少10根以上低但会漏信号长周期、大级别

"触发后假突破比例"这一列是我自己最看重的指标。min_bars太小的时候,倒数线离锚点太近,价格稍微反弹一下就算突破,突破完继续跌,反复打脸。把门槛提到3以后,假突破明显减少,代价是错过一些快速启动的行情。这个权衡没有标准答案,取决于你的持仓周期能容忍多长的等待。

4.2 硬条件二:锚点必须是"有效高点"

这里有个特别容易忽略的点:argmax在窗口里找最大值,它一定会给你返回一个索引,哪怕那个"最高点"只比周围高了一点点、在图上根本看不出是个高点。程序没有审美,你得自己加约束。

我用的判断是最小突出度:锚点的高点必须比它左右各若干根K线的最高价高出一定比例(比如0.5%),才算一个可识别的波段高点。不满足就往前找次高点,或者直接放弃当天信号。

def _is_valid_anchor(high, idx, side=3, min_prom=0.005): lo = max(0, idx - side) hi = min(len(high), idx + side + 1) neighbors = np.concatenate([high[lo:idx], high[idx+1:hi]]) if len(neighbors) == 0: return False return (high[idx] - neighbors.max()) / neighbors.max() >= min_prom

加了这个过滤之后,我的信号数量掉了大概三成,但每一根倒数线画在图上都能一眼看出是"某个明显高点延伸出来的线",肉眼验收通过率高太多了。这条我强烈建议加上,它带来的收益比调任何参数都明显。

4.3 硬条件三:趋势方向得对得上

倒数线是一个趋势内的回撤入场工具,不是反转工具。这句话说起来简单,实现上却经常被忽略。如果你在下跌趋势里也算一条多头倒数线出来,那它大概率会在你刚买入之后被下一根阴线穿透。

趋势怎么判断,看你手上有什么。最朴素的做法是用两组均线:一组短的(比如3、5、8、10、12、15日)和一组长的(30、35、40、45、50、60日),短均线组整体在长均线组之上,认为是多头环境。这个思路要求"整组都在上面",比单根均线穿越稳定得多,因为它把短期均线的纠缠过滤掉了。

def trend_filter(df, short=(3,5,8,10,12,15), long=(30,35,40,45,50,60)): s = df['close'].rolling(short[0]).mean() for w in short[1:]: s = s + df['close'].rolling(w).mean() s = s / len(short) l = df['close'].rolling(long[0]).mean() for w in long[1:]: l = l + df['close'].rolling(w).mean() l = l / len(long) df['trend_up'] = s > l return df

把build_cbl的信号再和trend_up取交集,才是我真正拿去回测的入场条件。这一步不做,回测出来的胜率会虚高,因为你会在一堆下跌趋势的反弹里"成功"抓到几个小反弹,掩盖掉大量失败。

4.4 软条件:时间衰减和"线被穿烂"的失效判定

倒数线有个属性:它是有保质期的。如果锚点是在二十根K线之前形成的,价格已经在它上方走了很远,这条线作为入场触发位就失去意义了(你不会在离现价十几个点的位置等回调)。更糟的是,它作为止损参考也失真了——真跌到那儿,你的浮盈已经回吐了大半。

我的处理是给倒数线加一个最大有效期,超过N根K线还没触发就直接作废,等下一个锚点。N取多少跟你的持仓周期有关,我做日线波段,一般用15到20根。

另一种失效情形是"线被反复穿越"。如果价格在倒数线附近来回摩擦了四五次,每次都突破一点又跌回去,说明这个位置根本没有支撑意义,只是一条被噪音填满的水平线。这种时候即便最后真的突破了,后续也很容易失败。我通常会统计锚点形成以来收盘价穿越这条线的次数,超过3次就降低信号权重或者直接跳过。

注意:这两个软条件都会减少信号数量,但它们减少的是"看起来能赚钱、实际会亏钱"的那部分信号。做减法在策略开发里往往比做加法更有价值。

5. 画图核对:肉眼看不顺眼的线,回测里一定坑你

5.1 用 matplotlib 把线和锚点一起标出来

我一直有个习惯:任何指标写完,先画图看,别看回测指标。回测数字会骗人,图不会。下面是画蜡烛图加倒数线的代码,涨红跌绿,倒数线用阶梯线(steps-post),因为它在一段时间内是恒定值、只在重算时跳变。

import matplotlib.pyplot as plt def plot_cbl(df, start, end, title=''): sub = df.iloc[start:end].reset_index(drop=True) x = np.arange(len(sub)) fig, ax = plt.subplots(figsize=(15, 7)) ax.vlines(x, sub['low'], sub['high'], color='#666', lw=0.8) up = sub['close'] >= sub['open'] bottom = np.minimum(sub['open'], sub['close']) height = (sub['close'] - sub['open']).abs().clip(lower=1e-8) ax.bar(x[up], height[up], bottom=bottom[up], width=0.6, color='#c0392b', edgecolor='#c0392b') ax.bar(x[~up], height[~up], bottom=bottom[~up], width=0.6, color='#27ae60', edgecolor='#27ae60') ax.plot(x, sub['cbl_line'], drawstyle='steps-post', color='#2c3e50', lw=1.6, label='Count Back Line') a = sub['cbl_anchor'].to_numpy() mask = a >= 0 if mask.any(): ax.scatter(x[mask], sub['high'].to_numpy()[mask], marker='v', s=60, color='#8e44ad', zorder=5, label='Anchor') sig = sub['cbl_sig_long'].to_numpy() == 1 if sig.any(): ax.scatter(x[sig], sub['low'].to_numpy()[sig] * 0.995, marker='^', s=80, color='#e67e22', zorder=5, label='Trigger') ax.set_title(title) ax.legend(loc='upper left') ax.grid(alpha=0.25) plt.tight_layout() return fig, ax

drawstyle='steps-post'这个细节别省。用普通plot画出来的斜线会让读者以为倒数线是连续变化的,实际上它在一段时间内是固定价位,只在锚点更新时才跳变。可视化上的小失真,会直接导致你对策略行为产生错误直觉。

5.2 三种典型形态的正确与错误画法对照

我整理了自己在图上反复对照出来的三类情形,你可以拿去当验收清单。

第一类:干净的阶梯式回调。锚点是一个明显的高点,之后三四根K线低点依次抬高,倒数线落在抬升结构的起点。价格回调到线附近,一根小阳线站上去,信号触发。这是标准形态,如果你的代码画不出这种,基本可以判定逻辑有问题。

第二类:锚点后立刻大跌。锚点的高点出现后,第二天直接跳空低开、低点远低于前一天。扫描循环在第一步就断了,cnt=0,返回NaN。这时候不会有任何信号,也不该有。新手常犯的错是"硬找一个价位"——加个兜底逻辑,把最近的若干根K线最低价拿来充数。这个兜底会毁掉整个工具,宁可没有信号,也不要有假信号。

第三类:箱体震荡里的伪锚点。价格在一个横盘区间里来回晃,argmax每两天就换一个位置,倒数线跟着上下跳,信号密集得离谱。这种行情下正确的做法是不交易,而不是去优化参数。判断依据是锚点的突出度(第4.2节那个过滤)和趋势过滤器,两者都能把这类信号挡掉大半。

5.3 我自己踩过的三个错位问题

错位一:用了复权后的数据,但对不上复权基准。如果你的行情源给的是前复权价,而你的锚点判断是用另一份未复权数据做的,倒数线会整体错位一截。这个坑很隐蔽,因为线看起来"形状是对的",只是位置差一点。建议全流程用同一份复权数据,中间不要混。

错位二:索引错位。argmax返回的是窗口内的相对索引,必须加上窗口起点才是全局索引。我第一版就漏了这一步,导致画出来的锚点总是落在窗口的左边界上,花了一晚上才找到。

错位三:盘中数据导致的线漂移。如果你在盘中用实时数据算倒数线,当天的最高价和最低价还在变,锚点和倒数线都会跟着变。同一个信号,上午看有、下午看没了。做法是只在收盘后计算,第二天用固定价位执行,这样才算可复现。

6. 回测落地:从"触发价"到"成交价"之间的那些损耗

6.1 突破用收盘价确认还是盘中价确认

倒数线是一个价位,价格"穿过"它的方式有很多种。用收盘价确认(close > cbl)意味着你要等到收盘才算信号,第二天开盘进场;用盘中价确认(high > cbl)意味着当天就能成交,但会被日内毛刺反复触发。

两种我都试过。收盘确认的好处是信号干净、几乎不会因为一根长上影被误触发,坏处是经常吃掉第二天的高开——尤其是有明显突破形态时,第二天直接跳空上去,你的实际成本比倒数线高出一大截。盘中确认的好处是贴着理论价成交,坏处是假信号多,我实测下来触发频率大约是收盘确认的两倍多。

我现在的做法是折中:信号用收盘确认,但记录下倒数线的价位,回测时用"第二天开盘价与倒数线中较高的那个"作为成交价。这样既能过滤毛刺,又不会在跳空时假装自己买在了理论价上。

prev_line = df['cbl_line'].shift(1) prev_sig = df['cbl_sig_long'].shift(1) == 1 df['entry_price'] = np.where( prev_sig, np.maximum(df['open'], prev_line.fillna(-np.inf)), np.nan )

这个np.maximum是关键,很多人写回测直接拿信号价当成交价,等于假设自己永远能买在最低的那个点。这个假设在单边行情里会让回测收益虚高20%以上,实盘根本复现不了。

6.2 跳空和大阳线:理想的触发在实际里长什么样

倒数线的触发经常伴随着跳空。价格在倒数线上方开盘,直接高开三四个点,这时候你面临一个真实的选择:追还是不追。

我的规则是给一个最大滑点容忍度。如果开盘价高于倒数线超过某个比例(我做日线,用2%左右),就放弃这次信号,等价格回落到容忍区间再考虑。这个规则会让我错过一些强势启动,但很好地避免了"追在当天最高点被套"的情况。

还有一种情况是一根大阳线直接穿过倒数线并且收在很高位置。这种形态在图上非常漂亮,回测里也很赚,但实盘你大概率买在阳线中后段,成本要比倒数线高不少。回测时对这种"大实体穿越"要单独加惩罚,否则你会高估这类信号的实际收益。

触发形态回测理论成交价现实中的成交估计偏离程度
小阳线贴线突破倒数线价位倒数线附近小
跳空高开1%以内倒数线价位开盘价小到中
跳空高开2%以上倒数线价位建议放弃大
大阳线穿线(实体>3%)倒数线价位阳线中段大

6.3 倒数线的第二重身份:初始止损与仓位换算

这一节是我认为倒数线最有价值、却最少被讲清楚的部分。它不只是入场触发位,同时还是一个技术上合理的初始止损位——因为跌破它就意味着多头结构被破坏,继续持有的理由消失了。

用法是这样:你的入场价是实际成交价 P,倒数线位置是 L,那么每单位标的的风险就是 (P - L)。如果你愿意在单笔交易上承担总资金 R% 的风险,那么可买入数量就是:

数量 = (总资金 × R%) / (P - L)

这个公式看起来平平无奇,真正有意思的是它带来的自动调节:当倒数线离现价很近(回调浅)时,仓位会很大;离得远(回调深)时,仓位会自动缩小。也就是说,同样的风险预算下,策略会自动在"结构紧凑"的机会上多下注、"结构松散"的机会上少下注。这比固定仓位高明太多,因为它把风险控制和图形结构绑定在了一起。

我做过对比:同样一批信号,固定仓位和倒数线换算仓位跑出来的最大回撤差了将近一倍,而总收益几乎没差。风险调整后的表现提升全部来自仓位这一环,信号本身一模一样。所以如果你只从这篇里拿走一件事,我希望是这个仓位公式。

提示:用这个公式前先检查 (P - L) 是否过小。有些信号倒数线几乎贴着入场价,算出来的仓位可能是你总资金的几倍,这显然不能执行。我一般会加一个下限,比如 (P - L) 至少要等于标的日均波动的一定倍数,不够就跳过这次信号。

7. 参数与标的的适配:我最后留下的那套配置

7.1 回看窗口:10、20、40的实测差异

lookback是控制锚点搜索范围的参数,它的作用很容易被误解。它不是"倒数线往前看多远",而是"在哪一段区间里找最高点"。这两个是不同的事情。

窗口太小(比如10),锚点会被锁死在最近的高点上,稍微长一点的走势中,真正有意义的波段高点在窗口外,你就永远找不到它。窗口太大(比如40),锚点容易落在很久以前的高点上,算出来的倒数线位置跟当前价格严重脱节,触发条件形同虚设。

我实测下来,日线级别用18到22比较合适,20是我的默认值。周线的话要相应放大到12到15周。这个参数对信号数量的影响很直接:窗口从10调到20,我样本里的信号数大概翻了一倍,因为更多有效的锚点被纳入了检索范围。再往上调到40,信号数只增加一点点,但无效信号的比例明显上升。

7.2 不同波动特征标的上的表现差异

这一点我必须说清楚,因为它决定了倒数线到底适不适合你的标的。

倒数线的核心假设是低点序列的连续性有意义。在趋势性强、K线排列有序的标的上,这个假设基本成立,画出来的线逻辑清晰、触发合理。但在高频震荡、日内噪音大的标的上,相邻K线的低点关系基本上是随机的,while循环走不了两步就断,你要么得到一堆NaN,要么把min_bars降到1换来一堆假信号。这两种结果都不能用。

我的做法是先做一次结构连续性体检:统计标的在样本期内,low[j-1] > low[j]这个条件成立的比例。如果是50%上下,说明低点关系基本随机,倒数线在这上面没有信息量;如果在60%到70%之间,说明存在一定的趋势惯性,值得继续做。这个检验几十行代码就能写,比你调三天参数有用得多。

def continuity_ratio(df, direction='up'): high = df['high'].to_numpy(float) low = df['low'].to_numpy(float) if direction == 'up': cond = low[:-1] > low[1:] else: cond = high[:-1] < high[1:] return float(cond.mean())

7.3 我最终固定下来的参数和状态机

把上面所有取舍收拢一下,我现在用的配置是:

参数取值说明
lookback20日线锚点搜索窗口
min_bars3至少3对相邻抬升,即4根K线
锚点突出度0.5%相对左右各3根K线的最高价
最大有效期18根超时重算,不沿用旧线
穿越次数上限3超过则跳过
趋势过滤双均线组相对位置短组整体在长组之上
触发确认收盘价成交价取次日开盘与线价较高者
仓位按 (P-L) 换算单笔风险预算固定

状态机这块,我前面给的build_cbl已经是简化版的状态机了,它有四个明确状态:无锚点 → 有锚点待触发 → 已触发 → 作废重找。我建议你在实际工程里把这个状态显式记录下来,写成字符串列,回测出问题时一眼就能看出某个信号是在哪个状态下产生的,比对着十几个布尔标记猜半天强得多。

df['cbl_state'] = np.select( [df['cbl_line'].notna() & (df['cbl_sig_long'] == 0), df['cbl_sig_long'] == 1], ['pending', 'triggered'], default='idle' )

最后分享一个我在调试阶段常用的小技巧:把倒数线和锚点单独导出一张表,列包括日期、锚点日期、锚点价格、倒数线价格、抬升次数、触发状态。然后拿这张表去跟图上手工核对十来个样本点。只要这十个点全对得上,后面成千上万个信号你就可以放心地信任代码了。这个验证习惯帮我省下过无数次"回测跑完才发现逻辑反了"的返工。

这套东西我自己前后迭代了三四版才稳定下来,中间最浪费时间的就是一开始想当然地以为"倒数线就是前低"。真正把它拆开看,锚点怎么选、数到哪停、什么时候重算、触发后怎么处理,每一个决策点都会实质性地改变策略行为。参数本身反而没那么重要,重要的是你把每个选择背后的逻辑想清楚了,换任何标的、任何周期,你都知道该动哪个旋钮。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/31 4:46:50

从Hey Siri到智能交互:语音识别与机器学习的工程实践

1. 从一句唤醒词说起&#xff1a;语音交互到底在背后做了什么对着手机喊一声“Hey Siri”&#xff0c;屏幕亮起、一个波形动画弹出来&#xff0c;然后你说“帮我定个明早七点的闹钟”&#xff0c;它回你“好的&#xff0c;已设定”。整个过程两三秒&#xff0c;自然得像跟人说话…

作者头像 李华
网站建设 2026/10/31 4:45:40

i2 Analyst‘s Notebook 8培训教材:概念、演练与案件串并

简介&#xff1a;这份 i2 Analysts Notebook 8 培训教材以单一 doc 文档交付&#xff0c;压缩包约 25.9MB&#xff0c;共 1 个文件&#xff0c;适合犯罪侦查、金融调查、安全分析等领域希望系统掌握数据可视化与关联分析工具的学习者。内容从软件入门、节点与边、时间线、聚类等…

作者头像 李华
网站建设 2026/10/31 4:45:18

大数据与计算机视觉融合架构的设计与优化

1. 项目概述&#xff1a;大数据与计算机视觉的融合架构当TB级的海量图像数据遇上分布式计算框架&#xff0c;一场关于效率与精度的技术革命正在发生。作为在数据工程领域深耕多年的实践者&#xff0c;我见证了传统数据架构在处理视觉内容时的力不从心——单机OpenCV处理千万级图…

作者头像 李华
网站建设 2026/10/31 4:43:32

语音大模型GA:当TTS会“演戏”,英语学习与视频创作如何受益

Gemini 3.8 语音大模型 GA&#xff0c;消息放出来的时候&#xff0c;我朋友圈里做教育产品和短视频的几个人几乎是同时转的。原因很简单&#xff1a;这一版 TTS 不再是"把文字念出来"&#xff0c;而是能把一句话按语境和角色"演出来"。用发布会演示里那个例…

作者头像 李华
网站建设 2026/10/31 4:42:51

零跑汽车全域自研技术解析与豪华转型

1. 零跑汽车的品牌转型之路零跑汽车从创立之初就确立了"半价理想"的市场定位&#xff0c;这一策略在初期为其赢得了可观的市场份额。所谓"半价理想"&#xff0c;指的是零跑以理想汽车一半左右的价格&#xff0c;提供相似级别的产品体验。这种定位在2020年前…

作者头像 李华
网站建设 2026/10/31 4:42:45

数据库锁机制详解:从原理到实战优化

1. 数据库锁的本质与核心价值数据库锁就像图书馆的借阅登记系统。想象一下&#xff0c;当多位读者同时想借阅同一本畅销书时&#xff0c;图书管理员会在借出时登记"已借出"&#xff0c;其他读者只能等待归还。数据库锁机制就是数字世界的"图书管理员"&…

作者头像 李华