☰
10种经典量化策略源码解析:从双均线到多因子的Python实现与避坑指南
2026/10/17 19:42:40 网站建设 项目流程

简介:这份资源面向量化交易初学者与进阶开发者,系统梳理了10种经典策略的核心逻辑与适用场景,涵盖趋势跟踪、均值回归、统计套利、高频交易、机器学习、事件驱动、波动率、多因子选股、套利及另类数据策略,帮助读者建立策略选型的整体认知框架。压缩包共5个文件,约146KB,包含Python策略源码、运行说明文本、网页展示文件及回测结果图,其中源码以移动平均线交叉的趋势跟踪策略为例,借助backtrader框架演示了策略实现与回测流程,便于直接运行验证。目前已有343人学习下载。读者可从中获得各策略的指标方法、市场特点与风险提示,并参考回测与过拟合、交易成本、风险控制等关键注意事项,为后续实盘实践提供可复用的代码基础与思路参考。

1. 10种经典量化策略源码:从双均线到多因子,一套能跑起来的骨架

很多人第一次接触量化,是从网上扒到一段“神一般的主图指标源码”开始的——打开一看,几百行公式,金叉买死叉卖,回测曲线漂亮得不像话。但真拿去跑实盘,滑点一加、手续费一扣,收益直接腰斩。问题不在策略本身,而在于那段代码只给了信号,没给仓位管理、没给风控、没给数据对齐。所谓“10种经典量化策略源码”,真正有价值的不是那10个信号公式,而是它们背后共用的一套工程骨架:数据怎么喂、信号怎么算、仓位怎么定、回撤怎么控。这篇文章面向的是想用Python把量化策略真正跑起来的从业者——不管你是做期货量化策略还是股票短线,这10种策略的源码结构是相通的。我会把双均线、布林带突破、RSI反转、动量轮动、均值回归、海龟交易、网格、多因子选股、配对交易、趋势跟踪这10种经典策略,拆成可复现的代码模块,重点讲清楚每个策略的参数怎么设、坑在哪、什么行情下会翻车。

2. 双均线与布林带:两条最基础的源码骨架怎么搭

2.1 双均线策略的源码结构与参数选择

双均线是所有量化策略的“Hello World”,但恰恰是它最容易让人产生“我行了”的错觉。核心逻辑简单到一句话:短均线上穿长均线做多,下穿做空或平仓。但源码里真正要处理的是三件事:均线类型(SMA还是EMA)、周期参数、以及信号确认机制。

我一般用EMA而不是SMA,因为EMA对近期价格赋予更高权重,信号滞后更小。周期上,经典组合是5/20、10/30、20/60,但这不是拍脑袋定的——你得看标的的波动周期。一个实用的方法是先做参数扫描,看哪个组合在样本内夏普比率最高且参数平原最宽(即邻近参数表现不急剧变差)。

import pandas as pd import numpy as np def dual_ma_strategy(df, short_window=10, long_window=30, ma_type='ema'): """ 双均线策略源码核心逻辑 df: 包含 'close' 列的 DataFrame,索引为日期 short_window: 短均线周期 long_window: 长均线周期 ma_type: 'sma' 或 'ema' """ data = df.copy() if ma_type == 'ema': data['ma_short'] = data['close'].ewm(span=short_window, adjust=False).mean() data['ma_long'] = data['close'].ewm(span=long_window, adjust=False).mean() else: data['ma_short'] = data['close'].rolling(window=short_window).mean() data['ma_long'] = data['close'].rolling(window=long_window).mean() # 信号:短均线在长均线上方为1,下方为0 data['signal'] = np.where(data['ma_short'] > data['ma_long'], 1, 0) # 持仓变化:signal从0变1为开多,从1变0为平仓 data['position'] = data['signal'].diff() return data

这段代码里,ewm(span=short_window, adjust=False)是EMA的标准写法,adjust=False保证递推计算与常见软件一致。signal.diff()捕捉的是信号变化点,而不是持仓状态本身——这是很多人写错的地方,直接用signal当仓位会导致在信号持续期间反复计算收益。参数上,短周期低于5会产生大量噪音信号,长周期超过120则信号稀疏到几乎没有交易机会。对于日线级别的期货量化策略,10/30是一个比较稳的起点。

2.2 布林带突破策略的源码实现与带宽过滤

布林带的核心不是中轨,而是带宽。很多新手写的布林带策略源码只用了上下轨突破,结果在震荡市里被反复打脸。正确的做法是加一层带宽过滤:当带宽(上轨减下轨除以中轨)低于某个阈值时,说明市场波动收缩,此时突破信号更可靠;带宽过高时反而要警惕假突破。

def bollinger_breakout(df, window=20, num_std=2.0, bandwidth_threshold=0.05): """ 布林带突破策略源码 window: 均线窗口 num_std: 标准差倍数 bandwidth_threshold: 带宽过滤阈值,低于此值才考虑突破 """ data = df.copy() data['mid'] = data['close'].rolling(window=window).mean() data['std'] = data['close'].rolling(window=window).std() data['upper'] = data['mid'] + num_std * data['std'] data['lower'] = data['mid'] - num_std * data['std'] # 带宽 = (上轨 - 下轨) / 中轨 data['bandwidth'] = (data['upper'] - data['lower']) / data['mid'] # 突破信号:收盘价突破上轨且带宽低于阈值 data['signal'] = 0 data.loc[(data['close'] > data['upper']) & (data['bandwidth'] < bandwidth_threshold), 'signal'] = 1 data.loc[(data['close'] < data['lower']) & (data['bandwidth'] < bandwidth_threshold), 'signal'] = -1 return data

参数上,window=20是经典值,num_std=2.0对应约95%的置信区间。bandwidth_threshold需要根据标的的历史带宽分布来定——我一般取过去250个交易日带宽的30%分位数。这样做的逻辑是:只在波动收缩后等待突破,避免在已经大幅波动后追高。注意,布林带策略在趋势启动初期表现最好,但在趋势中段会频繁被上下轨“扫损”,所以必须配合止损规则,不能只靠信号反手。

3. RSI反转与动量轮动:震荡市和趋势市的两种活法

3.1 RSI反转策略的源码细节与超卖阈值调整

RSI反转策略的逻辑是:RSI低于30超卖买入,高于70超买卖出。但直接套用这个规则在A股或期货上会亏得很惨,因为强趋势中RSI可以长期钝化在30以下或70以上。源码里必须加入趋势过滤——比如只在价格高于200日均线时才做多RSI超卖信号。

def rsi_reversal(df, rsi_period=14, oversold=30, overbought=70, trend_ma=200): """ RSI反转策略源码,带趋势过滤 """ data = df.copy() delta = data['close'].diff() gain = delta.where(delta > 0, 0.0) loss = -delta.where(delta < 0, 0.0) avg_gain = gain.ewm(alpha=1/rsi_period, adjust=False).mean() avg_loss = loss.ewm(alpha=1/rsi_period, adjust=False).mean() rs = avg_gain / avg_loss data['rsi'] = 100 - (100 / (1 + rs)) data['trend_ma'] = data['close'].rolling(window=trend_ma).mean() data['signal'] = 0 # 只在上升趋势中做多超卖 data.loc[(data['rsi'] < oversold) & (data['close'] > data['trend_ma']), 'signal'] = 1 # 只在下降趋势中做空超买 data.loc[(data['rsi'] > overbought) & (data['close'] < data['trend_ma']), 'signal'] = -1 return data

这里ewm(alpha=1/rsi_period, adjust=False)是Wilder原始RSI的递推算法,和常见的SMA版RSI有细微差别,但更贴近经典定义。超卖阈值不要死守30——对于波动大的品种,可以调到20甚至15;对于波动小的,25可能更合适。判断方法是看RSI在过去一年的分布,取5%分位数作为超卖线。趋势过滤的200日均线是分水岭,价格在均线下方时,RSI超卖信号大概率是下跌中继,不是反转。

3.2 动量轮动策略的源码框架与调仓周期

动量轮动是“买最强的,卖最弱的”,在期货多品种或股票多标的之间轮动。源码核心是计算每个标的的动量得分(比如过去20日收益率),然后持有得分最高的N个。但这里有个大坑:调仓周期太短会导致换手率爆炸,手续费吃掉所有利润;太长则跟不上风格切换。

def momentum_rotation(price_dict, lookback=20, top_n=3, rebalance_days=5): """ 动量轮动策略源码 price_dict: {标的代码: 收盘价Series} lookback: 动量计算回看天数 top_n: 持有标的数量 rebalance_days: 调仓间隔天数 """ # 合并所有标的收盘价 prices = pd.DataFrame(price_dict) # 计算动量得分 momentum = prices.pct_change(periods=lookback) signals = pd.DataFrame(0, index=prices.index, columns=prices.columns) for i in range(lookback, len(prices), rebalance_days): # 每 rebalance_days 调仓一次 scores = momentum.iloc[i].dropna() if len(scores) < top_n: continue top_assets = scores.nlargest(top_n).index signals.iloc[i:i+rebalance_days, :] = 0 signals.loc[signals.index[i:i+rebalance_days], top_assets] = 1 return signals

lookback=20对应约一个月的动量,rebalance_days=5是周度调仓。注意signals.iloc[i:i+rebalance_days, :] = 0这行先把区间清零再赋值,避免前一次调仓的持仓残留。动量轮动在趋势明显的年份表现极好,但在震荡市会反复被打脸——2021年春节后的核心资产回调就是典型翻车场景。所以源码里最好再加一层波动率过滤:当所有标的的动量得分都低于某个阈值时,空仓等待。

4. 均值回归与配对交易:统计套利的源码怎么落地

4.1 均值回归策略的源码实现与布林带变体

均值回归和布林带突破是反过来的:价格偏离均线越远,越可能回归。源码上可以用z-score来标准化偏离程度。

def mean_reversion(df, window=20, entry_z=2.0, exit_z=0.5): """ 均值回归策略源码 entry_z: 开仓z-score阈值 exit_z: 平仓z-score阈值 """ data = df.copy() data['ma'] = data['close'].rolling(window=window).mean() data['std'] = data['close'].rolling(window=window).std() data['zscore'] = (data['close'] - data['ma']) / data['std'] data['signal'] = 0 # z-score低于 -entry_z 做多 data.loc[data['zscore'] < -entry_z, 'signal'] = 1 # z-score高于 entry_z 做空 data.loc[data['zscore'] > entry_z, 'signal'] = -1 # z-score回归到 ±exit_z 以内平仓 data.loc[abs(data['zscore']) < exit_z, 'signal'] = 0 return data

entry_z=2.0意味着价格偏离均值2个标准差,exit_z=0.5是回到0.5个标准差内平仓。这个策略在震荡品种上表现好,比如某些农产品期货。但要注意:z-score的均值和标准差是滚动计算的,当趋势来临时,均线本身会跟着价格移动,导致z-score永远回不到0——这就是均值回归策略在单边行情中爆仓的原因。源码里必须加一个硬止损:当亏损超过账户权益的2%时强制平仓。

4.2 配对交易策略的协整检验与源码步骤

配对交易是均值回归的进阶版:找两个历史走势高度相关的标的,当价差偏离均值时做多一个做空另一个。源码的第一步不是写交易逻辑,而是做协整检验。

import statsmodels.api as sm from statsmodels.tsa.stattools import coint def pair_trading(price_a, price_b, window=60, entry_z=2.0, exit_z=0.5): """ 配对交易策略源码 price_a, price_b: 两个标的的收盘价Series """ # 第一步:协整检验 score, pvalue, _ = coint(price_a, price_b) if pvalue > 0.05: print(f"协整检验不通过,pvalue={pvalue:.4f},不适合配对交易") return None # 第二步:计算对冲比例(用OLS回归) X = sm.add_constant(price_b) model = sm.OLS(price_a, X).fit() hedge_ratio = model.params.iloc[1] # price_b的系数 # 第三步:计算价差和z-score spread = price_a - hedge_ratio * price_b spread_ma = spread.rolling(window=window).mean() spread_std = spread.rolling(window=window).std() zscore = (spread - spread_ma) / spread_std # 第四步:生成信号 signals = pd.DataFrame(index=price_a.index) signals['zscore'] = zscore signals['signal'] = 0 signals.loc[zscore < -entry_z, 'signal'] = 1 # 价差过低,做多价差 signals.loc[zscore > entry_z, 'signal'] = -1 # 价差过高,做空价差 signals.loc[abs(zscore) < exit_z, 'signal'] = 0 return signals, hedge_ratio

协整检验的p值必须小于0.05才能继续,否则两个标的的价差没有稳定的均值。hedge_ratio是对冲比例,决定了做多1手A的同时要做空多少手B。配对交易最大的坑是“协整关系破裂”——比如两个公司原本业务相似,突然一个转型了,价差就再也回不来了。所以源码里要定期重新做协整检验,一般每60个交易日重跑一次。

5. 海龟交易与网格策略:源码里的仓位管理和边界条件

5.1 海龟交易策略的ATR仓位计算源码

海龟交易的核心不是入场信号(唐奇安通道突破),而是仓位管理。源码里必须包含ATR计算和单位仓位调整。

def turtle_strategy(df, entry_period=20, exit_period=10, atr_period=20, risk_pct=0.01): """ 海龟交易策略源码 entry_period: 入场通道周期 exit_period: 出场通道周期 atr_period: ATR计算周期 risk_pct: 每笔交易风险占账户比例 """ data = df.copy() data['high_low'] = data['high'] - data['low'] data['high_close'] = abs(data['high'] - data['close'].shift(1)) data['low_close'] = abs(data['low'] - data['close'].shift(1)) data['tr'] = data[['high_low', 'high_close', 'low_close']].max(axis=1) data['atr'] = data['tr'].rolling(window=atr_period).mean() data['entry_high'] = data['high'].rolling(window=entry_period).max() data['entry_low'] = data['low'].rolling(window=entry_period).min() data['exit_high'] = data['high'].rolling(window=exit_period).max() data['exit_low'] = data['low'].rolling(window=exit_period).min() # 单位仓位 = (账户权益 * risk_pct) / ATR # 这里假设账户权益为100万 account_equity = 1000000 data['unit_size'] = (account_equity * risk_pct) / data['atr'] data['signal'] = 0 data.loc[data['close'] > data['entry_high'].shift(1), 'signal'] = 1 data.loc[data['close'] < data['entry_low'].shift(1), 'signal'] = -1 # 出场:跌破10日低点平多,涨破10日高点平空 data.loc[data['close'] < data['exit_low'].shift(1), 'signal'] = 0 data.loc[data['close'] > data['exit_high'].shift(1), 'signal'] = 0 return data

unit_size是海龟交易的精髓:ATR越大,仓位越小,保证每笔交易的风险敞口恒定。risk_pct=0.01意味着每笔交易最多亏账户的1%。这个策略在趋势流畅的品种上表现极好,但在震荡市会连续小亏。源码里还要加一个“加仓”逻辑:每盈利0.5个ATR加仓一次,最多加3次。不加仓的海龟不是完整的海龟。

5.2 网格策略的源码实现与震荡区间判定

网格策略是在价格区间内等距挂买卖单,适合震荡市。源码的关键是确定网格上下界和网格数量。

def grid_strategy(df, window=60, grid_num=10): """ 网格策略源码 window: 用于确定震荡区间的回看天数 grid_num: 网格数量 """ data = df.copy() data['upper'] = data['high'].rolling(window=window).max() data['lower'] = data['low'].rolling(window=window).min() data['grid_step'] = (data['upper'] - data['lower']) / grid_num # 生成网格线 for i in range(1, grid_num): data[f'grid_{i}'] = data['lower'] + i * data['grid_step'] # 信号:价格每跌破一条网格线,买入一份 data['signal'] = 0 for i in range(1, grid_num): data.loc[data['close'] < data[f'grid_{i}'], 'signal'] += 1 # 价格每涨破一条网格线,卖出一份 for i in range(1, grid_num): data.loc[data['close'] > data[f'grid_{i}'], 'signal'] -= 1 return data

网格策略最大的风险是单边行情——价格跌破下界后继续跌,网格策略会满仓被套。所以源码里必须加一个“破网止损”:当价格跌破下界超过一定幅度(比如2%),清仓并停止网格。window=60是确定震荡区间的回看期,太短会导致区间频繁变化,太长则反应迟钝。

6. 多因子选股与趋势跟踪:从单策略到组合的源码整合

6.1 多因子选股策略的源码框架与因子合成

多因子选股是股票量化的主流,源码核心是因子计算、因子标准化、因子合成、选股。这里给一个简化框架。

def multi_factor_selection(factor_dict, top_n=20): """ 多因子选股策略源码 factor_dict: {因子名: DataFrame(索引为日期,列为股票代码)} top_n: 选股数量 """ # 第一步:因子标准化(z-score) normalized = {} for name, df in factor_dict.items(): normalized[name] = (df - df.mean(axis=1, keepdims=True)) / df.std(axis=1, keepdims=True) # 第二步:因子合成(等权) composite = sum(normalized.values()) / len(normalized) # 第三步:选股 signals = pd.DataFrame(0, index=composite.index, columns=composite.columns) for date in composite.index: scores = composite.loc[date].dropna() if len(scores) < top_n: continue top_stocks = scores.nlargest(top_n).index signals.loc[date, top_stocks] = 1 return signals

因子标准化用横截面z-score,即每个交易日对所有股票做标准化。因子合成用等权,实际中可以用IC加权或机器学习加权。注意:多因子选股必须做行业中性化和市值中性化,否则选出来的全是小市值股票,风格暴露太大。

6.2 趋势跟踪策略的源码实现与多周期过滤

趋势跟踪是“截断亏损,让利润奔跑”。源码上可以用双均线加ATR止损。

def trend_following(df, fast=20, slow=60, atr_period=20, atr_mult=2.0): """ 趋势跟踪策略源码 """ data = df.copy() data['ma_fast'] = data['close'].rolling(window=fast).mean() data['ma_slow'] = data['close'].rolling(window=slow).mean() data['tr'] = data[['high', 'low', 'close']].apply( lambda x: max(x['high']-x['low'], abs(x['high']-x['close']), abs(x['low']-x['close'])), axis=1) data['atr'] = data['tr'].rolling(window=atr_period).mean() data['signal'] = 0 data.loc[data['ma_fast'] > data['ma_slow'], 'signal'] = 1 data.loc[data['ma_fast'] < data['ma_slow'], 'signal'] = -1 # ATR止损:入场价 ± atr_mult * ATR data['entry_price'] = data['close'].where(data['signal'].diff() != 0) data['entry_price'] = data['entry_price'].ffill() data['stop_long'] = data['entry_price'] - atr_mult * data['atr'] data['stop_short'] = data['entry_price'] + atr_mult * data['atr'] # 触发止损则平仓 data.loc[(data['signal'] == 1) & (data['close'] < data['stop_long']), 'signal'] = 0 data.loc[(data['signal'] == -1) & (data['close'] > data['stop_short']), 'signal'] = 0 return data

atr_mult=2.0是止损倍数,越大越能扛波动,但单笔亏损也越大。趋势跟踪在商品期货上表现最好,因为期货趋势持续时间长。源码里还可以加一个“多周期过滤”:只在周线趋势向上时,日线才做多。

7. 避坑与排查:10种策略源码里最容易翻车的5个地方

7.1 未来函数:回测漂亮实盘拉胯的元凶

现象:回测年化收益50%,实盘一跑就亏。原因:源码里用了未来数据,比如用当日收盘价计算信号,却用当日开盘价成交。解决:所有信号必须用shift(1)滞后一天,确保用昨天收盘后知道的信息,今天开盘才交易。

7.2 过拟合:参数扫描扫出来的“最优解”

现象:某个参数组合在历史上表现完美,换一段数据就崩。原因:参数被过度优化,拟合了历史噪音。解决:用样本外测试和滚动窗口验证,参数平原比参数峰值更重要。

7.3 手续费和滑点被忽略

现象:回测收益高,实盘被手续费吃光。原因:源码里没扣交易成本。解决:股票双边千分之三,期货按手数算,滑点至少加一个最小变动价位。

7.4 数据对齐错误

现象:多品种回测时,某些品种的信号用了未来数据。原因:不同品种的交易时间不同,直接合并DataFrame会导致对齐错误。解决:用reindex对齐到统一交易日历,并检查每个品种的信号是否滞后。

7.5 仓位管理缺失

现象:策略信号对,但账户波动巨大。原因:每次满仓进出,没有根据波动率调整仓位。解决:用ATR或波动率倒数分配仓位,确保每笔交易风险恒定。

8. 把10种策略源码串成一个可复用的回测框架

写到这里,10种策略的源码骨架都过了一遍。但真正让它们产生价值的,不是单独跑某一个,而是把它们塞进同一个回测框架里,用同一套数据、同一套成本模型、同一套风控规则去对比。我自己的习惯是:先写一个BaseStrategy类,定义generate_signals和calculate_position两个抽象方法,然后每个策略继承它。这样换策略只需要改信号逻辑,不用动回测引擎。

class BaseStrategy: def __init__(self, data, params): self.data = data self.params = params def generate_signals(self): raise NotImplementedError def calculate_position(self, signals): # 默认等权仓位,子类可覆盖 return signals * (1.0 / signals.abs().sum(axis=1).max()) def backtest(self): signals = self.generate_signals() positions = self.calculate_position(signals) returns = self.data['close'].pct_change().shift(-1) strategy_returns = (positions * returns).sum(axis=1) return strategy_returns

这个框架里,returns.shift(-1)是第二天收益,positions是今天收盘后确定的仓位,两者相乘就是策略收益。注意calculate_position里做了归一化,确保总仓位不超过1。实际用的时候,每个策略子类只需要实现generate_signals,返回一个和data同索引的DataFrame,列是标的,值是目标仓位。

最后一个技巧:不要迷信任何一个策略。我自己的习惯是每周跑一次全部10个策略的回测,看哪些策略近期在回撤、哪些在创新高,然后动态调整权重。量化交易不是找到圣杯,而是管理一组相关性低的策略,让整体曲线平滑。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询