重要提示:超过三个也可以开,开完之后客户经理会协助你解决账户。
费率具体以开户成功后客户经理确认为准,大家理性投资哦~
避免未来数据泄露:用 Python 正确回测 SMA 交叉策略
引言
你是否曾经写过一个回测策略,结果好得令人难以置信?年化收益 50%、夏普比率 3.0 以上?先别高兴太早——很可能你的回测存在前瞻偏差(Lookahead Bias)。
前瞻偏差是回测中最常见也最致命的错误之一。简单来说,就是你的策略在做决策时,"偷看"了未来的数据。这会导致回测结果虚高,策略一旦实盘就会惨不忍睹。d本文将结合一个简单移动平均线(SMA)交叉策略的案例,教你如何识别和避免前瞻偏差,让你的回测结果更加真实可靠。
什么是前瞻偏差
前瞻偏差发生在回测过程中使用了"当时不可能知道"的信息。常见的来源包括:
第一,错误的 SMA 计算时机。使用当天的收盘价计算当天的 SMA,然后在当天交易。但实际上,你在收盘前是不可能知道收盘价的。
第二,信号和交易不对齐。在同一根 K 线上生成信号并执行交易。正确的做法是信号只能用于下一根 K 线的交易。
第三,库函数的数据泄露。Pandas 的 rolling().mean() 默认会使用当前数据点,如果不做 shift 处理就会引入偏差。
第四,用未来收益给信号打标签。这在机器学习策略中尤其常见。
有偏差 vs 无偏差:一个对比案例
让我们用 AAPL 股票的 SMA(5, 10) 交叉策略来演示。
有偏差的版本(错误示范)
import pandas as pdimport numpy as np# 计算短期和长期 SMAdf['SMA_short'] = df['close'].rolling(5).mean()df['SMA_long'] = df['close'].rolling(10).mean()# 错误:信号使用了当天的数据df['Signal'] = np.where(df['SMA_short'] > df['SMA_long'], 1, -1)# 错误:在同一天执行交易df['Strategy_Return0'] = df['Signal'] * df['close'].pct_change()df['Cumulative_Return0'] = np.exp(np.log1p(df['Strategy_Return0']).cumsum())
这个版本的问题在于:策略在第 t 天"知道"了第 t 天的收盘价,然后决定在第 t 天买卖。这在真实交易中是不可能的。
无偏差的版本(正确做法)
import pandas as pdimport numpy as np# 计算短期和长期 SMAdf['SMA_short'] = df['close'].rolling(5).mean()df['SMA_long'] = df['close'].rolling(10).mean()# 正确:生成信号df['Signal1'] = np.where(df['SMA_short'] > df['SMA_long'], 1, -1)# 关键:将信号延迟一天,避免使用未来数据df['Signal1'] = df['Signal1'].shift(1)# 使用延迟后的信号计算收益df['Strategy_Return1'] = df['Signal1'] * df['close'].pct_change()df['CumulativeReturn1'] = np.exp(np.log1p(df['Strategy_Return1']).cumsum())
通过 shift(1) 操作,我们确保交易信号只基于昨天的数据,交易在第二天执行。这样就避免了使用未来信息。
对比这两个版本的累计收益曲线,你会发现无偏差版本的收益明显更低,但这才是真实的策略表现。
完整的无偏差回测框架
下面是一个更完整的 SMA(10, 50) 交叉策略回测示例,包含滑点和手续费:
import pandas as pdimport numpy as np# 初始设置INITIAL_CAPITAL = 100000.0 # 初始资金 10 万美元POSITION_SIZE = 1.0 # 仓位比例,1.0 表示全仓SLIPPAGE_PCT = 0.0005 # 滑点 0.05%COMMISSION_PER_TRADE = 1.0 # 每笔交易手续费SHORT = 10 # 短期 SMA 周期LONG = 50 # 长期 SMA 周期# 计算 SMA 指标(只使用历史数据)df['SMA_short'] = df['close'].rolling(SHORT, min_periods=1).mean()df['SMA_long'] = df['close'].rolling(LONG, min_periods=1).mean()# 生成交易信号:短期 SMA 上穿长期 SMA 时做多df['signal_at_close'] = (df['SMA_short'] > df['SMA_long']).astype(int)# 初始化变量capital = INITIAL_CAPITALposition = 0.0 # 当前持有的股票数量cash = capital # 现金equity_curve = [] # 权益曲线trade_log = [] # 交易记录dates = df.index.to_list()n = len(df)# 逐行模拟,在下一根 K 线开盘价执行交易for i in range(n - 1): date = dates[i] next_date = dates[i + 1] # 获取当天收盘时的信号(只使用截至当天的数据) sig = df.at[date, 'signal_at_close'] # 根据信号确定目标持仓 desired_position_shares = 0 if sig == 1: # 做多信号:计算能买多少股 buy_price = df.at[next_date, 'open'] desired_position_shares = (capital * POSITION_SIZE) // (buy_price * (1 + SLIPPAGE_PCT)) # 如果需要调整仓位 if desired_position_shares != position: # 考虑滑点计算执行价格 exec_price = df.at[next_date, 'open'] * (1 + SLIPPAGE_PCT if desired_position_shares > position else 1 - SLIPPAGE_PCT) shares_diff = desired_position_shares - position if shares_diff > 0: # 买入 cost = exec_price * shares_diff + COMMISSION_PER_TRADE if cost <= cash: cash -= cost position += shares_diff trade_log.append({ 'date': next_date, 'side': 'BUY', 'shares': shares_diff, 'price': exec_price }) elif shares_diff < 0: # 卖出 sell_shares = -shares_diff proceeds = exec_price * sell_shares - COMMISSION_PER_TRADE cash += proceeds position -= sell_shares trade_log.append({ 'date': next_date, 'side': 'SELL', 'shares': sell_shares, 'price': exec_price }) # 计算当日权益(现金 + 持仓市值) equity = cash + position * df.at[next_date, 'close'] equity_curve.append({'date': next_date, 'equity': equity})# 转换为 DataFrame 并计算绩效ec = pd.DataFrame(equity_curve).set_index('date')ec['pct_return'] = ec['equity'].pct_change().fillna(0)# 输出绩效指标total_return = ec['equity'].iloc[-1] / INITIAL_CAPITAL - 1ann_vol = ec['pct_return'].std() * np.sqrt(252)sharpe = (ec['pct_return'].mean() * 252) / ann_volprint(f"总收益率: {total_return:.2%}")print(f"年化波动率: {ann_vol:.2%}")print(f"夏普比率: {sharpe:.2f}")print(f"交易次数: {len(trade_log)}")
这个框架的关键点在于:信号在第 t 天收盘时生成,但交易在第 t+1 天开盘时执行,完全避免了前瞻偏差。
使用 vectorbt 简化回测
如果你觉得手写模拟器太麻烦,可以使用 vectorbt 库。它内置了防止前瞻偏差的机制:
import vectorbt as vbt# 下载数据price = vbt.YFData.download('AAPL', start='2024-01-01', end='2025-10-11').get('Close')# 计算 SMA 指标fast_sma = vbt.MA.run(price, window=10)slow_sma = vbt.MA.run(price, window=60)# 生成交易信号:金叉买入,死叉卖出entries = fast_sma.ma.vbt.crossed_above(slow_sma.ma)exits = fast_sma.ma.vbt.crossed_below(slow_sma.ma)# 回测pf = vbt.Portfolio.from_signals( price, entries, exits, fees=0.001, # 手续费 0.1% slippage=0.001, # 滑点 0.1% init_cash=10000, # 初始资金 1 万美元)# 查看绩效print(pf.stats())# 绘制权益曲线pf.plot().show()
vectorbt 的优势在于:指标计算完全向量化且只使用历史数据;交叉信号自动在下一根 K 线执行;没有显式循环,减少了人为错误的可能。
如何判断你的回测是否有偏差
有一些明显的信号表明你的回测可能存在前瞻偏差:
第一,收益曲线过于平滑。真实的策略收益应该有波动,如果你的权益曲线像一条直线向上,那很可能有问题。
第二,夏普比率高得离谱。一般来说,夏普比率超过 2.0 就需要仔细检查了,超过 3.0 几乎可以肯定有偏差。
第三,回撤极小。真实策略不可能完美避开所有下跌。
总结
避免前瞻偏差的核心原则可以归纳为以下几点:
第一,使用滞后信号。计算移动平均线时只使用截至前一根 K 线的数据,不包括当前收盘价。
第二,在下一根 K 线执行交易。信号生成后,在下一根 K 线的开盘价执行买卖操作。
第三,检查滚动窗口设置。确保 rolling 函数不使用 center=True 参数,也不要对指标做 shift(-1) 操作。
第四,优先使用成熟的回测库。vectorbt 等库的内置指标和信号管道已经处理好了时间对齐问题。
记住:如果回测结果好得令人难以置信,那它很可能就是假的。真实可靠的回测才是构建盈利策略的基础。
参考文章
- 1. Backtesting without Lookahead Bias — 1. SMA Crossovers:https://wire.insiderfinance.io/backtesting-without-lookahead-bias-1-sma-crossovers-9147b8ec82ff
财经数据与量化投研知识社区
2026年全面升级已落地!【数据科学实战】知识星球核心权益如下:
- 1. 双典系统赋能:获赠《财经数据宝典》与《量化投研宝典》完整文档,凝练多年实战经验,构建系统化知识框架;
- 2. 量化因子日更教程(2026重磅新增):每日更新「量化因子专题教程」,配套完整可运行代码与实战案例,深度拆解因子构建、回测与优化全流程;
- 3. 量化文章专题教程库:300+篇星球独有高质量教程式文章,系统覆盖策略开发、因子研究、风险管理等核心领域,内容基本每日更新,并配套精选学习资料与实战参考;
- 4. PyBroker实战课程:赠送《PyBroker-入门及实战》视频课程,手把手教学,快速掌握量化策略开发技能;
- 5. 财经数据支持:定期更新国内外财经数据,为策略研发提供精准、可靠的数据基础;
- 6. 顶尖学者与行业专家分享:年度邀请学术界博士与业界资深专家开展前沿论文精讲与实战案例分享,不少于4场,直击研究前沿与产业实践;专家直连答疑:与核心开发者及领域专家实时互动,高效解决投研实战难题;
- 7. 专业社群与专属福利:加入高质量交流社群,获取课程折扣及更多独家资源。
星球已沉淀丰富内容生态——涵盖量化文章专题教程库、因子日更系列、高频数据集、PyBroker实战课程、专家深度分享与实时答疑服务。无论您是初探量化的学习者,还是深耕领域的从业者,这里都是助您少走弯路、高效成长的理想平台。诚邀加入,共探数据驱动的投资未来!