跳到正文

回测撒谎的五种方式

五种陷阱让漂亮的回测变成亏钱的策略。绕开这些,你能比 90% 的散户量化活得久。

第 2 篇 · 共 8 篇 约 6 分钟

回测撒谎的路子就那么几种。下面按出现频率排个序。

要是你给别人看过一个回测:收益 300%、夏普 2.5,对方礼貌点头,然后你看着实盘跑了半年稳稳亏钱——这篇文章就是给你写的。回测和现实之间的差距,几乎从来不是随机性。几乎都是这五种偏差中的某一个,安安静静地躺在代码里,把你的数字往好看里带。

大部分散户量化一辈子都没搞清楚是哪一个杀了他们。他们就是不做交易了。

陷阱 1:用到未来

这是最常见的谎,也是最难发现的,因为代码”看起来”是对的。

用到未来,意思是在做决策那一刻用了当时根本拿不到的信息。最常见的形式:用今天的收盘来决定今天的交易。

# 错误:偷看了今天的收盘
signal = (prices > prices.shift(1)).astype(int)
returns = prices.pct_change() * signal

# 正确:做决策那一刻只能拿到昨天的收盘
signal = (prices.shift(1) > prices.shift(2)).astype(int)
returns = prices.pct_change() * signal

提示词

用 pandas 演示 lookahead bias:对比用今天 vs 昨天的错误信号,和用昨天 vs 前天的正确信号。

第一段看的是 prices > prices.shift(1)——今天比昨天涨吗?涨就今天买。但你收盘之前根本不知道今天收在哪儿,你只能明天再动手。第一段等于给策略发了一台时光机。

shift 看起来像改一个字符的事儿,但它把整个回测的含义改了。夏普会掉,资金曲线会滞后,策略甚至可能从赚钱变成亏钱。

隐蔽的形式。 复权因子倒着算(于是你的数据说 AAPL 在 2014 年是 700 美元,而它实际上当时才 100 美元,还没拆股)。盈利数据用了修订后的版本。指数成份股变更——拿今天的 SP500 名单去回测 2005 年。每一次”我下载的数据看起来没问题”都值得怀疑,除非你跟一个已知基准对过。

检测方法:信号里每个点都问自己一句”做决策那一刻我能知道这个吗?“答案需要时光机,那就有 bug。

陷阱 2:幸存者偏差

你用当下的标普 500 成份股测试动量策略。AAPL、MSFT、NVDA——全在里面,全都涨了巨多。回测一片大好。

坑在这儿:2000 年的标普 500 里还有 Lucent、WorldCom、Enron、Tyco、Pets.com。这些票已经没了。它们是从指数里被剔掉的,通常是在跌了 80%+ 之后剔的。如果你的回测里没有这些破产案例,你就是在幸存者名单上做测试。

偏差就在这儿:你算收益的时候,相当于假设你只持有活下来的那些票。真实情况是你也会持有那些死掉的票,然后亏钱。

# 错误:在当下的 SP500 名单上测试
current_sp500 = yf.download(tickers, start="2000-01-01")
returns = current_sp500.pct_change().mean(axis=1)  # 幸存者偏差

# 正确:用 point-in-time 指数
# (Arithmion 的 apexquant.data.load_index_history 干这事儿)
pivot_data = load_index_history("SP500", start="2000-01-01")

提示词

用 yfinance 和 Arithmion SDK 演示幸存者偏差:对比当下 SP500 名单回测 vs 用 point-in-time 历史成份股回测。

第二段给你的是每个时点真实的成份股。死掉的票在里面,那些暴跌也在里面。你的回测会难看很多——但老实很多。

影响有多大?研究给的数字是美股大盘股每年 1-3%,小盘股和国际股更高。差出来的就是夏普 0.8 和夏普 0.4 的距离。是真金白银。

陷阱 3:忘了交易成本

这条你听过了。它照样年年杀策略。

错的假设:交易免费。天真的假设:交易成本 1 个 bp。现实的假设:美股流动性好的票,散户双边每条腿 2-5 个 bp,机构用聪明执行算法 1-2 个 bp。小盘股、期货、期权,或者任何不是中午的 SPY 的东西,成本都更高。

# 错误:没有成本
returns = signal * prices.pct_change()

# 正确:每次仓位变化都扣成本
cost_per_leg = 0.00025  # 单边 2.5 个 bp;双边 = 5 个 bp,散户现实水平
turnover = signal.diff().abs().fillna(0)
returns = signal * prices.pct_change() - turnover * cost_per_leg

提示词

用 pandas 演示忽略交易成本的陷阱:对比零成本的错误版本 vs 扣换手率和双边成本的正确版本。

就那行 turnover * cost_per_leg,在高换手策略上就是夏普 1.2 和夏普 0.4 的差距。要是策略天天交易,这成本复利起来相当凶;要是季度才交易一次,那就当噪声。

坑就坑在”5 个 bp 听起来很小”。单笔确实小。每年 200 笔交易,复利 10 年下来,就是从年化 8% 变成 3%。你那点策略优势整个没了。

经验法则: 要是你的回测里没有显式的成本模型,那不叫回测,那叫策略收益的天花板。

陷阱 4:用参数搜索过拟合

试 100 个不同的均线长度。挑出夏普最高的那一个。报那个夏普。把它叫做你的策略。

这不是策略。这是搜索结果。

偏差就在这儿:100 组随机参数里表现最好的那一组,当然好看。哪怕是随机数据,也能凑出几组看起来不错的参数。你要是搜得够久,你能找到一个”策略”完美描述历史价格走势,预测能力为零。

检测方法:把信号换成随机数生成器,跑同样的参数搜索,看能不能搜出同样好的”策略”?

import numpy as np

# 用真信号跑参数搜索
best_sharpe_real = max(sharpe for params in grid if signal_real(params))

# 用随机收益跑同样的搜索
random_returns = np.random.normal(0, 0.01, len(prices))
best_sharpe_random = max(sharpe for params in grid if signal_random(params))

# 如果 best_sharpe_random 接近 best_sharpe_real,
# 那不是你信号在起作用,是你的搜索在起作用。

提示词

用 numpy 演示参数搜索过拟合:同一参数网格上对比真信号和随机收益的最优夏普。

要是随机信号经过 100 组参数搜索能搞出夏普 0.8,那你的”真”信号搞出夏普 0.85 就是噪声里冒出来的。参数选择毫无意义。

修复办法很残忍:少参数、长样本外、前推验证。这个后面专门讲。现在就一条规矩:任何你没法用一句话讲明白的策略,多半是过拟合。

陷阱 5:在不存在的库存上做空

你的回测假设你可以随时做空任何股票、任何规模、零摩擦。

现实:要融券才能做空。有些票”难借”——小盘股、最近出过利空的票、高做空比例的票。你要付借券费,年化 1%-50% 不等,有时候更多。有些票你根本借不到。

你的回测觉得它在一个破产小盘股上做空赚了 8%。现实里你做空不了。或者能,但 40% 的年化借券成本把利润全吃掉了。

纯做多策略和大盘流动性票这块问题小。其他场景,这就是一个被忽略的隐藏假设,得显式建模。

# 错误:随便做空
returns = signal * prices.pct_change()  # signal 可以是 -1 代表做空

# 正确:把做空的借券成本建模进去
borrow_rate = get_borrow_rate(ticker)  # Arithmion SDK 函数
long_pnl = signal.clip(lower=0) * prices.pct_change()
short_pnl = signal.clip(upper=0) * prices.pct_change()
borrow_cost = -signal.clip(upper=0) * borrow_rate / 252
returns = long_pnl + short_pnl - borrow_cost

提示词

用 pandas 演示做空借券成本:clip 把信号拆成多头和空头,分别算 P&L,再扣每日借券费。

大部分散户量化压根不管这个。要么做多,要么就只交易 SPY。你要是做小盘股或者想做空,这块绕不开。

陷阱 6(白送的):行情切换

你拿 2010-2019 做回测。你的均值回归策略夏普 1.8。然后 2022 年你上线——通胀、加息、十年里均值回归最惨的一年。你的夏普变成 -0.4。咋回事?

行情切换了。在一个环境里 work 的策略,换个环境就不 work 了。2010 年代的标签是央行放水、低利率、低波动、趋势持续。2020 年代这个剧本反复被打破。一个在 2010-2019 数据上优化出来的策略,没有理由在 2022-2024 还能 work。

这事儿没法靠模型解决,靠的是谦卑。依赖特定行情的策略需要行情检测器,而大部分检测器滞后到让它们形同虚设。老实的做法是按”策略可能连续好几年都跑得差”来分配仓位,扛得住这段回撤。

老实的教训

要是你 30 分钟跑出来的回测看起来很美,那不是回测,那只是跑了段代码。

一个真正的回测是多天的工作:数据质量校验、样本外切分、参数稳健性、交易成本敏感性、行情分析、模拟盘。30 分钟的版本筛出来的就是那些最后用真金白银上线然后亏光的人。

上面这五个坑解释了大部分散户量化的失败。偷看未来和幸存者偏差是闷声杀手。忘了成本和过拟合是慢性杀手。做空假设和行情切换是意外。

你要是能写出一个回测把这六个都扛下来,你就领先 90% 的同行了。剩下那 10% 输给了随机性、券商故障、以及他们自己没本事在策略亏钱的时候扛到均值回归。

参考资料

  • López de Prado, Marcos. Advances in Financial Machine Learning. Wiley, 2018. Chapters 11-12. ISBN 978-1119482086.
  • Aronson, David. Evidence-Based Technical Analysis: Applying the Scientific Method and Statistical Inference to Trading Signals. Wiley, 2006. Chapters 4-5. ISBN 978-0470008744.
  • Harvey, Campbell R., and Yan Liu. “Backtesting.” Journal of Portfolio Management 42, no. 5 (2016): 13-28.