2. 策略回测与参数优化

任务目标

基于历史K线数据,实现双均线策略的回测引擎,并通过网格搜索寻找最优的短期与长期均线参数组合。回测结果包含总收益率、最大回撤与夏普比率,用于量化评估策略表现。

环境与依赖

需要 pandas、numpy。数据使用本地CSV文件(包含 date, close 列),可通过 yfinance 下载或使用任意历史行情数据。

关键完整代码

import pandas as pd
import numpy as np

def load_data(csv_path):
    """加载CSV,按日期升序排列"""
    df = pd.read_csv(csv_path, parse_dates=['date'])
    df = df.sort_values('date').reset_index(drop=True)
    return df

def backtest_ma(df, short_win, long_win, commission=0.0005):
    """
    双均线策略回测
    规则:短期均线上穿长期均线时全仓买入,下穿时清仓
    返回:总收益率、最大回撤、夏普比率(年化)
    """
    df = df.copy()
    df['short_ma'] = df['close'].rolling(short_win).mean()
    df['long_ma'] = df['close'].rolling(long_win).mean()
    
    # 生成信号:1持仓,0空仓
    df['signal'] = 0
    df.loc[df['short_ma'] > df['long_ma'], 'signal'] = 1
    df['position'] = df['signal'].diff().fillna(0)
    
    # 计算每日收益率(含手续费)
    df['daily_ret'] = df['close'].pct_change().fillna(0)
    df['strategy_ret'] = df['signal'].shift(1) * df['daily_ret'] - commission * df['position'].abs()
    
    # 累计净值
    df['equity'] = (1 + df['strategy_ret']).cumprod()
    
    # 总收益率
    total_return = df['equity'].iloc[-1] - 1
    
    # 最大回撤
    peak = df['equity'].cummax()
    drawdown = (df['equity'] - peak) / peak
    max_drawdown = drawdown.min()
    
    # 夏普比率(年化,假设252个交易日)
    std = df['strategy_ret'].std()
    sharpe = 0 if std == 0 else (df['strategy_ret'].mean() / std) * np.sqrt(252)
    
    return total_return, max_drawdown, sharpe

def grid_search(df, short_range, long_range):
    """网格搜索最优参数(按夏普比率排序)"""
    results = []
    for s in short_range:
        for l in long_range:
            if s >= l:  # 短期必须小于长期
                continue
            tr, mdd, sharpe = backtest_ma(df, s, l)
            results.append({
                'short_win': s,
                'long_win': l,
                'total_return': tr,
                'max_drawdown': mdd,
                'sharpe': sharpe
            })
    return pd.DataFrame(results).sort_values('sharpe', ascending=False)

# ========== 最小执行入口 ==========
if __name__ == '__main__':
    # 使用示例数据(实际使用时替换为真实CSV路径)
    dates = pd.date_range('2020-01-01', periods=500, freq='D')
    np.random.seed(42)
    close = 100 + np.cumsum(np.random.randn(500) * 0.5)
    demo_df = pd.DataFrame({'date': dates, 'close': close})
    demo_df.to_csv('demo_data.csv', index=False)
    
    # 执行网格搜索
    df = load_data('demo_data.csv')
    short_wins = range(5, 21, 2)   # 5,7,...,19
    long_wins = range(20, 61, 5)   # 20,25,...,60
    result_df = grid_search(df, short_wins, long_wins)
    
    # 输出最优参数
    best = result_df.iloc[0]
    print(f"最优参数: short={best['short_win']}, long={best['long_win']}")
    print(f"夏普比率: {best['sharpe']:.3f}")
    print(f"总收益率: {best['total_return']:.2%}")
    print(f"最大回撤: {best['max_drawdown']:.2%}")
    print(f"\n参数组合数: {len(result_df)}")
    print("Top 5 结果:")
    print(result_df.head().to_string(index=False))

运行与验证

运行方式:

python ma_backtest.py

验证方法:

  1. 程序正常结束并输出最优参数及对应指标。
  2. 检查输出中 short_win < long_win,且所有指标数值在合理范围(总收益率通常在 -1~5 之间,夏普比率通常 -2~5)。
  3. 将 demo_data.csv 替换为真实历史数据(如沪深300日线),重新运行,观察结果是否合理变化。

通过条件:程序无报错,输出包含5列完整指标的最优参数行,且参数组合数等于 len(short_wins) * len(long_wins) 减去无效组合数(即 s >= l 的组合)。

核心逻辑说明

回测引擎在每日收盘后计算双均线,信号在次日开盘生效(通过 shift(1) 实现),避免未来函数。手续费按每次仓位变化的成交金额双边收取。网格搜索遍历所有短长均线组合,以夏普比率作为参数择优标准,兼顾收益与波动风险。