做量化的人大多经历过这个阶段:指标越堆越多,均线、MACD、KDJ 叠满一屏,结果行情一震荡,信号全打架。我后来做趋势转折类的策略,反而越来越偏爱那些"用价格结构自己说话"的工具,顾比倒数线就是其中一个。它不画均线、不算指标值,只干一件事——从一段下跌的最低点往回数三根 K 线,用这三根 K 线的高点关系,判断这个低点到底是不是真的低点。这篇文章我把顾比倒数线的画法、使用条件,以及怎么把它写进Python 量化脚本里跑通,一次性讲透。适合已经会写 pandas、想做趋势转折策略的朋友,也适合刚接触量化、想找一个逻辑干净、可验证的入场规则的读者。全文的代码都可以直接抄去改,参数我会把设定理由一起说清楚。看完你至少能得到三样东西:一条能自动画出来的倒数线、一套明确的多空确认条件、一份踩过的坑清单。
1. 先搞清楚这条线到底在解决什么问题
1.1 从趋势体系的定位说起
顾比倒数线不是一个孤立的买卖信号,它是趋势转折确认环节里的一个"门槛"。任何一段下跌,中间都会出现无数个局部低点,其中只有一个(或少数几个)会真正变成趋势的起点。问题在于,低点在当时是看不出来的——你站在最低点那根 K 线上,往左看是一路阴线,往右看是空白,你凭什么说这里就是底?绝大多数亏钱的操作,都是在"看起来像底"的位置反复抄底,然后被下一根阴线打脸。
倒数线的思路很朴素:既然低点无法预判,那就等它自己证明。具体做法是把"低点成立"这件事,转化成一个可量化的价格条件——从最低点往回数三根 K 线,如果这三根的最高价呈现依次抬高的结构,说明下跌的斜率在衰减,抛压在收敛;此时把这三根里最早那根的最高价画成一条水平线,价格收盘重新站上这条线,就确认转折成立。注意这里的关键:线不是画在最低点上,而是画在低点左侧的高点上,这条线天然高于最低点,所以它给出的买点是有"溢价"的,这个溢价买的就是确定性。
我在实盘和回测里反复验证过一点:这条线最大的价值不是提高胜率,而是过滤掉大量"下跌中继"的假反转。它把"我觉得是底"变成了"结构证明是底",这个转变对策略稳定性的贡献,比多加三个指标都大。
1.2 用"数数"的方式理解倒数逻辑
名字里的"倒数",很多人第一次听会懵,其实它就是个方向问题。常规思路是从左往右数——第一根、第二根、第三根,顺着时间走。倒数线是反过来的:以最低点那根 K 线为起点,往回(向历史方向)数,最低点是第 0 根,它左边一根是第 1 根,再左边是第 2 根。数到第 2 根,凑齐三根,检查这三根的最高价是不是"越靠右越高"。
生活里有个很贴切的类比:判断一个人跑步是不是在减速,你不会去看他起跑时多快,而是看最后三步的步幅——如果最后三步一步比一步大,说明他还在加速;如果最后三步一步比一步小,说明他在收力。倒数线看的就是下跌末端的"最后三步":最低点那根、它左边一根、再左边一根。如果这三根的高点依次抬高,说明空头在最后阶段已经推不出更低的反弹高点了,节奏被打断,这时候才有理由怀疑趋势要换。
这个类比还带出一个容易被忽略的细节:倒数线只关心高点,不关心低点。很多新手习惯用收盘价或者最低价去算,结果线画出来位置完全不对。原因很简单,低点是用来定位"数数的起点"的,真正的结构信息在反弹高点里。你如果拿最低价去比,得到的往往是"低点不断降低"的空头排列,那是顺势信号,不是转折信号。
1.3 它明确不适合什么场景
任何工具都有失效区,把失效区讲清楚比讲成功案例更有用。顾比倒数线在三种行情里表现很差,我在早期吃过大亏。
第一种是单边急跌。连续跌停或者跳空暴跌的品种,三根 K 线的高点关系完全被缺口破坏,倒数线要么画不出来,要么画出来位置极低,一突破就追进去,紧接着继续跌。这种行情里倒数线的"三根 K 线"样本太小,根本代表不了抛压的真实变化。
第二种是超窄幅横盘。K 线的高点差几个价位,所谓"依次抬高"可能只是 0.1% 的随机波动,这种结构毫无意义。我一般会加一个最小振幅过滤,比如三根 K 线的最高价与最低价之差,至少要占到价格的 1.5% 以上,否则直接判定这条线无效。
第三种是流动性差、K 线毛刺多的品种。分钟级别的低流动性标的,一根插针就能把最低点打歪,倒数线的起点错了,后面全错。所以我的默认建议是:倒数线优先用在日线及以上周期,且标的日均成交额要过门槛。这条不是玄学,是纯粹的数据质量问题——数据脏了,再精巧的算法都是在算噪声。
2. 画法逐条拆解:手把手算一遍
2.1 三个构成要素,缺一不可
把画法拆开,其实只有三个要素,我用一张表列清楚,后面写代码就照这个表来。
| 要素 | 具体定义 | 常见错误 |
|---|---|---|
| 起点位置 | 一段下跌中的最低价所在 K 线,记为第 0 根 | 用了收盘价最低而非最低价最低 |
| 取样范围 | 第 0 根、第 1 根(左一)、第 2 根(左二),共三根 | 数成四根或只数两根 |
| 结构条件 | 高点满足 H₂ < H₁ < H₀,即越靠右越高 | 写成 H₀ < H₁ < H₂,方向搞反 |
| 线位取值 | 取最左侧那根(第 2 根)的最高价 H₂ | 取了最低点价格或三根最高价的最大值 |
这张表里最后一行值得多说两句。线位为什么取"最左边那根"的高点,而不是三根里的最大值?因为倒数线的逻辑是"突破前期反弹高点"。三根里最右边那根就是低点本身,它的高点往往很低(因为收在低位);中间那根是第一次反弹尝试;最左边那根是下跌途中的最后一次像样的反弹高点。要让结构成立,价格必须一步步把这个反弹高点顶过去,所以线画在最左边那根的高点上,它天然是三根里最高的那个——如果它不最高,H₂ < H₁ < H₀ 这个条件本身就不成立。换句话说,条件成立时,"最左"和"最大"是等价的,但写成"最左"更贴合原意,也更好在代码里定位。
2.2 拿一组真实结构的数据手推一遍
假设某标的从高位下来,最近一段的 K 线数据是这样的(只列关键字段,价格做了简化):
| 序号 | 最高价 | 最低价 | 收盘价 | 备注 |
|---|---|---|---|---|
| T-6 | 12.80 | 12.10 | 12.20 | |
| T-5 | 12.35 | 11.80 | 11.85 | 第一次反弹失败 |
| T-4 | 11.95 | 11.42 | 11.50 | |
| T-3 | 12.28 | 11.70 | 11.75 | 第 2 根(左二),H₂=12.28 |
| T-2 | 12.05 | 11.55 | 11.60 | 第 1 根(左一),H₁=12.05 |
| T-1 | 11.72 | 11.10 | 11.18 | 第 0 根(最低点),H₀=11.72 |
先定位最低点:T-1 的最低价 11.10,是这段里最低的,确认为第 0 根。然后取三根:T-3、T-2、T-1。检查高点:11.72 > 12.05?不成立,H₀ 小于 H₁。所以这个结构不满足"依次抬高",倒数线画不出来,判断为"结构未确认,继续等待"。
这时候很多人会着急,觉得明明跌了这么久怎么还不能买。这恰恰是对的——T-1 的高点 11.72 低于 T-2 的 12.05,说明最后一根 K 线的反弹力度还不如前一根,空头仍在主导。正确做法是把观察窗口往后推,等新的 K 线出来,重新判断最低点是否下移。
再看一个成立的结构:
| 序号 | 最高价 | 最低价 | 备注 |
|---|---|---|---|
| T-4 | 11.90 | 11.35 | 第 2 根,H₂=11.90 |
| T-3 | 12.15 | 11.50 | 第 1 根,H₁=12.15 |
| T-2 | 12.42 | 11.62 | 第 0 根,H₀=12.42 |
| T-1 | 12.60 | 12.10 | 突破那根,收盘 12.55 |
T-2 的最低价 11.62 是窗口内最低,作为第 0 根。三根高点:11.90、12.15、12.42,依次抬高,条件成立。倒数线 = H₂ = 11.90,画一条 11.90 的水平线。T-1 收盘 12.55,高于 11.90,且收盘价高于线位,突破确认。这条线的意义是:11.90 是这段下跌里最后一个像样的反弹高点,突破它,说明市场愿意用高于前次反弹的价格成交,抛压释放完毕。
注意 T-1 那根的位置,它是"确认 K 线",不是"信号预测"。所有信号都在收盘后产生,盘中突破只能作为盘中预警,不能算正式信号,这一点后面讲回测时会重点说。
2.3 反方向的顶点倒数线怎么画
下跌有低点,上涨自然有高点,逻辑完全对称。找一段上涨中的最高点,记为第 0 根,往回数两根凑齐三根,要求最低价满足 L₂ > L₁ > L₀,也就是越靠右越低(上涨末端回踩越来越深),线位取最左侧那根的最低价 L₂,价格收盘跌破这条线,确认上涨结束。
| 要素 | 低点倒数线(做多) | 顶点倒数线(做空/离场) |
|---|---|---|
| 起点 | 最低价所在 K 线 | 最高价所在 K 线 |
| 排序条件 | H₂ < H₁ < H₀ | L₂ > L₁ > L₀ |
| 线位取值 | H₂(最左一根最高价) | L₂(最左一根最低价) |
| 触发条件 | 收盘价上穿线位 | 收盘价下穿线位 |
| 典型用途 | 趋势反转做多、空头平仓 | 多头止盈、趋势反转做空 |
实际使用中,做空方向的倒数线我建议只用来做多头的离场信号,而不是直接反手做空。原因是我做过统计,在多数权益类品种上,向上突破后的趋势延续性明显强于向下破位,做空信号的假突破比例更高,尤其是流动性一般的标的。用它来管住利润,比用它来赚新钱靠谱得多。
2.4 关于"严格版"和"宽松版"的取舍
画法上有个分歧点必须交代清楚:低点必须是最新的那根吗?有两种实现。
严格版:以当前最新 K 线为第 0 根,直接往前取两根,检查三根高点是否递增。优点是完全无歧义、零延迟;缺点是绝大多数时候条件不成立,信号极少,而且一旦最低点已经过去好几天,再往后数就数不到它了,会漏掉真正的转折。
宽松版(我采用的):先用某种方式识别出"最近一个显著低点",以它为第 0 根往前取两根检查。如果条件不成立,就从这个低点往前逐格滑动,找一个满足条件的三根组合。这样线的位置依然由低点决定,但不会因为低点不在最新位置而漏判。
宽松版的问题是需要定义"显著低点"。我常用的做法是分形法:某根 K 线的最低价同时低于左侧 N 根和右侧 N 根的最低价,N 取 3。这个做法的代价是有右侧 N 根的确认延迟——第 0 根出现后,要等 3 根 K 线走完才能确认它是不是低点,这 3 根的延迟是硬成本,也是回测里最容易作弊的地方,后面第 6 章会专门讲。
两个版本的信号数量差得很远,我做过对比,同一段三年的日线数据,严格版出信号不到 5 次,宽松版出 30 次左右。样本量太少就没法做统计,所以我最终选了宽松版,但把延迟也如实计入回测。
3. Python 实现:把画法翻译成可复现的代码
3.1 数据结构与字段口径约定
代码之前先把口径钉死,否则后面全是坑。我要求数据表至少有六列:datetime、open、high、low、close、volume,索引统一为按时间升序的 DatetimeIndex,价格必须是后复权(做回测用后复权,做信号跟踪用不复权看真实盘口,两套数据不要混用)。
import pandas as pd import numpy as np def prepare(df: pd.DataFrame) -> pd.DataFrame: df = df.copy() df.columns = [c.lower() for c in df.columns] df["datetime"] = pd.to_datetime(df["datetime"]) df = df.sort_values("datetime").drop_duplicates("datetime") df = df.set_index("datetime") # 价格类字段统一转 float,缺数据的行直接剔除 for c in ["open", "high", "low", "close", "volume"]: df[c] = pd.to_numeric(df[c], errors="coerce") df = df.dropna(subset=["high", "low", "close"]) # 数据自检:high 必须 >= low,收盘必须落在高低之间 bad = (df["high"] < df["low"]) | (df["close"] > df["high"]) | (df["close"] < df["low"]) if bad.any(): print(f"[警告] 发现 {bad.sum()} 根异常K线,已剔除") df = df[~bad] return df这段看着啰嗦,但异常 K 线不清掉,倒数线会被一根错误数据带偏整个结构。我实际跑过的几十个数据集里,几乎每一个都有零星的脏行,来源五花八门:除权未调整、节假日补数据、接口返回重复。加五行校验,能省掉后面几小时的排查。
3.2 低点识别:分形法加振幅过滤
def find_pivot_lows(df: pd.DataFrame, n: int = 3, min_amp: float = 0.015): low = df["low"].values high = df["high"].values idx = [] for i in range(n, len(df) - n): window_low = low[i - n: i + n + 1] if low[i] != window_low.min(): continue # 振幅过滤:该低点附近的价格区间不能太窄 local_high = high[i - n: i + n + 1].max() if local_high <= 0: continue if (local_high - low[i]) / low[i] < min_amp: continue idx.append(i) return idxn=3意味着左右各看三根,一个低点要确认,必须等右边三根走完,也就是三根 K 线的延迟。min_amp=0.015是我按经验设的振幅下限,用意在第 1 章说过:过滤掉那几个价位之差的伪结构。这个参数不要拍脑袋,后面第 5 章会做敏感性扫描,你会看到它对信号数量的影响非常大。
还有一个细节:分形法用严格不等号还是允许相等?我用的是low[i] != window_low.min()判断,也就是说如果两根 K 线最低价完全相同,只有最左边那根会被保留。这个处理避免了平台底(两根并排最低)被重复计算,实战里更稳。
3.3 倒数线的核心计算
def countdown_line(df: pd.DataFrame, low_idx: int, max_slide: int = 3): """ 以 low_idx 为起点(第0根),向左数三根, 要求 high 依次抬高,返回 (线位, (i2, i1, i0)) 若不满足,向左滑动最多 max_slide 格重新尝试 """ h = df["high"].values for shift in range(0, max_slide + 1): i0 = low_idx - shift i1, i2 = i0 - 1, i0 - 2 if i2 < 0: break h0, h1, h2 = h[i0], h[i1], h[i2] if h2 < h1 < h0: return float(h2), (i2, i1, i0) return np.nan, Nonemax_slide=3是宽松版的滑动上限。为什么不无限滑?因为滑得太远,那三根 K 线跟当前低点就没关系了,结构意义消失。三格以内还能说得通——相当于允许一两根 K 线的高点被"跳过"。这个参数我试过 0 到 8,超过 4 之后信号质量明显下滑,假信号变多,最终定在 3。
h2 < h1 < h0这行是全篇最核心的一行代码,写成链式比较,既表达了三个数严格递增,也避免了and写错顺序。
3.4 状态机:信号触发、失效与重置
单靠上面两个函数只能算出"某一天有一条线",但策略需要知道"当前是否持有一条有效线"。这就需要一个状态机,我把它写成逐日推进的循环版本,可读性优先。
def build_signals(df: pd.DataFrame, n: int = 3, min_amp: float = 0.015, valid_bars: int = 15, stop_atr: float = 1.5): df = df.copy() # 预计算 ATR 用于止损 pc = df["close"].shift(1) tr = pd.concat([df["high"] - df["low"], (df["high"] - pc).abs(), (df["low"] - pc).abs()], axis=1).max(axis=1) df["atr"] = tr.rolling(14).mean() pivots = find_pivot_lows(df, n=n, min_amp=min_amp) pivot_set = set(pivots) active_line = np.nan # 当前有效的倒数线 active_low = np.nan # 对应的低点价格 active_age = 0 # 线已存续的K线数 pivot_buffer = [] # 已确认但尚未处理的分形低点 signals = [] low_arr = df["low"].values close_arr = df["close"].values atr_arr = df["atr"].values dt_arr = df.index for i in range(len(df)): # 1. 有新的分形低点确认,尝试画线 if i in pivot_set: pivot_buffer.append(i) if not np.isnan(active_line): active_age += 1 # 2. 价格创新低,原线作废 if low_arr[i] < active_low: active_line, active_low, active_age = np.nan, np.nan, 0 # 3. 超时未触发,作废 elif active_age > valid_bars: active_line, active_low, active_age = np.nan, np.nan, 0 if np.isnan(active_line) and pivot_buffer: # 取最近一个分形低点尝试画线 for p in sorted(pivot_buffer, reverse=True): line, _ = countdown_line(df, p) if not np.isnan(line): active_line = line active_low = low_arr[p] active_age = 0 break # 4. 突破判断:收盘价上穿线位 if not np.isnan(active_line): prev_close = close_arr[i - 1] if i > 0 else np.nan if close_arr[i] > active_line and prev_close <= active_line: stop = active_low - stop_atr * atr_arr[i] signals.append({ "date": dt_arr[i], "close": close_arr[i], "line": active_line, "low": active_low, "stop": stop, "risk_pct": (close_arr[i] - stop) / close_arr[i] }) active_line, active_low, active_age = np.nan, np.nan, 0 return pd.DataFrame(signals)这段代码里有几个设计决定,值得单独说明。
为什么要求"上穿"而不是"高于":close > line and prev_close <= line,这样同一段行情只会在突破当天出一次信号,不会连续几十天重复触发。如果只写close > line,回测里会看到同一个低点被反复买入,仓位直接爆掉。我最早就是栽在这个地方,回测收益曲线漂亮得离谱,后来发现是重复计数。
为什么要作废条件:价格创新低,说明原来那个低点根本不是低点,线自然失效;超时 15 根 K 线还没突破,说明市场在磨,结构已经陈旧,也作废。这两个条件保证了不会拿着一根三个月前的线去交易今天的行情。
止损为什么放在低点下方:倒数线的逻辑前提是"这个低点是有效的转折低点",那么最直接的证伪方式就是跌破它。用低点 - 1.5 × ATR留一点缓冲,避免被插针扫掉。ATR 倍数这个参数我在 1.0 到 2.5 之间扫过,1.5 是性价比比较平衡的位置,再小容易被噪声打掉,再大单笔亏损吃掉的利润太多。
3.5 仓位与风险控制
def size_position(capital: float, risk_per_trade: float, entry: float, stop: float, lot: int = 100) -> int: risk_per_share = entry - stop if risk_per_share <= 0: return 0 shares = int(capital * risk_per_trade / risk_per_share) return (shares // lot) * lot固定风险法,每笔交易最大亏损占总资金的 0.5% 到 1%。这里用entry - stop而不是百分比,是因为倒数线的止损距离本身就是变动的——波动大的时候止损远,仓位自然就小,波动小的时候仓位大,风险敞口始终恒定。这个设计比固定手数稳健得多,尤其配合 ATR 止损,两者是同一套逻辑的两面。
4. 使用条件详解:什么时候这条线才真的算数
4.1 趋势前提:下跌得有"段落感"
倒数线是转折工具,转折的前提是得有一个要转折的东西。如果标的长期横盘,随便一个低点画出来的线,突破了也只是区间内的一次普通波动,没有任何趋势意义。我的判断标准是:低点之前必须存在一段清晰的下跌,用简单的量化方式表达就是——低点前 20 根 K 线的累计跌幅要超过某个阈值(我常用 8%),且这段下跌中至少有一根 K 线的跌幅大于 2%。
这个条件听着粗糙,但它把"顺势下跌后的反转"和"横盘里的随机波动"区分开了。我在回测里做过对照,加上这个前置条件后,信号数量减少大约四成,但平均单笔收益提升明显,胜率也有改善。原因不难理解:在一段真正的下跌之后,市场的反弹动能更充足,突破后的空间更大;而横盘里的突破往往马上被拉回。
提示:这个 8% 的阈值跟品种波动率强相关。做高波动标的要往上调,做低波动标的要往下调,更稳妥的做法是用标的自身的波动率做标准化,比如要求跌幅超过 20 日 ATR 的 2 倍。
4.2 突破的有效性验证:三个辅助条件
信号当天收盘成立之后,我还会检查三个条件,全中才建仓,中两个降半仓,只中一个直接放弃。
第一个是量能:突破当天的成交量要高于过去 20 日均量的 1.2 倍。放量突破说明有增量资金参与,是"真突破"最朴素的证据。缩量突破我见过太多,往往第二天就回落,尤其是在弱势市场。
第二个是突破幅度:收盘价至少高出线位 0.5%,或者高出 0.3 倍 ATR。贴着线位收上去一分钱,这种只能叫"摸线",不能叫"破线",容错空间几乎为零。
第三个是K 线形态:突破那根不能是长上影。我统计过,突破日留下长上影(上影线长度超过实体的 1.5 倍)的信号,后续表现显著偏弱,因为这说明盘中有资金在高位兑现。这个条件不算严格,但很有用。
| 验证条件 | 阈值 | 不满足时处理 |
|---|---|---|
| 突破日量能 | > 20 日均量 × 1.2 | 放弃或降半仓 |
| 突破幅度 | 收盘 > 线位 × 1.005 或 +0.3ATR | 放弃 |
| K 线形态 | 上影线 < 实体 × 1.5 | 降半仓观察 |
| 大盘环境 | 指数不在急跌段 | 降仓或空仓 |
| 板块联动 | 同板块有其它标的同步走强 | 加分项 |
4.3 周期适配:日线为主,周线为辅
我个人的配置是:周线用来定方向,日线用来找入场。周线上出现倒数线突破,说明中期底部结构形成,这时候在日线上找同方向的信号,胜率明显更高。反过来,如果周线还在下跌趋势里,日线的倒数线信号我只做到"轻仓短线",不重仓拿。
分钟级别我也试过,结论是能用但很挑品种。5 分钟、15 分钟周期上,倒数线的三根 K 线样本太短,信号密度高得离谱,一天能出好几次,交易成本直接吃掉利润。要玩短周期,必须配合更高的振幅门槛和更强的过滤条件,而且只在流动性最好的时段(开盘后一小时、收盘前一小时)做,其它时段信号全部忽略。
4.4 组合使用:倒数线 + 一条均线就够
我不建议给倒数线再叠一堆指标,但加一条中期均线(比如 20 日或 30 日)确实有用。用法很简单:倒数线突破如果同时伴随价格站上均线,视为强信号;如果价格还在均线下方很远,说明只是超跌反弹,仓位要收着点。
这个组合的逻辑是:均线反映的是中期资金的成本,倒数线反映的是短期结构的转折。两者共振时,说明短期转折和中期成本修复同步发生,这种信号的延续性最好。我做过分组统计,共振信号的持仓周期中位数明显长于纯倒数线信号,说明它不是靠频繁进出赚小钱,而是真能抓住一段趋势。
5. 回测与参数调试:用数据说话
5.1 一个最小可用的回测框架
def backtest(df, signals, hold_max=20, fee=0.0012): trades = [] close = df["close"] for _, s in signals.iterrows(): entry_date = s["date"] entry = s["close"] stop = s["stop"] idx = df.index.get_loc(entry_date) exit_price, exit_date, reason = None, None, None for j in range(idx + 1, min(idx + 1 + hold_max, len(df))): low_j = df["low"].iloc[j] close_j = df["close"].iloc[j] if low_j <= stop: exit_price, exit_date, reason = stop, df.index[j], "止损" break if close_j < signals_line_guard(df, j): exit_price, exit_date, reason = close_j, df.index[j], "结构破坏" break if exit_price is None: j = min(idx + hold_max, len(df) - 1) exit_price, exit_date, reason = close.iloc[j], df.index[j], "时间止盈" ret = (exit_price - entry) / entry - fee * 2 trades.append({"entry_date": entry_date, "exit_date": exit_date, "entry": entry, "exit": exit_price, "ret": ret, "reason": reason}) return pd.DataFrame(trades) def signals_line_guard(df, j): # 简化版:跌破 10 日均线视为结构破坏,实际可换成新的倒数线 return df["close"].rolling(10).mean().iloc[j]回测里我特意加了三个离场条件:止损、结构破坏、时间止盈。只用止损和止盈的回测很容易美化结果,因为大多数失败的交易并不是一把亏光,而是拖了很久最后小亏,这种"温水煮青蛙"的亏损不加时间限制根本测不出来。20 根 K 线的时间上限,逼着策略在合理周期内给出结果,实盘执行也更干脆。
5.2 参数敏感性:别只跑一组数
| 参数 | 取值区间 | 对信号数的影响 | 对胜率的影响 | 我的选择 |
|---|---|---|---|---|
| 分形窗口 n | 2 / 3 / 4 / 5 | n 越大信号越少 | n=2 噪声多,n≥5 延迟大 | 3 |
| 滑动上限 max_slide | 0 / 2 / 3 / 5 | 越大信号越多 | 超过 4 明显下滑 | 3 |
| 振幅门槛 min_amp | 1% / 1.5% / 2.5% | 越大信号越少 | 单调改善但样本变少 | 1.5% |
| ATR 止损倍数 | 1.0 / 1.5 / 2.5 | 不影响信号 | 1.0 被打掉多,2.5 单笔亏太多 | 1.5 |
| 有效期 valid_bars | 10 / 15 / 25 | 越大信号越多但陈旧 | 15 附近较稳 | 15 |
这张表是我自己跑出来的,你拿去只能当参考,因为参数一定跟标的、周期、手续费强相关。真正要学的是方法:一次只动一个参数,观察信号数量和胜率的同时变化。如果某个参数一调整,胜率暴涨但信号数掉到个位数,那基本可以判定是过拟合,不能用。
5.3 结果怎么看:三个必看的指标
第一个是盈亏比。倒数线的天然优势是止损位置明确(低点下方),所以盈亏比通常能做得比较好看。我看到的合理区间是 1.8 到 2.5,低于 1.5 说明要么止损放得太远,要么离场太早。
第二个是最大回撤。这个比收益重要得多。倒数线策略的回撤通常来自连续假信号,也就是市场反复在下跌中继里给出"结构成立"的样子。如果回撤超过 25%,要么是过滤条件不够,要么是仓位太重。
第三个是信号的时间分布。把所有信号按年份统计一下,如果某一年的信号特别密集、收益特别好,而其它年份很差,就要小心了——这大概率是某一段特殊行情喂出来的结果,换个市场环境就失效。稳健的策略,信号应该在时间轴上分布得比较均匀。
6. 常见问题与排查实录
6.1 问题速查表
| 现象 | 可能原因 | 排查方法 | 处理 |
|---|---|---|---|
| 回测完全没有信号 | 三个高点顺序写反 | 打印一组样本的 H 值验证 | 检查h2 < h1 < h0 |
| 信号极其密集 | 没做"上穿"判断 | 看同一天是否重复记录 | 加prev_close <= line |
| 回测收益离谱地高 | 未来函数 / 复权错 | 逐笔打印信号日与低点日 | 检查低点确认延迟是否被吃掉 |
| 同一低点被反复买入 | 状态没重置 | 统计信号对应的 low 值 | 触发后清空 active_line |
| 实盘和回测差很远 | 数据源不一致 | 对比同一天的高低价 | 统一复权口径与数据源 |
| 线画在很低的位置 | 用了收盘价取最低点 | 检查低点定位字段 | 必须用 low 而非 close |
| 除权日附近信号异常 | 未复权数据 | 看除权前后价格跳变 | 回测用后复权数据 |
6.2 我踩过的三个坑
第一个坑:未来函数藏得比想象中深。分形法识别低点,需要右侧 3 根 K 线确认。我在早期版本的代码里,直接用了最终确认的低点索引去计算当天的信号,结果相当于"提前 3 天知道那是最低点"。回测曲线漂亮得像作弊,实盘一跑就原形毕露。正确的做法是:第 i 天只能使用第 i 天及之前的信息,低点在 i+3 天才被"发现",信号也只能从 i+3 天之后开始判断。这个错误非常隐蔽,因为逻辑上你确实"用了三根 K 线确认",只是确认的时点搞错了。建议在回测里加一条硬规则:任何信号日期必须晚于其依赖数据的确认日期。
第二个坑:复权口径混用。有一次我把行情接口拉的后复权数据和自己本地存的不复权数据拼在一起,除权日那天价格直接跳空 30%,系统兴奋地识别出一个"深度下跌后的完美倒数线",然后疯狂发信号。排查了两天才发现是数据拼接的问题。现在的做法是:单一标的、单一数据源、单一复权口径,从拉取到回测全程不换,落盘时把复权方式和拉取时间一起存进元数据。
第三个坑:把止损当成万能保险。倒数线的止损放在低点下方,看起来很合理,但在跳空市场里毫无作用。我遇到过开盘直接跳空 6% 的情况,止损单成交在远低于止损价的位置,单笔亏损是预期的三倍。解决办法有两个方向:一是控制单笔仓位,把跳空风险也算进风险预算里;二是对高跳空风险的品种,改用期权或者降低仓位来对冲。任何基于"止损价格"的风险模型,都要在跳空场景下做压力测试,这一步省不得。
6.3 几个实操心得
倒数线最好的用法,我个人的体会是"少而精"。它一年给的信号本来就不多,如果为了增加交易频率去放松条件,最后得到的是一堆噪声。我见过不少朋友把max_slide调到 8、把振幅门槛降到 0.3%,信号确实多了,交易成本也跟着多了,净值曲线反而更差。
另外一条:这条线配合"等待"用,效果最好。倒数线画出来之后,价格不是马上突破,往往会磨几天。这几天很多人拿不住,要么提前进场,要么干脆放弃。我的做法是把它当成一个挂单区——线在那里,价格什么时候收上去我什么时候动手,不预判、不抢跑。这种"等它自己证明"的耐心,比任何参数优化都值钱。
还有个小技巧:如果同一时间多个标的都出现倒数线突破,优先选那个低点结构最规整的——三根 K 线高点抬升均匀、中间没有跳空、振幅适中。这种结构的信号质量明显更高,因为它的价格行为最能反映真实的供需变化,而不是被某笔异常大单砸出来的毛刺。
至于后面还能怎么扩展,我最近在做的一个方向是把倒数线的结构信息量化成特征,比如三根 K 线高点的抬升斜率、突破时的量价比、低点距离均线的幅度,把这些丢进一个简单的分类模型里,筛掉低质量的信号。目前看效果还行,但它只是在做减法,核心逻辑还是那条"从低点数三根、等收盘站上"的线——如果这层基础不扎实,后面堆再多的模型也白搭。