量化策略:双均线交叉策略的Python实现

双均线交叉是最经典的趋势跟踪策略之一,原理简单但能有效捕捉中长期趋势。本文用 Python 从零实现一个 MA5/MA20 的金叉死叉策略,包括信号生成、回测和收益可视化。

策略原理

双均线策略使用两条不同周期的移动平均线:

  • 短期均线(MA5):反映近期价格趋势
  • 长期均线(MA20):反映中期价格趋势

交易信号:

  • 金叉(买入信号):短期均线从下方穿越长期均线,表明短期趋势转强
  • 死叉(卖出信号):短期均线从上方穿越长期均线,表明短期趋势转弱

这本质上是一个动量策略——当近期涨势超过中期均值时入场,反之离场。

数据准备

使用 tushare 获取 A 股数据(也可以换成其他数据源):

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt

# 生成模拟数据(实际使用时替换为真实数据)
np.random.seed(42)
dates = pd.date_range('2020-01-01', '2021-12-01', freq='B')
price = 100 * np.exp(np.cumsum(np.random.randn(len(dates)) * 0.02))
df = pd.DataFrame({'date': dates, 'close': price})
df.set_index('date', inplace=True)

计算均线和生成信号

def compute_signals(df, short_window=5, long_window=20):
    """计算均线并生成交易信号"""
    data = df.copy()
    data['ma_short'] = data['close'].rolling(window=short_window).mean()
    data['ma_long'] = data['close'].rolling(window=long_window).mean()

    # signal: 1 表示持仓,0 表示空仓
    data['signal'] = 0
    data.loc[data['ma_short'] > data['ma_long'], 'signal'] = 1

    # position 变化点就是交易点
    data['position'] = data['signal'].diff()
    # position == 1  -> 金叉买入
    # position == -1 -> 死叉卖出

    return data.dropna()

data = compute_signals(df)

回测引擎

一个简化的回测框架,计算策略收益和基准收益:

def backtest(data, initial_capital=100000, commission=0.001):
    """回测计算"""
    result = data.copy()

    # 每日收益率
    result['daily_return'] = result['close'].pct_change()

    # 策略收益:前一天的信号决定今天是否持仓
    result['strategy_return'] = result['signal'].shift(1) * result['daily_return']

    # 扣除交易成本
    result['trade'] = result['position'].abs()
    result['strategy_return'] -= result['trade'] * commission

    # 累计收益
    result['cumulative_market'] = (1 + result['daily_return']).cumprod()
    result['cumulative_strategy'] = (1 + result['strategy_return']).cumprod()

    # 资金曲线
    result['portfolio_value'] = initial_capital * result['cumulative_strategy']

    return result

result = backtest(data)

收益分析

def analyze_performance(result):
    """计算关键指标"""
    strategy_returns = result['strategy_return'].dropna()

    total_return = result['cumulative_strategy'].iloc[-1] - 1
    annual_return = (1 + total_return) ** (252 / len(result)) - 1
    sharpe_ratio = strategy_returns.mean() / strategy_returns.std() * np.sqrt(252)

    # 最大回撤
    cummax = result['cumulative_strategy'].cummax()
    drawdown = (result['cumulative_strategy'] - cummax) / cummax
    max_drawdown = drawdown.min()

    # 交易次数
    trades = (result['position'].abs() > 0).sum()

    print(f"总收益率:   {total_return:.2%}")
    print(f"年化收益率: {annual_return:.2%}")
    print(f"Sharpe比率: {sharpe_ratio:.2f}")
    print(f"最大回撤:   {max_drawdown:.2%}")
    print(f"交易次数:   {trades}")

    return {
        'total_return': total_return,
        'annual_return': annual_return,
        'sharpe_ratio': sharpe_ratio,
        'max_drawdown': max_drawdown,
        'trades': trades,
    }

stats = analyze_performance(result)

可视化

def plot_strategy(result):
    """绘制策略图表"""
    fig, axes = plt.subplots(3, 1, figsize=(14, 10), sharex=True)

    # 价格和均线
    ax1 = axes[0]
    ax1.plot(result.index, result['close'], label='Price', alpha=0.7)
    ax1.plot(result.index, result['ma_short'], label='MA5', linewidth=1)
    ax1.plot(result.index, result['ma_long'], label='MA20', linewidth=1)

    # 标记买卖点
    buy_signals = result[result['position'] == 1]
    sell_signals = result[result['position'] == -1]
    ax1.scatter(buy_signals.index, buy_signals['close'],
                marker='^', color='red', s=80, label='Buy')
    ax1.scatter(sell_signals.index, sell_signals['close'],
                marker='v', color='green', s=80, label='Sell')
    ax1.set_title('Price & Moving Averages')
    ax1.legend()

    # 累计收益对比
    ax2 = axes[1]
    ax2.plot(result.index, result['cumulative_market'], label='Buy & Hold')
    ax2.plot(result.index, result['cumulative_strategy'], label='Strategy')
    ax2.set_title('Cumulative Returns')
    ax2.legend()

    # 回撤
    ax3 = axes[2]
    cummax = result['cumulative_strategy'].cummax()
    drawdown = (result['cumulative_strategy'] - cummax) / cummax
    ax3.fill_between(result.index, drawdown, 0, alpha=0.3, color='red')
    ax3.set_title('Drawdown')

    plt.tight_layout()
    plt.savefig('strategy_result.png', dpi=100)
    plt.close()

plot_strategy(result)

策略局限性

双均线策略在趋势明显的行情中表现不错,但在震荡市中会频繁产生假信号,导致反复止损。改进方向:

  1. 加入过滤条件:比如要求金叉时成交量放大,或者 MACD 也确认
  2. 参数优化:用网格搜索或遗传算法寻找最优的均线周期组合
  3. 动态止损:加入 ATR 止损,控制单笔亏损
  4. 仓位管理:根据信号强度和波动率调整持仓比例

双均线虽然简单,但它是理解趋势跟踪策略的好起点。在这个基础上可以逐步叠加更多因子,演化出更复杂的策略。