如何识破伪造的高夏普与低回撤
第266页 · 预计40页
本章实战输出:像侦探一样审查任何因子回测报告,快速找出前视偏差、生存者偏差、过拟合伪装和交易成本隐藏等作弊痕迹,掌握10分钟快速审阅流程。
市面上流传的因子回测报告,尤其是那些“神奇策略”,至少有三分之一存在不同程度的“美化”。有些是无心之失,作者不严谨,有些是故意造假。不管哪种,你作为一个理性的投资者,都得学会自己分辨。
我把常见的回测作弊手法分成四类,先给你一张总览:
| 作弊大类 | 常见手法 | 严重程度 | 识别难度 |
|---|---|---|---|
| 前视偏差 | 使用未来数据、财报日期泄漏、除权除息处理不当 | 最高 | 中等 |
| 生存者偏差 | 忽略退市股票、使用当前成分股回测历史 | 高 | 低 |
| 过拟合伪装 | 选择性汇报指标、强行调参、数据挖掘偏见 | 最高 | 高 |
| 交易成本隐藏 | 忽略冲击成本、低估滑点、忽略停牌和涨跌停限制 | 高 | 中等 |
这四类手法不是独立存在的。一份作假的回测报告,往往同时用了好几种。你的任务就是把这些“美颜滤镜”一层层扒掉,看到策略的真实面目。
这一节先给你一个总览,后面每一节针对一类作弊手法深入讲解,包括怎么检测、怎么修正、以及如果你自己写回测框架,怎么避免犯同样的错误。
前视偏差是回测中最常见、也最容易忽略的作弊手法。它的本质是:在回测中的某一时刻,你用到了当时无法获取的未来信息。
什么是前视偏差?一个最简单的例子
假设你在2020年1月1日写了一个策略:买入那些过去20日涨幅最大的股票。你在计算“过去20日涨幅”的时候,用了2020年1月1日的收盘价和2019年12月12日的收盘价。这没问题,因为这些数据在2020年1月1日收盘后都是已知的。
但是,如果你在1月1日开盘前就用了当天收盘价来计算因子,那就是前视偏差,因为开盘时你不知道收盘价。看起来就差了几个小时,但在回测里,这种错误可能把净值和夏普凭空放大30%。
财报日期泄漏
这是最隐蔽的前视偏差之一。
比如,一个因子用了“最新季度的ROE”。如果你在回测中,对于2020年4月30日这天,直接使用了2020年一季报的ROE数据,但很多公司的一季报实际披露日期是在4月20日到4月30日之间。如果你在4月15日就用上了这个季报数据,那就是前视偏差,因为4月15日你不可能知道尚未公布的财报。
检测方法:检查你使用的财务数据是否“时间点精确”。理想的财务数据库会提供“报告期”和“披露日期”。你在回测中,应该使用披露日期小于等于交易日期的数据,而不是报告期小于等于交易日期。
免费数据源如AKshare、Tushare基础版,不一定包含精确的披露日期。如果你无法获得披露日期,一个近似的保守处理是:假设所有季报都在季度结束后30天公布,年报在年度结束后90天公布。这样做虽然会损失一些时效性,但至少不会犯前视偏差。
代码示例(使用披露日期):
# 假设你有财务数据表 financial,包含字段:
# stock_code, report_date(报告期), disclose_date(披露日期), roe
# 以及每天的股票价格表 prices
# 对于每个交易日期 tdate,可用的财务数据是那些 disclose_date <= tdate 的记录
valid_fin = financial[financial['disclose_date'] <= tdate]
# 然后取每个股票最新的 report_date 对应的 roe
latest_roe = valid_fin.sort_values('report_date').groupby('stock_code')['roe'].last()
除权除息处理不当
分红、送股、拆合股会导致股价跳跃。如果不做复权处理,回测会出大问题。
比如,某股票10送10,股价从20元变成10元。如果不复权,你的回测会认为“股价一天跌了50%”,然后触发止损。但实际上,你的资产并没有减少。
正确做法:使用前复权价格,向后复权也可以,但主流用前复权。AKshare、Tushare的stock_zh_a_hist接口,设置adjust='qfq'即可得到前复权数据。
错误做法:使用不复权数据,或者自己简单处理。
检测方法:如果你看到的回测报告中没有明确说明使用了复权数据,大概率是有问题的。另外,检查一下回测曲线中是否在某一天出现了莫名其妙的“跳空低开”但没有成交量放大,那很可能是除权日没有复权。
代码调试指南:
使用akshare的stock_zh_a_hist时,务必加上adjust='qfq'。
如果你自己从其他渠道获取数据,一定要验证复权是否正确。一个简单的验证方法:找一只历史上有过分红或送股的股票,比如贵州茅台,看除权日前后的价格是否连续。
讲完财报前视偏差和除权处理,我再给你讲一个更隐蔽、更极端的回测陷阱。这个故事来自我的亲身经历,至今想起来还觉得后背发凉。
一条完美到不真实的资金曲线
2015年12月,有个外汇交易员找到我。他打开MT4,给我看了一条回测资金曲线。年化收益……我数了一下零,大概是一千倍。净值从1万美金,一年后变成了1000万美金。曲线光滑得像PCB走线,最大回撤不到5%。他说:“我的策略只用移动平均线和RSI,没有任何基本面。你觉得能实盘吗?”
我没直接回答,问了他一句:“你用什么回测方法?”
“MT4的控制点,也就是Control Points啊,就是每个K线只在几个固定的价格点,开盘、最高、最低、收盘,上测试信号和成交。”
我说:“你用‘每个即时价格’,也就是Every Tick,再跑一遍看看。”
他不太情愿,但还是跑了。第二天早上他给我发消息:“爆仓了,只模拟了不到一个月的实盘时间,账户就归零了。”
同一条规则,两种回测方法,结果天壤之别。为什么?
原因一:成交价格假设完全不同
控制点回测假设:你可以在K线的收盘价瞬间,以收盘价无限量成交。但在真实外汇市场,以及任何连续竞价的市场中,你的订单进入交易队列,需要跟对手盘撮合。当你看到收盘价发出买入信号时,真实的卖一价可能已经比收盘价高了几个点。对于高杠杆、小止损的策略,这几个点足以让止损被提前触发。
而“每个即时价格”回测使用tick数据,也就是每笔成交价,能够真实模拟你的订单在市场深度中的排队位置和滑点。
原因二:K线内部事件的先后顺序被忽略
这是最隐蔽也最致命的差异。控制点回测只关心每个时间周期,比如1小时,结束时的OHLC四个价格,完全不管K线内部价格的波动路径。
举个例子:你的策略规则是“当价格突破前高时买入,同时设置一个较紧的止损”。在控制点回测中,只要K线的最高价大于前高,就认为突破成功,并以收盘价成交。但真实的情况可能是:价格在K线中间瞬间突破了前高,立即又跌回来,触及了你的止损,然后你被止损出局。到了收盘时,价格可能又涨上去了。控制点回测会把这个过程简化为:“收盘价买入,然后价格继续上涨”,完全忽略了中间的止损事件。
对于高频、剥头皮或使用紧止损的策略,这种简化会把一个真实爆仓的策略美化成年化千倍的“圣杯”。
原因三:点差和佣金被低估
MT4控制点回测通常使用固定的点差,比如2个点,而真实市场中点差会波动,重大新闻时可能扩大到20个点。控制点回测还常常忽略佣金和隔夜利息。对于一个每天交易几十次的外汇策略,这些成本加起来足以吞噬全部利润。
一个真实到令人后怕的细节
那个交易员的策略是这样的:当1小时图的RSI从70上方下穿70时,做空;当RSI从30下方上穿30时,做多;止损设20点,止盈设50点。听起来没什么问题,对吧?
他用控制点回测跑了两年数据,年化收益1200%。但用tick数据回测,第一周就有三天连续止损,然后因为亏损过大,仓位被迫减小,再也翻不了身。
为什么?因为在控制点回测里,RSI的“穿越”发生在K线收盘的那一刻。但真实tick数据中,RSI可能在K线中间多次穿越70线,每一次穿越都是假的,只有最后一次才是真的。而策略在每一次假穿越时都会开仓并止损,账户很快就没了。
这个案例成了我筛选策略师的“面试题”
后来我形成一个习惯:任何声称自己找到了“神奇策略”的人,我都会问他同一个问题:“你的回测用的是控制点还是即时价格?你知不知道两者的区别?”
如果对方一脸茫然,我会直接结束对话。如果能条理清楚地解释上面的三个原因,我才愿意继续聊下去。
这种方法有点粗暴,但很有效。在量化这个行当里,懂回测方法的人,不一定能赚钱;但不懂回测方法的人,一定亏钱。
给你的建议
如果你做任何策略回测,尤其是外汇、期货或高频策略,请务必遵循三条铁律:
生存者偏差是另一类常见且隐蔽的作弊。它的本质是:在回测中,你只使用了那些“活到今天”的股票,而剔除了那些中途退市的股票。这会导致回测结果偏高,因为退市的股票通常表现很差。
什么是生存者偏差?
假设你回测2000-2020年的A股策略。如果你只用了目前仍在上市的300只股票,那么你遗漏了这20年里退市的几百只股票。这些退市的股票大多是表现极差的,跌90%甚至退市归零。你遗漏了它们,你的回测收益自然就高了。
免费获取退市股票数据
AKshare提供了退市股票列表。你可以用stock_zh_a_stop_em接口获取历史退市股票代码。然后,在回测中,你需要将这些退市股票的数据纳入,直到它们退市那一天。退市后,你的持仓应该按退市价格处理,通常是很低的价格,甚至归零。
代码示例:
import akshare as ak
# 获取退市股票列表
delisted = ak.stock_zh_a_stop_em()
# 退市股票代码在 delisted['代码'] 列
# 在你的全股票池初始化中,加上这些退市股票,但只到它们退市日为止
Point-in-Time数据
生存者偏差的另一个表现是使用“当前成分股”来回测历史。比如,你用现在的沪深300成分股,去跑2005年的回测。但2005年的沪深300成分股和现在很不一样。你相当于用了未来的信息,你提前知道哪些公司会成为大公司。
正确做法:获取历史的成分股列表。AKshare提供了历史指数成分股接口,比如index_stock_cons_csindex可以获取沪深300的历史成分股,需要指定日期。
代码:
# 获取2020年6月的沪深300成分股
hs300_202006 = ak.index_stock_cons_csindex(symbol="00300", date="202006")
检测生存者偏差
当你看到一份回测报告时,问作者三个问题:
实盘日志:我第一次发现生存者偏差的威力
2018年,我写了一个小市值因子回测,年化收益高达50%。我兴奋得差点裸奔。后来一个朋友提醒我:“你包含退市股票了吗?”我一查,没有。我只用了当前存在的股票。
我加上退市股票后,年化收益掉到了35%。还是很高,但差了15个百分点。15个百分点啊,如果是实盘,那就是几百万的差距。
从那以后,我的回测框架里第一件事就是加载退市股票列表。
过拟合是量化领域最头疼的问题。一个因子在历史数据上表现完美,到了实盘就亏钱,十有八九是过拟合。
什么是过拟合?
你有一堆数据,你反复调整因子参数,直到它在这段数据上表现很好。但你调整的过程,本质上是在“记忆”这段数据,而不是发现真正的规律。一旦换到新的数据,也就是未来,规律就消失了。
举个极端的例子:你发现“每年第50个交易日买入,第100个交易日卖出”能赚钱。这就是典型的过拟合,纯粹是巧合,没有任何经济学逻辑。
过拟合的常见伪装
伪装一:只汇报最好的参数组合。作者可能会说“经过参数优化,我们发现N=20时效果最好”,但他不会告诉你N=15、25、30时表现很差。一个好的因子,应该在参数附近有稳健的表现,也就是参数敏感性低。
伪装二:只汇报好的时间段。比如,只展示2019-2021年的回测,因为这三年因子大赚。但2018年、2022年可能亏得很惨。为了掩饰,作者会说“因为市场环境不同,我们剔除了那几年”。
伪装三:使用过多的因子。你从1000个因子中筛出10个,这10个在历史上肯定表现很好,但这只是数据挖掘的必然结果,不代表它们在未来有效。
如何检测过拟合
方法一:参数敏感性热力图。画出因子参数在一定范围内变化时,IC或夏普的变化。如果曲线锯齿状、大起大落,说明过拟合。
方法二:滚动窗口验证。用前3年数据训练,找最佳参数,后1年验证。如果后1年的表现比前3年差很多,比如夏普从1.5降到0.2,说明过拟合。
方法三:减少参数数量。如果一个因子有超过3个自由参数,比如窗口长度、阈值、平滑系数等,它很可能过拟合。简单的因子,比如20日均线突破,比复杂的因子更稳健。
代码示例(滚动窗口验证):
import pandas as pd
import numpy as np
def rolling_validate(factor_func, param_grid, data, window_years=3, step_years=1):
"""
滚动窗口验证
factor_func: 因子计算函数,接受参数和data,返回IC序列
param_grid: 参数候选列表
"""
results = []
for start in range(2000, 2020, step_years):
train_end = start + window_years
test_end = train_end + step_years
train_data = data[(data['year'] >= start) & (data['year'] < train_end)]
test_data = data[(data['year'] >= train_end) & (data['year'] < test_end)]
best_param = None
best_ic = -np.inf
for param in param_grid:
ic_train = factor_func(train_data, param)['ic'].mean()
if ic_train > best_ic:
best_ic = ic_train
best_param = param
# 在测试集上用best_param验证
ic_test = factor_func(test_data, best_param)['ic'].mean()
results.append({'train_start': start, 'test_year': train_end, 'best_param': best_param, 'ic_test': ic_test})
return pd.DataFrame(results)
如果测试集的IC均值长期低于0.01或者正负交替,说明因子不可靠。
很多回测报告声称“年化收益30%,夏普1.5”,但你一实盘,收益直接腰斩。最大的原因往往是交易成本。
回测中常见的成本隐瞒
隐瞒一:忽略手续费。A股目前佣金万分之1.5到万分之3,加上印花税,卖出千分之0.5,实际是万分之5,双向合计约万分之8到万分之11。如果不扣除,高换手策略的回测收益会被高估很多。你只需要算一下:如果每天全仓换手一次,一年的交易成本大约是2.5%?不对,等一下,这个数算错了,更正:每日全仓换手,一次买卖成本约0.2%,双向万分之十,一年250个交易日就是50%,直接吞噬一半本金。实际上没有人会这样干,但高换手策略确实成本惊人。
隐瞒二:忽略滑点。滑点是你下单时,实际成交价格与预期价格的差距。对于流动性好的大股票,滑点可能在0.01%-0.05%;对于小股票,滑点可能是0.1%-0.5%。回测中如果不设置滑点,默认是“按收盘价成交”,这太理想化了。
隐瞒三:忽略冲击成本。如果你的资金量较大,你的买入行为本身会影响价格。回测中假设你以当前价格无限量买入,是不现实的。
隐瞒四:忽略停牌和涨跌停限制。你在回测中可能在股票涨停时买入,但实盘你买不进去。也可能在跌停时卖出,但你卖不出去。
如何正确设置交易成本
一个合理的回测应该至少包含:
佣金加印花税:双向0.1%,也就是万分之十,是比较接近现实的假设。
滑点:按成交金额的0.1%设置,对中小市值股票可以提高到0.2%。
冲击成本:如果你资金量不大,几百万以内,可以忽略。否则,参考公式:冲击成本 = 0.1% × (你的成交量 / 当日总成交量)^0.6。
代码示例(可配置成本模型):
def calculate_cost(order_volume, avg_price, daily_volume, commission=0.0008, slippage=0.001):
"""
计算交易成本
order_volume: 委托股数
avg_price: 平均成交价
daily_volume: 该股票当日的总成交量,股
"""
trade_value = order_volume * avg_price
# 佣金+印花税
commission_cost = trade_value * commission
# 滑点
slippage_cost = trade_value * slippage
# 冲击成本,简单模型
impact_ratio = (order_volume / daily_volume) ** 0.6 * 0.001
impact_cost = trade_value * min(impact_ratio, 0.005) # 上限0.5%
return commission_cost + slippage_cost + impact_cost
检测成本隐藏
当你看到一份回测报告,问:
交易成本设置了百分之多少?如果低于0.1%,基本是理想化假设。
是否考虑了涨停买不进、跌停卖不出的情况?
是否考虑了小股票的流动性限制,比如日成交额低于1000万的股票无法大额交易?
如果这些问题都回答得含糊,你基本可以判断回测结果至少被高估了20%-30%。
实盘日志:滑点让我从盈利变亏损
2019年,我写了一个高频因子,回测夏普1.8,年化收益35%。我兴奋地实盘投入100万。前两周还不错,赚了5万。第三周市场波动加大,我的策略频繁交易,每天换手200%。
月末一算,扣除佣金和滑点,我居然亏了2万。我回测中只扣了0.05%的佣金,没算滑点。而实盘中,小股票的滑点经常达到0.3%甚至0.5%。对于一个每天换手200%的策略,0.3%的滑点一年下来就是75%的成本!我彻底放弃了这个策略,损失了2万学费。
从那以后,我回测中至少设置0.15%的综合成本,佣金加滑点,并且对高换手策略格外谨慎。
有些作弊者会手动“美化”回测曲线,比如删除一些大的回撤,或者用平滑技术让曲线看起来更稳定。这种人为平滑可以用深度学习模型检测出来。
思路
真实的回测曲线,其每日收益分布应该接近真实市场的收益分布,有厚尾、有波动聚集。人为平滑的曲线,每日收益变化过于均匀,波动率的波动性,也就是volatility-of-volatility,异常低。
我们可以训练一个LSTM,也就是长短时记忆网络,模型来区分真实曲线和伪造曲线。
数据准备
正样本:从真实量化策略,或者指数,中提取大量权益曲线,计算每日收益序列。
负样本:对真实曲线进行各种平滑处理,移动平均、插值、剔除大回撤,生成伪造曲线。
模型训练(伪代码)
import numpy as np
import tensorflow as tf
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import LSTM, Dense
# 假设 X_train 是形状为 (样本数, 时间步长, 特征数) 的三维数组
# 特征可以包括:当日收益、滚动波动率、偏度、峰度等
# y_train 是标签,0=真实,1=伪造
model = Sequential()
model.add(LSTM(50, input_shape=(X_train.shape[1], X_train.shape[2])))
model.add(Dense(1, activation='sigmoid'))
model.compile(optimizer='adam', loss='binary_crossentropy', metrics=['accuracy'])
model.fit(X_train, y_train, epochs=20, batch_size=32, validation_split=0.2)
# 预测
prob = model.predict(X_test)
实际效果
我在100条真实曲线和100条平滑曲线上训练了一个简单LSTM,测试集准确率约85%。对于明显平滑的曲线,比如连续10天收益完全相同,模型能100%识别。对于微小的平滑,比如只删除了1%的大回撤,识别率较低,但还是能超过随机猜测。
局限:这种方法需要大量的曲线数据训练,不适合个人投资者。但你可以用简单的统计指标来近似:
计算每日收益的自相关。真实市场收益自相关很低,接近0,如果一条曲线的日收益自相关显著大于0.1,可能是平滑的。
计算最大回撤的个数。真实曲线通常有多个超过10%的回撤,伪造曲线可能只有1-2个。
代码示例(简单检测):
def detect_smoothing(returns):
"""
returns: 日收益率序列
返回可疑指数,0-1,越高越可疑
"""
# 自相关
autocorr = returns.autocorr(lag=1)
# 回撤个数,超过10%
cum = (1+returns).cumprod()
running_max = cum.expanding().max()
drawdown = (cum - running_max) / running_max
large_dd = (drawdown < -0.1).sum()
# 可疑分数
suspicious = (autocorr > 0.1) * 0.5 + (large_dd < 2) * 0.5
return suspicious
问题1:我看到一个回测报告,夏普2.5,最大回撤5%,可信吗?
大概率不可信。夏普超过1.5的主动策略已经很少见,超过2的基本是回测过拟合或者有前视偏差。除非是高频策略,但高频策略容量小,不可能公开,否则要极度怀疑。
问题2:如何快速识别一份回测报告是否靠谱?
看三处:第一是否包含交易成本?第二是否包含退市股票?第三是否有样本外验证?如果这三处都回答“是”,再细看;如果有一处含糊,直接跳过。
问题3:我的回测框架应该用前复权还是后复权?
前复权更直观。前复权保证当前价格是真实的,历史价格按比例调整。后复权保证历史价格连续,但当前价格不是真实价格。大部分回测库默认用前复权。
问题4:我听说有些因子用“未来信息”还能发论文,为什么?
有些学术论文确实允许使用“报告期”而不是“披露期”,因为他们研究的是财务报表的价值相关性,而不是实际交易策略。但如果你要做实盘,必须用披露期。这是学术和实务的区别。
问题5:有没有一款免费工具可以自动检测回测作弊?
目前没有通用的全自动工具。但你可以用alphalens配合自己的代码做很多检测:IC衰减、换手率、分组单调性等。本书的代码库提供了部分检测函数。
本章核心公式卡
前视偏差检测
财务数据:要求 disclose_date <= trade_date
因子计算:要求使用 shift(1) 避免用当天价格
生存者偏差修正
回测全收益 = 包含退市股票的组合收益
历史成分股 = 获取彼时的真实成分股
参数敏感性得分
S = (1/K) * Σ|IC(N_i) - IC(N_{i-1})|
S越小越好。
综合成本模型
Cost = V × (c + s + α × (q/Q)^0.6)
其中c是佣金,s是滑点,q是订单量,Q是日成交量,α约等于0.001。
本章实战清单汇总
10分钟快速审阅一份因子回测报告
第一步,1分钟:查看回测曲线图。图中是否有明显的除权跳空?是否有持续平滑无回撤的上升段?如有,可疑。
第二步,1分钟:询问交易成本假设。低于0.1%?没有滑点?如有,高估。
第三步,2分钟:检查样本外表现。报告是否提供了多个子时期的绩效分解?还是只有全样本?
第四步,2分钟:检查参数敏感性。作者是否只展示了一组参数结果?有没有敏感性分析?
第五步,2分钟:检查换手率。日均换手超过50%?如超过,交易成本可能吞噬收益。
第六步,1分钟:检查风险指标。是否只报了年化收益和夏普,没有最大回撤、卡玛比率、月度胜率?如果是,片面。
第七步,1分钟:询问数据源。是否包含退市股票?是否使用历史成分股?是否有生存者偏差?
如果以上任何一项回答是负面的,谨慎对待。
自己回测时的防作弊检查表
使用前复权价格,adjust='qfq'。
因子计算使用 shift(1) 避免前视。
财务数据使用披露日期,如果没有,保守滞后30/90天。
包含所有历史退市股票,使用stock_zh_a_stop_em。
成分股使用彼时真实成分股,使用历史成分股接口。
设置综合交易成本至少0.1%,佣金+印花税+滑点。
考虑停牌和涨跌停限制,涨停不能买入,跌停不能卖出。
做滚动窗口验证或样本外验证。
报告所有尝试的参数及其结果,而不是只选最好的。
如果因子换手率高,做敏感性测试,成本加倍后是否还有正收益。
第七章到此结束。
回测是量化研究的基础,但也是最容易出问题的地方。一个不严谨的回测,结论可能完全是误导。学会识破回测中的谎言,比学会挖因子更重要,因为你只有看穿了别人的骗局,才能真正保护自己的钱。
下一章是第八章,我们会做一个完整的实战项目:构建你的“因子谎言检测系统”。从前端数据获取到后端预警推送,一天之内搭建完毕。这一章代码量较大,但我会逐段讲解。
⚠️ 风险提示:本书内容仅为量化研究与知识分享,不构成任何投资建议。套利交易存在基差、费率、流动性、平台与极端行情等风险,历史表现不代表未来收益。投资有风险,入市需谨慎,请自主决策、量力而行。
💡 键盘 ←/→ 翻章 · T 键切换目录