1. 基于均线交叉的简单调仓策略

基于均线交叉的简单调仓策略 学习卡片

任务目标

实现一个基于双均线交叉的调仓策略:当短期均线(如20日)上穿长期均线(如60日)时产生买入信号(金叉),下穿时产生卖出信号(死叉)。策略根据信号在满仓(100%)与空仓(0%)之间切换,并在同一数据集上完成回测,输出策略净值与基准(买入持有)净值的对比结果。

环境与依赖

本案例使用 Python 3.8+,依赖 pandas、numpy。无需安装额外回测框架,全部逻辑在单个脚本中实现。

完整实现代码

import pandas as pd
import numpy as np

# ---------- 1. 生成模拟价格数据(确定性,可复现) ----------
def generate_price_data(n=500, seed=42):
    np.random.seed(seed)
    returns = np.random.normal(0.0002, 0.015, n)
    price = 100 * np.exp(np.cumsum(returns))
    dates = pd.date_range('2022-01-01', periods=n, freq='B')
    return pd.Series(price, index=dates, name='close')

# ---------- 2. 均线交叉调仓策略 ----------
def moving_average_crossover_strategy(price, short_win=20, long_win=60):
    df = pd.DataFrame({'close': price})
    df['short_ma'] = df['close'].rolling(short_win).mean()
    df['long_ma'] = df['close'].rolling(long_win).mean()
    
    # 信号:1=金叉买入,-1=死叉卖出,0=无操作
    df['signal'] = 0
    df.loc[df['short_ma'] > df['long_ma'], 'signal'] = 1
    df.loc[df['short_ma'] < df['long_ma'], 'signal'] = -1
    
    # 仓位:金叉后满仓,死叉后空仓(使用shift避免未来函数)
    df['position'] = df['signal'].replace(0, np.nan).ffill().fillna(0).shift(1).fillna(0)
    df['position'] = df['position'].clip(lower=0, upper=1)
    
    return df

# ---------- 3. 回测 ----------
def backtest(df, initial_capital=10000):
    df = df.copy()
    df['daily_return'] = df['close'].pct_change().fillna(0)
    df['strategy_return'] = df['position'] * df['daily_return']
    df['strategy_net'] = initial_capital * (1 + df['strategy_return']).cumprod()
    df['benchmark_net'] = initial_capital * (1 + df['daily_return']).cumprod()
    return df

# ---------- 4. 运行与输出 ----------
if __name__ == '__main__':
    price = generate_price_data()
    df = moving_average_crossover_strategy(price)
    result = backtest(df)
    
    # 输出关键指标
    total_return_strategy = result['strategy_net'].iloc[-1] / 10000 - 1
    total_return_benchmark = result['benchmark_net'].iloc[-1] / 10000 - 1
    
    print(f"策略累计收益率: {total_return_strategy:.2%}")
    print(f"基准累计收益率: {total_return_benchmark:.2%}")
    print(f"最终策略净值: {result['strategy_net'].iloc[-1]:.2f}")
    print(f"最终基准净值: {result['benchmark_net'].iloc[-1]:.2f}")
    print(f"调仓次数: {int((df['signal'].diff() != 0).sum() - 1)}")
    
    # 保存净值序列(可选)
    result[['strategy_net', 'benchmark_net']].to_csv('backtest_result.csv')
    print("净值数据已保存至 backtest_result.csv")

运行方式

python ma_crossover_backtest.py

验证方法与判定标准

运行脚本后,控制台将输出策略与基准的累计收益率、最终净值及调仓次数。判定通过条件:

  • 程序无报错正常退出;
  • 输出结果中策略净值曲线与基准净值曲线均从10000起算;
  • 调仓次数大于0(说明均线交叉信号确实触发了仓位变化);
  • 生成的 backtest_result.csv 包含两列净值数据,行数与输入价格数据一致。

由于使用固定随机种子(seed=42),每次运行结果完全一致,便于验证。策略累计收益率与基准的差异取决于均线参数及市场数据特征,本案例中金叉/死叉信号在震荡行情下可能产生连续亏损,这是策略本身的特性而非代码错误。

关键实现说明

  • 避免未来函数:仓位信号通过 shift(1) 延迟一天生效,即当日收盘确认金叉后,次日开盘才调整仓位,符合真实交易逻辑。
  • 信号处理:使用 ffill() 将离散信号转换为连续持仓状态,直到下一次反向信号出现才改变仓位。
  • 边界处理:前60个交易日因长期均线未形成,仓位默认为0(空仓),不参与交易。

常见问题

  • 为什么策略收益可能低于基准?:均线交叉在趋势行情中表现较好,但在震荡市中会频繁产生错误信号,导致高买低卖。这是策略本身的局限,可通过增加过滤条件(如价格高于长期均线才做多)优化。
  • 如何修改均线参数?:直接修改 moving_average_crossover_strategy 函数中的 short_win 和 long_win 参数即可。
  • 能否替换为真实行情数据?:将 generate_price_data() 替换为从CSV或API读取的收盘价序列即可,后续逻辑无需改动。