Skip to content

高胜率策略最怕这一问:换成随机进场还行吗?

作 者:老余捞鱼

原创不易,转载请标明出处及原作者。

技术讨论不构成任何操作建议。不荐股、不卖课、不承诺收益。
市场有风险,请合法合规投资。

本文约 3300 字 | 预计阅读 10 分钟,后有完整回测代码,建议先收藏

我想写这篇,是因为这套规则的截图在海外量化圈传了一阵。胜率七成、回撤只有一直持有的一半、曲线是楼梯状。但真正该问的是:把它的进场日换成随机日期,还能不能跑出同样的曲线(回测 21 年,205 笔交易,胜率 67.8%,年化 2.56%)。讽刺的是同样的钱躺着不动,年化收益 6.37%。

我往下分两半讲,前半是规则和它跑出来的数字,后半是三道检验和随机进场。该怎么正确的用这套策略我放在了最后。

先把“数”说清楚

IBS 的算法只有一行,(收盘 - 最低) / (最高 - 最低),量的是收盘价站在当天那根K线的哪个位置。收在最低价,值等于0;收在最高价,值等于1;落在中间,就是0.5上下。

拿一根真实的K线算一遍。2018 年 1 月 30 日,XLP 开 58.08、高 58.58、低 57.94、收 58.09。分子是 58.09 减 57.94,等于 0.15。分母是 58.58 减 57.94,等于 0.64。两下一除,IBS 是 0.234。当天收在半山腰偏下,卖方小胜。

换句大白话,这个数在问一件事,今天这一天,买方和卖方谁笑到了最后。数值低说明卖方赢了,而均值回归的赌注是卖方用力过头了。我一开始也只是把它当温度计用,读数低就说明今天冷,冷过头会回温,没往别处想。

XLP 是标普必需消费板块的 ETF,装的是牙膏、麦片、可乐、香烟。它不会一年涨一倍,也很少因为一条新闻跳空。价格波动大多是噪音,这正是我想拿它试短线回归的原因。噪音多的东西,回归的空间才干净。

四条规则

规则来自一份公开的海外量化笔记。它写得很口语,只说昨天低、今天高一点这种话,具体门槛是我按常识补的,补得对不对后面会交代。

  • 进场,昨日 IBS 低于 0.2,今日 IBS 低于 0.5 且高于昨日,RSI(5) 低于 50
  • 离场,收盘价高于昨日最高价,进场当天不判,同一根K线不重复
  • 仓位,一次只持一笔,只做多,全额进,不加杠杆,不留底仓
  • 风控,不设止损,不设时间止损,最差那一笔亏 7%

三个条件同时成立才算信号,缺一条就不动。前面那根 2018 年 1 月 30 日的K线刚好全中。昨日 IBS 0.078 够低。今日 0.234 还在下半区,而且比昨天高,RSI(5) 是 47.4 没超买。第二天开盘 58.14 买进去,这就是后面要反复提到的那一笔。

落地的口径我按最保守的来。信号在收盘生成,次日开盘按开盘价成交,单边成本 0.03%,其中 0.01% 费用、0.02% 滑点。不少公开回测习惯在信号那根收盘价上直接成交,等于知道了收盘价还能按收盘价买,我没那么做。

翻译成人话,前天收在低位,昨天弹了一点但还在下半区。短期动量没超买就进场,之后第一次收在昨天最高价之上就走。

说实话,我第一反应是这两个门槛定得太窄。0.2 意味着收盘要压到当天振幅最下面两成,一年能出几次。跑完才发现一年九笔多,比我预期密。

那份笔记自己贴的图

那份笔记贴了一张回测图,四栏叠在一起。把它放上来,因为后面要拿它跟我自己跑出来的对。

从上到下四栏:权益、水下、买卖点、单笔盈亏。标题那行写着 XLP – XLP_IBS (next_open),next_open 就是次日开盘成交。手机上点开能看清每栏刻度

第一栏是权益,从 10 万爬到 30 万出头,绿色那一整块是已经离场的盈利单占的面积。第三栏是买卖点,绿三角买、红三角卖,几乎全部贴着价格线,进场离场都在当日区间附近,没有追高。第四栏是每笔盈亏,赢的点在零轴上方密密麻麻,输的点在下方疏一些,最下面两颗红点落在 -7% 附近。

第二栏水下那栏有个细节,刻度写成 0.000000% 和 -5.000000%。这个我看了好几遍,先以为数据坏了,后来才认出是 vectorbt 默认的格式化没改,不是算错。最深处那条尖线掉到 -13% 附近。

图是好看的,但好看不能当证据。它只说明这份笔记是这么跑的,但换个人跑还成立吗?

换成我的口径重跑一遍

以上是那份笔记的说法和它自己的图。下面换成我拿得到的数据、我自己定的成本假设,把同一套规则重跑一遍,看哪些能复现、哪些对不上。

数据取新浪美股日线,XLP 从 2005 年 1 月跑到 2026 年 11 月,5493 根K线。十万本金,每笔全额投入。这段区间横跨了 2008 年金融危机和 2020 年疫情两次急跌,对回归策略算友好。

指标这套规则一直持有
交易笔数2050
单笔平均+0.28%无
胜率67.80%无
年化收益2.56%6.37%
最大回撤-13.54%-34.18%
夏普比率0.3900.506
持仓时间17.06%100%

输了,年化差 3.81 个百分点。十万本金跑到期末 173,494,一直持有是 38.5 万,差出 21 万。

说实话,跑完第一遍我盯着那个 2.56% 看了半天。我一开始以为成本算高了,把费用和滑点全设成零重跑,还是差得远。后来才想明白,问题不在成本。持仓时间只有 17%,剩下 83% 的日历它躺在现金里,一分钱不赚。

两条假设值多少钱,我拿同一套信号对跑了两个版本。改成当日收盘就成交,年化 2.62%,只多 0.06 个百分点。完全不记成本,年化 3.13%,多 0.57 个百分点。结论有点反直觉,这条规则不怕成交假设,怕成本。一年九笔多,每笔多付 0.03%,一年就少半个点。这类小额高频的玩法,成本才是它的天花板。

回撤确实小一半,这是它最拿得出手的一项。但夏普 0.390 对 0.506,连单位风险的收益也没赢。回撤小是因为它大部分时间不在场。不是因为买点更聪明。

同一件事我自己画一遍,对数轴,绿线是策略,灰线是一直持有。形状像,斜率差一截

它的形状

赢 139 笔,输 66 笔,平均盈利 1.12%,平均亏损 -1.48%。赢的次数是输的两倍,但每次只赢一点点。这个形状我在别的回归策略上也见过,它不靠判断准,靠重复足够多次。用次数换掉单次的判断压力。

最差的两笔值得单独看。2018 年 1 月 31 日进,2 月 13 日出,-7.07%。2008 年 9 月 19 日进,9 月 26 日出,-6.97%。这两笔的日期我看了好几遍,后面还要用到。

最大回撤 -13.54% 就是这两笔叠出来的。0.929 乘 0.930 等于 0.864,正好 -13.6%。没有止损的规则,回撤不由单笔决定,由最差的那几笔挨得多近决定。

最好的一笔在 2020 年 4 月 2 日进,四个自然日拿 7.36%,那是疫情底反弹最猛的一段。这类机会十年才几次,指望它撑起年化不现实。

横轴是单笔收益,柱子集中在 0 到 2% 右侧,左边拖出两根细长的尾巴

— · —

以上是规则和它跑出来的结果,对着数字就能看。往下是三道检验,也是这篇真正想写的部分。

先看这九格

多数回测死在同一个问题上,门槛是不是照着历史数据挑出来的。我把两个 IBS 门槛各取三档,跑九格。

门槛组合笔数单笔年化回撤
0.1 / 0.3930.13%0.50%-14.70%
0.1 / 0.51420.28%1.76%-14.70%
0.1 / 0.71750.25%1.87%-20.34%
0.2 / 0.31290.13%0.66%-18.03%
0.2 / 0.52050.28%2.56%-13.54%
0.2 / 0.72560.27%3.00%-20.34%
0.3 / 0.31420.12%0.67%-20.62%
0.3 / 0.52290.26%2.56%-15.20%
0.3 / 0.72910.28%3.57%-21.53%

基准格 0.2 / 0.5 ,0.3 / 0.7 年化 3.57%,笔数也多 86 笔。我原本以为收紧门槛会更稳,跑完发现今日门槛收到 0.3 那一列,单笔只剩 0.12% 到 0.13%,边际基本被削平。我没往最高分那格调,报出来的也不是网格里的最大值。

形状本身有信息。中间一列单笔收益 0.26% 到 0.28%,是最稳的一列。收紧到 0.3,笔数掉到 130 上下。放宽到 0.7,笔数上来了,回撤一路推到 -20%。

看图像平台,不像尖峰。这是好消息,但仍然只是单一标的上的九格。

每格两根柱,左年化、右回撤。中间那列单笔收益最厚,两头都薄

换成随机进场

这是全文最关键的一步。我保留真实的笔数,也保留每一笔的持有天数,然后把这些持仓随机丢到 21.8 年里的任意日期,跑 2000 次。随机进场平均年化 1.08%,95 分位 2.87%,这套规则 2.56%。位置很清楚,它跑在随机结果的九成之上,但没到 95 分位,经验 p 值 0.085。

我一开始不太信这个数,怕是自己的随机方法有问题。把种子换了四个重跑,p 值落在 0.078 到 0.085 之间,结论稳。

没过 5% 这条线。

我原本以为它会稳稳过。同一套规则在另一份从 1999 年起的样本上跑出过 p=0.002,我没打算推翻它。差别在样本,我这份数据从 2005 年开始,1999 到 2004 那五年拿不到。少掉的那五年正好是互联网泡沫破裂之后,急跌接急反弹,回归策略最舒服的环境。说不好把那段补进来会是什么结果。

单笔收益的 bootstrap 结果贴得更近。均值 0.283%,95% 置信区间 0.05% 到 0.51%,下限离零只有五个基点,P(均值小于等于 0) 是 0.009。所以准确的说法是,方向上有一个正的边际,但它薄到换一段样本就可能消失。

2000 次随机进场的年化分布,红线是策略的 2.56%,落在 95 分位左侧一小截

四条规则里哪条在干活

我把进场条件一条条拆掉重跑,想看是哪一条在提供信息。两个发现。

方案笔数单笔年化回撤
四条全用2050.28%2.56%-13.54%
去掉 RSI2630.26%2.95%-14.74%
去掉今日更高2330.29%2.97%-13.54%
去掉今日门槛3100.32%4.35%-20.34%

RSI(5) 低于 50 这条几乎不起作用。去掉它,笔数从 205 涨到 263,单笔从 0.283% 掉到 0.255%,年化反而升到 2.95%。它只是在削信号,没在提质量。

今天比昨天高也一样。去掉之后 233 笔,单笔 0.289%,年化 2.97%,回撤一分没变。这条在笔记里是用「弹了一点」这种口语写的,落到数字上什么也没做。

真正管用的只有 IBS 的两个门槛,而且它们管的是风险预算,不是收益。放开今日门槛,年化升到 4.35%,回撤从 13.54% 翻到 20.34%,持仓时间从 17% 涨到 26%。收紧一点,收益和回撤一起缩。

这笔账得算清楚。四条规则里两条是装饰,剩下两条是一个风险刻度盘。拧松了多赚也多吃回撤,拧紧了都小。它决定的是愿意拿多大波动去换这点收益,不是能不能赚钱。

六年没创新高

这个数出来以后,我改了原本想写的结尾。它 82.94% 的时间空着,权益曲线靠一笔笔小赚往上垒。垒得慢,就意味着离前高近的时候多。我数了一下,最长的一段 1497 个交易日没回到新高。差不多 5.9 快 6 年,91.8% 的交易日都在水下。

年化 2.56% 已经不算高,再叠加五年半不创新高,很难有人在第 300 笔之前不关掉它。这不是心态问题,是设计问题。一个只赚零头的策略,必须配上足够多的现金去干别的事,否则它连被坚持下来的资格都没有。

所以它到底能干什么

回到开头那两个问题。规则有效吗?方向上有效。最差的两笔跟我算出来的年份、幅度完全对得上,2008 年 9 月和 2018 年 1 月各约 -7%。最大回撤也因此一模一样。这说明买卖点的判定没跑偏,差的是样本和口径,不是实现。

能当独立策略用吗?我的回答是不能。年化输 3.81 个百分点,随机进场没过 5% 线,四条规则里两条在划水。

那它能干什么?我更倾向把它当进场的开关,不当独立策略。它回答的问题是今天该不该动手。不回答该买多少、拿多久。这个顺序一旦反了,就变成拿一个 0.28% 的边际去承担一次 -7% 的尾部。

落到一个动作,就一句话。把这 82.94% 的现金时间配给一个能自己长大的东西,比如定投。只在 IBS 低于 0.5 的日子,把当期那笔提前打进去。这样它赚的是能让你买得便宜一点,而不再是跑赢市场!

附:核心代码

指标、信号、回测循环一共四十来行,换标的只改读数据那一步。跑完打印笔数和年化。

# 数据:date 索引,open/high/low/close 四列
gap = df["high"] - df["low"]
gap = gap.replace(0, np.nan)
ibs = (df["close"] - df["low"]) / gap
ibs_p = ibs.shift(1)

dlt = df["close"].diff()
# Wilder 平滑
k = dict(alpha=.2, adjust=False)
k["min_periods"] = 5
up = dlt.clip(lower=0).ewm(**k).mean()
dn = (-dlt.clip(upper=0)).ewm(**k).mean()
rsi5 = 100 - 100 / (1 + up / dn)

lo = (ibs_p < 0.2) & (ibs > ibs_p)
cond = lo & (ibs < 0.5) & (rsi5 < 50)
entry = cond.fillna(False).to_numpy()
prev_high = df["high"].shift(1)
exit_ = (df["close"] > prev_high)
exit_ = exit_.fillna(False).to_numpy()

# 次日开盘成交,成本进出各扣一次
o = df["open"].to_numpy()
c = df["close"].to_numpy()
cost, cash, shares = 0.0003, 100_000.0, 0.0
hold, pend_e, pend_x = -1, False, False
eq, rets = [], []

for i in range(len(df)):
    if pend_x:
        cash = shares * o[i] * (1 - cost)
        rets.append(cash / eq_in - 1)
        shares, hold, pend_x = 0.0, -1, False
    if pend_e:
        eq_in = cash
        shares = cash / (o[i] * (1 + cost))
        cash, hold, pend_e = 0.0, i, False
    eq.append(cash + shares * c[i])
    if hold >= 0 and exit_[i] and i > hold:
        pend_x = True
    elif hold < 0 and entry[i]:
        pend_e = True

d0, d1 = df.index[0], df.index[-1]
yrs = (d1 - d0).days / 365.25
cagr = (eq[-1] / eq[0]) ** (1 / yrs) - 1
print(len(rets), f"{cagr * 100:.2f}%")

跑出来是 205 笔、年化 2.56%、回撤 -13.54%、期末 173,494,跟正文那组数字一致。随机进场和消融的代码在项目目录另外两支脚本里,改的是 entry 的构造方式和持仓日期的来源。


#回测 #均值回归 #IBS #量化策略 #Python #过拟合 #ETF #随机进场检验 #统计学显著性 #量化研究

回测结果不预示未来表现,本文不构成操作建议。

Published inAI&Invest专栏

Be First to Comment

发表回复