简介:一份面向量化交易入门者与金融科技开发者的经典策略源码包,内容以趋势跟踪、均值回归、统计套利、高频交易、机器学习、事件驱动、波动率、多因子选股、套利和另类数据10类策略为主线,逐一说明核心逻辑、常用指标、适用市场与潜在风险,帮助读者快速建立量化策略选型框架。包内文件共5个,大小约146KB,涵盖Python策略脚本、依赖清单、HTML说明文档、回测结果图以及在线运行配置等,文件类型精简且分工明确,既能查看代码逻辑,也可对照图表检验回测效果。其中演示代码以移动平均线交叉趋势跟踪策略为案例,基于backtrader框架完成策略定义、数据加载与回测,并输出可视化结果,展示了从指标计算到资金曲线生成的关键步骤。读者可从源码中直接学到如何组织策略类、设置交易信号、控制仓位与评估绩效,结合依赖清单可快速复现实验环境。目前已有348人学习下载,对于希望实际动手验证量化思路的初学者而言,是一份轻量且完整的参考资源。
1. 十个经典量化策略源码,值不值得花两周去跑通
拿到这类源码包,我的建议是先别急着跑回测,因为十个经典量化策略赚的是不同市场状态下的钱:双均线和海龟吃单边趋势,布林带和 RSI 吃震荡修复,网格吃每一格的差价,配对交易吃价差回归,轮动吃标的之间的强弱差异。把它们放在一起比总收益没有意义,先分对类,再看曲线才有参考价值。
这套代码适合两类人。一类是想把「金叉死叉」从看盘直觉升级成可回测信号的新手,另一类是已经有实盘经验、想拿经典策略做基准对照的熟手。前者跟着代码改参数,后者能直接看出每个策略在什么行情下会失效。我给自己定的验收标准是:十个策略全部跑通,能解释每一根信号的来源,能说出每个参数改大改小的后果。如果只是跑出一条净值图,那只是安装了源码,还没开始用它。
先用一句话立个标准:策略源码的难点从来不在信号那几行,而在成本模型、仓位管理和行情边界。下文从策略分类讲起,逐个拆实现,最后给你几组我自己踩过的坑。
2. 先把策略分好类:四类经典策略各自赚什么钱
这一步不做,后面调参就是玄学。十个策略看着多,按盈利来源划分其实只有四类,分类决定了你该用什么参数心态去面对回测曲线。
2.1 趋势、回归、套利、横截面:四类盈利逻辑与适用边界
我平时给新同学讲源码,先发一张分类表,让每个人把一个策略先对号入座再动手。
| 策略类别 | 代表策略 | 盈利来源 | 最怕的市场状态 |
|---|---|---|---|
| 趋势跟踪 | 双均线、MACD、唐奇安通道(海龟) | 单边行情中的持续涨幅 | 长期窄幅震荡 |
| 均值回归 | 布林带、RSI、网格 | 价格偏离中枢后的修复 | 单边趋势(网格最危险) |
| 统计套利 | 配对交易 | 价差偏离后的回归 | 价差中枢永久漂移 |
| 横截面比较 | 动量、轮动、多因子 | 不同标的之间的强弱差 | 风格快速切换 |
注意看「最怕的市场状态」这一列。趋势和回归在盈利逻辑上是互斥的:趋势策略喜欢大波段,震荡行情里频繁止损;回归策略喜欢震荡,单边行情里不断逆势接飞刀。一个很常见的错误是拿布林带的回测曲线去要求它在单边行情里也赚钱,这是用错了工具。
横截面类策略和前三类不一样,它不预测涨跌,只比较强弱。动量策略买过去 N 日涨得最多的那个资产,轮动策略在每个调仓周期把持仓换成排名最前的 K 个。这类策略在标的池足够多、分化足够大时表现稳,但风格一旦切换,排名靠前的标的可能被快速打回去。
统计套利最接近「市场中性」,同时持有多头和空头,赚价差收敛的钱。源码难度不高,但隐含前提是价差会回归。如果两个标的的基本面关系变了,价差不回归就是这类策略的翻车现场。
2.2 信号、仓位、成本、风控:源码的四层结构
看源码不要从第一行顺序看到最后一行,先找四样东西:信号生成、仓位计算、交易成本、风控条件。大多数经典策略代码里,信号生成只占三分之一,剩下的工作是把信号换算成每一期的持仓,再扣掉手续费和滑点,最后统计净值和最大回撤。
下面这段是我习惯的最小回测骨架,四个函数各管一层。后文所有策略都复用这套骨架,信号函数只负责生成 signal 列,回测逻辑完全隔离。
import pandas as pd import numpy as np def make_signal(df, **params): """策略接口:在 df 里生成 signal 列。 signal=1 表示持有,0 表示空仓,-1 表示做空。 """ raise NotImplementedError def run_backtest(df, fee_rate=0.0003, slippage=0.0005, init_cash=100000): df = df.copy() df['ret'] = df['close'].pct_change() df['turnover'] = df['signal'].diff().abs().fillna(0) df['cost'] = df['turnover'] * (fee_rate + slippage) df['strategy_ret'] = df['signal'].shift(1) * df['ret'] - df['cost'] df['equity'] = (1 + df['strategy_ret'].fillna(0)).cumprod() * init_cash dd = (df['equity'] / df['equity'].cummax() - 1).min() print(f"总收益 {(df['equity'].iloc[-1] / init_cash - 1) * 100:.2f}%") print(f"最大回撤 {dd * 100:.2f}%") return df def add_atr_at(df, n=14): """预计算 ATR(真实波幅),海龟这类需要波动率定仓位的策略要用。""" prev_close = df['close'].shift(1) tr = pd.concat([ df['high'] - df['low'], (df['high'] - prev_close).abs(), (df['low'] - prev_close).abs() ], axis=1).max(axis=1) df['atr'] = tr.rolling(n).mean() return df每个函数的职责说明:
make_signal是留给具体策略的接口,所有策略都生成一列signal,取值限定 1 / 0 / -1,后端回测逻辑完全不用改,十个策略共用一套骨架。run_backtest里的shift(1)是防未来函数的命门:第 t 天产生的信号,只能作用于第 t+1 天的收益。这一行被很多人忽略,后面避坑章节会重点展开。turnover用signal.diff().abs()估算换手率,信号从 0 变 1 视为买入 100% 仓位,成本按换手率线性扣除。add_atr_at里的 TR 取三个值的最大值,对应跳空、涨跌停等极端情形的真实波幅,是海龟类仓位管理的基础数据。
这里立一个原则:任何策略源码,只要收益计算里没有shift(1)或等价的滞后处理,基本可以判定作者要么是新手,要么在测试未来函数能跑出多好看的假曲线。
3. 十个经典策略逐个拆解:核心逻辑与最小 Python 实现
十个策略按顺序是:双均线、MACD、唐奇安通道(海龟)、布林带、RSI、动量、轮动、网格、配对交易、多因子选股。这一章每个策略给一个最小可运行的信号函数加逻辑说明,参数给经典默认值并解释为什么是这个值。以下函数默认已经导入 pandas 和 numpy,数据列名统一用 open / high / low / close / volume。
3.1 双均线和 MACD:两个最容易被新手低估的趋势指标
双均线策略的逻辑一句话讲完:快线上穿慢线做多,下穿做空。它是理解「趋势策略为什么会被反复打脸」的最佳教材,代码也最短。
def dual_ma_signal(df, fast=5, slow=20): df = df.copy() df['ma_fast'] = df['close'].rolling(fast).mean() df['ma_slow'] = df['close'].rolling(slow).mean() df['signal'] = (df['ma_fast'] > df['ma_slow']).astype(int) return df参数说明:fast=5、slow=20 是日线级别的经典组合,对应一周和一个月的交易周期。fast 越短,信号越密,手续费消耗越大;fast 和 slow 越接近,策略越容易被一根大阴线打出假交叉。双均线的核心缺点是震荡市里均线反复缠绕,每次交叉都是一次止损,所以它必须配合仓位管理而不是单靠信号。
MACD 可以看成双均线的高级版,用 EMA 差值和差值均线的交叉生成信号,对滞后性做了一点补偿。金叉死叉的源码实现同样短。
def macd_signal(df, fast=12, slow=26, signal=9): df = df.copy() df['ema_fast'] = df['close'].ewm(span=fast, adjust=False).mean() df['ema_slow'] = df['close'].ewm(span=slow, adjust=False).mean() df['dif'] = df['ema_fast'] - df['ema_slow'] df['dea'] = df['dif'].ewm(span=signal, adjust=False).mean() df['signal'] = (df['dif'] > df['dea']).astype(int) return df12 / 26 / 9 是 MACD 的教科书参数,最初来自证券市场的老经验。注意ewm(adjust=False):这是国内行情软件里 MACD 的标准算法,如果用了默认的adjust=True,算出来的 DIF 和行情软件对不上,第一次对图就会被怀疑代码有误。两个策略可以互相印证:双均线给大方向,MACD 柱状图缩减给出离场预警。但入门阶段我建议先别做这种组合,把单一策略的成本、回撤吃透再谈叠加。
3.2 唐奇安通道与海龟:通道突破与波动率仓位管理
海龟交易法则是最广为人知的趋势跟踪系统,核心是唐奇安通道:突破过去 N 日最高价开多,跌破过去 N 日最低价开空。海龟的价值不在信号,而在仓位管理,后面会单独讲。
def donchian_signal(df, entry_n=20, exit_n=10): df = df.copy() df['upper_entry'] = df['high'].rolling(entry_n).max().shift(1) df['lower_entry'] = df['low'].rolling(entry_n).min().shift(1) df['upper_exit'] = df['high'].rolling(exit_n).max().shift(1) df['lower_exit'] = df['low'].rolling(exit_n).min().shift(1) df['signal'] = 0 df.loc[df['close'] > df['upper_entry'], 'signal'] = 1 df.loc[df['close'] < df['lower_entry'], 'signal'] = -1 df['signal'] = df['signal'].where(df['signal'] != 0).ffill().fillna(0) # 离场规则独立维护:持多单时跌破 exit 低点平仓,持空单时突破 exit 高点平仓 df.loc[(df['signal'] == 1) & (df['close'] < df['lower_exit']), 'signal'] = 0 df.loc[(df['signal'] == -1) & (df['close'] > df['upper_exit']), 'signal'] = 0 return df这里有两个非常容易踩的细节。第一,rolling(n).max()必须加shift(1),否则当天的最高价被用来判断当天突破,而现实中你不可能在最高价成交,这就是标准的未来函数。第二,入场用 entry 通道(20 日)、出场用 exit 通道(10 日),两条通道得分开放,很多网上源码把退出逻辑和入场逻辑混在一起,导致持仓状态错乱。where(...).ffill().fillna(0)的作用是把 0 信号延续成上一个非零状态,同时避免开头一段数据被错误填充成持仓。
海龟真正值钱的部分是 ATR 仓位管理:每笔交易承担的风险固定,仓位随波动率调整。
def turtle_position(df, equity=100000, risk_pct=0.01, atr_n=14, point_value=1.0): df = add_atr_at(df, n=atr_n) df['position_units'] = (equity * risk_pct) / (df['atr'] * point_value) return dfrisk_pct=0.01表示每笔交易最多亏本金的 1%,point_value是每波动一个最小价位对应的盈亏金额,期货和股票取值不同。波动率越大,仓位自动越小。信号决定方向,ATR 决定下多重的手,两者解耦——这就是海龟哲学的核心,信号错了靠仓位保护,信号对了靠趋势放大。
3.3 布林带与 RSI:均值回归的两个标准件
均值回归策略的假设是价格会回到均值附近。布林带盯着收盘价和 20 日均线的距离,RSI 盯着超买超卖区间,两个策略在震荡市里是双胞胎,在趋势市里会一起翻车。
def bollinger_signal(df, n=20, k=2.0): df = df.copy() df['mid'] = df['close'].rolling(n).mean() df['std'] = df['close'].rolling(n).std(ddof=0) df['upper'] = df['mid'] + k * df['std'] df['lower'] = df['mid'] - k * df['std'] df['signal'] = 0 df.loc[df['close'] < df['lower'], 'signal'] = 1 df.loc[df['close'] > df['upper'], 'signal'] = -1 df['signal'] = df['signal'].where(df['signal'] != 0).ffill().fillna(0) return df参数说明:n=20 是标准周期,k=2.0 是两倍标准差,对应约 95% 的正态分布区间。std(ddof=0)特意用了总体标准差,因为行情软件里的布林带就是这么算的;如果用ddof=1算出来的带状更宽,和你的行情软件对不上,很多人回测和看盘观感不一致,根源就在这一个参数。实战版本通常会在价格回到中轨时平仓,代码里加一行df.loc[df['close'] > df['mid'], 'signal'] = 0就行,代价是交易次数变多。
RSI 的源码核心是计算涨跌幅的强弱比值,低于 30 买、高于 70 卖。
def rsi_signal(df, n=14, buy_th=30, sell_th=70): df = df.copy() delta = df['close'].diff() gain = delta.clip(lower=0).rolling(n).mean() loss = (-delta.clip(upper=0)).rolling(n).mean() rs = gain / loss.replace(0, np.nan) df['rsi'] = 100 - 100 / (1 + rs) df['signal'] = 0 df.loc[df['rsi'] < buy_th, 'signal'] = 1 df.loc[df['rsi'] > sell_th, 'signal'] = -1 df['signal'] = df['signal'].where(df['signal'] != 0).ffill().fillna(0) return df参数说明:n=14 是威尔德在原始著作里给出的周期,buy_th=30 和 sell_th=70 是经典超买超卖阈值。loss.replace(0, np.nan)是为了防止连续上涨时 loss=0 导致除零。这里用的是简单滚动均值而不是威尔德平滑,数值上略有差异但逻辑完全一致,入门阶段更容易调试。
均值回归类策略有一个所有源码都不会替你处理的问题:跌破下轨买入后,行情可能继续单边下跌。这类策略的实盘必须有硬止损,止损位一般放在开仓价减去 1.5 到 2 倍 ATR 的位置。源码里没写止损,不代表不需要。
3.4 动量与轮动:把收益率排名直接当信号
动量策略是最简单也最反直觉的策略之一:买入过去 N 日涨得最好的资产,逻辑是「强者恒强」。轮动是动量的横截面版本,在多个资产里每期买入排名前 K 的。绝对动量策略的最小代码:
def momentum_signal(df, lookback=20, hold_days=5): df = df.copy() df['momentum'] = df['close'] / df['close'].shift(lookback) - 1 df['signal'] = (df['momentum'] > 0).astype(int) idx = np.arange(len(df)) df['signal'] = df['signal'].where(idx % hold_days == 0).ffill().fillna(0) return df这个实现有个值得说的细节:where(idx % hold_days == 0)表示只在固定周期调仓,其余时间保持上一期信号不动。好处有两个——减少手续费损耗,避免被动量数据的小波动反复触发。动量策略的高频版本坏就坏在每天调仓,手续费会吃掉大部分超额收益。
轮动策略的视角从单资产变成一张收益率矩阵。假设prices是多个资产的收盘价 DataFrame,列名是各资产的代码。
def rotation_signal(prices, lookback=20, top_k=2, rebalance_days=5): prices = prices.copy() ret = prices.pct_change(lookback) positions = pd.DataFrame(0.0, index=prices.index, columns=prices.columns) rebalance_dates = prices.index[::rebalance_days] for date in rebalance_dates: row = ret.loc[date].dropna() if row.empty: continue picks = row.nlargest(top_k).index positions.loc[date, :] = 0.0 positions.loc[date, picks] = 1.0 / top_k positions = positions.ffill() return positions代码逻辑:每个调仓日先清空全部仓位,再把排名前 K 的资产等权分配,两次调仓之间用ffill()保持仓位不变。nlargest(top_k)选出排名最前的 K 个,1.0 / top_k是等权权重。轮动的关键参数是标的池大小和 top_k:池子越分散策略越稳,但如果资产同质化高,排名靠前的资产反而容易快速反转;rebalance_days 和手续费直接相关,第 4 章会给出敏感性测试方法。
多因子选股是横截面策略的进阶版,把市盈率、动量、波动率等多个维度标准化后加权打分,每期选总分最高的前 N 个。这本质上是把「强弱」的定义从单一因子扩展成多维。
def multi_factor_score(factor_df, weights=None): # factor_df 是各股票的因子值,行是股票,列是因子 weights = weights or {'momentum': 0.4, 'value': 0.3, 'low_vol': 0.3} ranks = factor_df.rank(pct=True) scores = sum(ranks[col] * w for col, w in weights.items()) return scores.sort_values(ascending=False)rank(pct=True)把所有因子归一化到 0 到 1 的百分位,消除市盈率和波动率的量纲差异,这是多因子最基础的预处理。要注意因子方向:市盈率越低越好,排名时要反转,实战里会把 value 因子取倒数或做 1 - rank 处理。权重怎么定?人工经验给权重是初版做法,进阶做法是对因子做 IC 分析后动态调整,那超出十个经典策略的范畴,这里不乱展开。
3.5 网格交易与配对交易:两套特殊行情工具
网格交易在所有策略里最特殊,它没有「信号」概念,而是预先在价格上下方挂一张买卖网:价格每跌一格买一份,每涨一格卖一份,赚震荡市里每一格的差价。它的核心是生成网格价位。
def build_grid(anchor_price, grid_step=0.03, grid_num=10): levels = [anchor_price * (1 + grid_step * i) for i in range(-grid_num, grid_num + 1)] return levels def grid_trade_orders(df, anchor_price, grid_step=0.03, grid_num=10): levels = np.array(build_grid(anchor_price, grid_step, grid_num)) orders = [] prev_price = anchor_price for i, price in enumerate(df['close']): # 向下破一格买入,向上破一格卖出;真实实现必须叠加仓位上限 if price <= prev_price - grid_step * anchor_price: orders.append(('buy', df.index[i], price)) elif price >= prev_price + grid_step * anchor_price: orders.append(('sell', df.index[i], price)) prev_price = price return orders网格最容易被忽略的风险是单边行情:价格一路下跌,网格不断触发买入,资金链断裂就是网格策略的爆仓方式。实盘至少要做三件事:每格买入的仓位上限(防止资金耗尽)、anchor_price 的动态调整(行情中枢漂移后网格要跟着移动)、以及网格失效的退出条件。上面的代码只是示意,真正的网格实现还必须处理同一根 K 线内多次触格的情况。
配对交易是统计套利最经典的入门策略。它找两个长期走势相关的标的,当价差偏离历史均值超过两个标准差时,做空偏高的、做多偏低的,等价差回归后平仓。
def pairs_signal(a, b, window=60, entry_z=2.0): spread = a - b mean = spread.rolling(window).mean() std = spread.rolling(window).std() zscore = (spread - mean) / std df = pd.DataFrame({'zscore': zscore}) df['signal'] = 0 df.loc[df['zscore'] < -entry_z, 'signal'] = 1 df.loc[df['zscore'] > entry_z, 'signal'] = -1 df['signal'] = df['signal'].where(df['signal'] != 0).ffill().fillna(0) return df配对交易和前面所有策略的最大区别是同时持有多空,净值曲线理论上对大盘涨跌不敏感。但价差不回归时它就是最深的坑:两个基本面已经脱钩的标的,价差可能永远回不到均值。所以配对第一步要做协整检验而不是只看相关系数——相关系数高不代表价差会回归。源码里spread = a - b是等权价差,实战中更常见的是用 OLS 求对冲比率 beta,再构造spread = a - beta * b。我给这个策略加一条铁律:实盘设硬止损,价差超过 3 倍历史标准差就认错离场,绝不等它回归。
3.6 十个策略一起跑,该怎么组织代码
最后补充一种组织方式,让你不用为每个策略各写一遍回测。把所有信号函数放进字典统一调度,输出净值曲线直接对比。
strategies = { 'dual_ma': lambda df: dual_ma_signal(df, 5, 20), 'macd': lambda df: macd_signal(df, 12, 26, 9), 'donchian': lambda df: donchian_signal(df, 20, 10), 'bollinger': lambda df: bollinger_signal(df, 20, 2.0), 'rsi': lambda df: rsi_signal(df, 14, 30, 70), 'momentum': lambda df: momentum_signal(df, 20, 5), } # rotation / grid / pairs / multi_factor 入参不同, # 需要包一层 adapter 把各自输出转成 signal 或 positions 再进回测 results = {} for name, func in strategies.items(): data = func(df.copy()) results[name] = run_backtest(data, fee_rate=0.0003)这段代码的价值在于:参数全部集中在构造处,十个策略的回测口径完全一致,手续费和滑点统一设置,结果直接可比。有了这层封装,第 4 章的参数敏感性测试只需要写成两层循环就能跑完,不用复制粘贴十个回测脚本。
4. 回测参数怎么设:成本模型、样本外验证与过拟合识别
跑通信号只是第一步,回测的可信度取决于成本模型和验证方式。这一章把参数和验证讲透。
4.1 手续费、滑点、停牌:回测的可信度从成本模型开始
经典策略源码里的默认成本参数往往是写给人看的,不是写给实盘用的。手续费、滑点、印花税、冲击成本,每一项在高频调仓的策略里都会被放大。网格和轮动这种高换手策略,成本可以直接决定策略从赚钱变亏钱。
国内市场的经验值,我一般建议用下面这组参数基准:
| 成本项 | 保守值 | 激进值 | 说明 |
|---|---|---|---|
| 佣金费率 | 0.0003 | 0.0001 | 双边收取,按成交额算 |
| 滑点 | 0.0005 | 0.0002 | 流动性好的品种更接近下限 |
| 印花税(卖出) | 0.0005 | 0.0005 | 卖出单边,部分品种免收 |
| 冲击成本 | 0.0005 | 0 | 小资金可以忽略 |
把保守值代回run_backtest的fee_rate + slippage,单边成本大约 0.0008 到 0.001。记住一个经验值:如果某个策略改参数后收益提升的幅度小于成本提升的幅度,这个优化就没有意义。
滑点之外,还有一个容易被忽视的问题:涨跌停。信号触发时如果当天涨停或跌停,实际是无法成交的。完善的做法是在回测里加成交过滤,把停牌日和涨跌停日的 signal 置 0,或者把成交价从收盘价改为次日开盘价并按涨跌停过滤。
def apply_limit_filter(df, limit=0.09): df = df.copy() df['limit_up'] = df['close'] >= df['close'].shift(1) * (1 + limit) df['limit_down'] = df['close'] <= df['close'].shift(1) * (1 - limit) df.loc[df['limit_up'] | df['limit_down'], 'signal'] = 0 return dflimit=0.09是接近真实涨跌幅上限的保守值,留一点余量是因为不同品种的涨跌停是 10% 或 20%,取 9% 覆盖四舍五入和异常情形。把过滤加在信号生成之后,回测曲线的可信度会明显上一个台阶。
4.2 参数敏感性:不要被一组最优参数带偏
十个经典策略每个都有三四个参数,网格搜索找最优参数是所有人拿到源码后最想做的事,但这也是过拟合的开始。我跑网格搜索时会重点看一件事:最优参数是孤峰还是高原。
孤峰意味着参数一点小变动收益就大幅下滑,这种参数不可靠;高原意味着参数在很大范围内都能赚钱,这才是值得实盘的区间。下面是网格搜索的骨架。
import itertools def grid_search(signal_func, df, param_grid, fee_rate=0.001): results = [] for combo in itertools.product(*param_grid.values()): params = dict(zip(param_grid.keys(), combo)) data = signal_func(df.copy(), **params) data = apply_limit_filter(data) out = run_backtest(data, fee_rate=fee_rate) max_dd = (out['equity'] / out['equity'].cummax() - 1).min() results.append((out['equity'].iloc[-1], max_dd, params)) return sorted(results, key=lambda x: x[0], reverse=True)输出结果要看两个维度:排名前几的组合之间收益率差距是否很大,以及各自最大回撤是否相近。如果第一名和第二名收益率差 30 个百分点但回撤类似,第一名大概率是碰巧踩中了一段特殊行情。
样本外验证是防止过拟合的底线操作。常见做法是把数据按时间切成三段:训练段、验证段、测试段。在训练段上做网格搜索,在验证段上确认参数有效,最后在从未参与选择的测试段上跑一次最终确认。
train = df.iloc[:int(len(df) * 0.6)] valid = df.iloc[int(len(df) * 0.6): int(len(df) * 0.8)] test = df.iloc[int(len(df) * 0.8):]顺序很关键:训练段选参数,验证段微调,测试段只能跑一次。测试段跑出来不好看,也不要回头改参数,否则测试段就失去了意义。我见过太多人在测试段之后继续回去调参,这套流程一旦被污染,整个回测的可信度就归零了——这属于典型的「后悔药」越吃越后悔。
5. 源码落地避坑:五个真实翻车现场与排查步骤
这一章写的是我自己和身边人在这类源码上踩过的坑,都是能直接复现的现象,按「现象 → 原因 → 解决」写。这些教训用钱买来的居多,能省你一次就省一次。
5.1 回测收益漂亮到像作弊
现象:某个策略回测三年跑出年化 50% 以上,最大回撤不到 5%,净值曲线平滑向上,让人热血沸腾。
原因:十有八九是未来函数。最常见的是rolling(n).max()没有加shift(1),当天的最高价被用来判断当天突破,而现实中你不可能在最高价成交。
解决:把信号函数里所有rolling统计量都加shift(1),再在信号与收益之间加一层signal.shift(1)对齐验证。我有个习惯:回测收益超过同策略行情软件显示结果的好几倍,第一反应就是查未来函数,而不是庆祝。
5.2 回测表现和实盘差了十万八千里
现象:回测里每次信号都完美成交,实盘里要么价格跳空买不到,要么涨停排队排不上。
原因:回测用收盘价成交且没有涨跌停过滤,实盘的成交价格和市场状态跟理想模型完全不一致。
解决:把成交价改成次日开盘价,加上第 4.1 节的apply_limit_filter,再把滑点从 0 提高到保守值。改完这三点之后,回测收益通常会下降 20% 到 40%,这个下降是正常的,不回落的反而要警惕。
5.3 换了数据时间段,收益曲线直接变脸
现象:同一策略、同一参数,在 2019 到 2021 年表现优异,换到 2022 到 2024 年就变成稳定亏损。
原因:策略参数过拟合了前一段行情的特征。震荡市里选中的参数在趋势市里必然失效。
解决:用第 4.2 节的训练、验证、测试三段式流程重新评估,并且别只盯收益率,要看参数敏感性。如果最优参数附近一片深谷,那这个策略在当前行情里就不该用这一段参数。
5.4 网格交易在单边行情里把资金打光
现象:网格策略回测时年化收益很稳定,实盘遇到一波连续下跌,网格不断触发买入,资金在底部区域耗尽,后面反弹也没有仓位可卖了。
原因:网格的盈利模型建立在价格来回震荡的假设上,单边行情下网格只会单向加仓,没有任何止刹机制。
解决:给网格加两个硬约束:最大持仓限额(比如总资金的 60%)和网格失效退出条件(价格跌破最低网格且超过 N 日不回归就全部清仓)。网格不是不能赚钱,但它必须带刹车。
5.5 配对交易价差死活不回归
现象:按历史数据选出的配对标的,价差在开仓后不仅没收敛,反而越拉越大,多头空头两头亏。
原因:两个标的的相关性在某个时间点发生了结构性变化,价差中枢已经漂移,历史均值不再有效。
解决:配对之前先做协整检验而不是只看相关系数;实盘设硬性止损阈值,比如价差超过 3 倍历史标准差就离场。配对交易最忌讳的是「等它回归」四个字,价差可能永远不回来。
6. 让十个策略源码真正变成生产力:三条进阶用法
最后给三条我自己验证过的进阶用法:滚动验证、行情状态过滤、策略组合。
滚动验证比一次性切分数据集更接近实战。做法是固定训练窗口长度,比如 250 个交易日,每次向前滚动 50 天,用训练窗口选参数,只在接下来 50 天里执行,执行完再滚动。这模拟了「每隔一段真实时间就要重新决策」的实盘节奏,参数永远只在已发生的数据上确定过,不会偷看未来。
行情状态过滤是我认为性价比最高的一条。某个趋势策略在震荡市里必然亏损,那就加一层「只在大趋势里开仓」的开关。最简单实现是加一条 200 日均线判断牛熊,价格在长均线上方允许做多,下方禁止开仓。给双均线加状态过滤,信号函数只需要三行改动:
def dual_ma_with_filter(df, fast=5, slow=20, regime_n=200): df = dual_ma_signal(df, fast, slow) df['regime'] = df['close'] > df['close'].rolling(regime_n).mean() df['signal'] = df['signal'] * df['regime'].astype(int) return df这一行相乘把策略和状态过滤接成了串联电路:状态不对,信号直接归零。加了这层过滤后,双均线在震荡市里的频繁交叉被长周期状态压住,回撤通常能显著下降。
策略组合是更进阶的一步。把十个策略分成趋势组和回归组,让它们在同一段行情里互相制衡:趋势组贡献大行情的盈利,回归组贡献震荡市的平滑收益。组合时不要按收益率加权,要按最大回撤加权,目标是最小化组合回撤而不是最大化收益。这一步做完,十个策略源码就不再是十段孤立代码,而是一套能应对不同行情状态的策略库。
最后说一个我自己的习惯:每跑完一个策略,我会在代码文件头部写下三个问题的答案——这个策略赚哪种行情的钱,最怕什么行情,参数在哪几个值附近失效。时间长了你会发现,真正有用的不是那几行信号代码,而是你给每个策略建立的上下文。经典策略的源码不难找,难的是想清楚它在你的资金、你的标的上还能不能成立。希望这几组代码和踩坑记录能帮到你,哪怕只帮你少交一次学费,也值了。
本文还有配套的精品资源,点击获取