1. 搭建最小可运行的回测框架

案例1:搭建最小回测框架 学习卡片

任务说明

本案例实现一个最小可用的单资产回测框架:给定历史价格序列和一个简单的均线交叉策略,框架按日逐笔模拟交易,输出每日账户权益曲线,并计算累计收益率、年化收益率、最大回撤、夏普比率和胜率五项基础统计指标。代码不涉及滑点、手续费、复利以外的资金管理细节,以清晰展示回测主循环为核心。

核心实现

import numpy as np
import pandas as pd

def generate_sample_data(n=500, seed=42):
    """生成模拟价格序列(几何布朗运动)"""
    np.random.seed(seed)
    returns = np.random.normal(0.0005, 0.01, n)
    price = 100 * np.exp(np.cumsum(returns))
    return pd.Series(price, index=pd.date_range('2022-01-01', periods=n, freq='D'))

def moving_average_crossover(price, short=5, long=20):
    """均线交叉策略:短期上穿长期产生买入信号,下穿产生卖出信号"""
    signal = pd.Series(0, index=price.index)
    ma_short = price.rolling(short).mean()
    ma_long = price.rolling(long).mean()
    # 1=持有,0=空仓
    position = (ma_short > ma_long).astype(int)
    signal = position.diff().fillna(0)
    return signal, position

def run_backtest(price, signal, initial_cash=10000):
    """最小回测循环:逐日更新持仓与现金"""
    cash = initial_cash
    shares = 0
    equity_curve = []
    trades = []

    for i in range(len(price)):
        today_price = price.iloc[i]
        # 先处理信号(用当日收盘价成交)
        if signal.iloc[i] == 1 and shares == 0:
            shares = cash // today_price
            cash -= shares * today_price
            trades.append(('BUY', price.index[i], today_price, shares))
        elif signal.iloc[i] == -1 and shares > 0:
            cash += shares * today_price
            trades.append(('SELL', price.index[i], today_price, shares))
            shares = 0
        # 记录每日总资产
        equity_curve.append(cash + shares * today_price)

    equity = pd.Series(equity_curve, index=price.index)
    return equity, trades

def compute_stats(equity, risk_free_rate=0.02):
    """计算基础统计指标"""
    total_return = equity.iloc[-1] / equity.iloc[0] - 1
    years = len(equity) / 252
    annual_return = (1 + total_return) ** (1 / years) - 1
    daily_returns = equity.pct_change().dropna()
    sharpe = np.sqrt(252) * (daily_returns.mean() - risk_free_rate / 252) / daily_returns.std()
    # 最大回撤
    cummax = equity.cummax()
    drawdown = (equity - cummax) / cummax
    max_drawdown = drawdown.min()
    # 胜率(按交易对统计)
    win_count = 0
    total_trades = 0
    buy_prices = []
    for t in trades:
        if t[0] == 'BUY':
            buy_prices.append(t[2])
        else:
            if buy_prices:
                total_trades += 1
                if t[2] > buy_prices.pop(0):
                    win_count += 1
    win_rate = win_count / total_trades if total_trades > 0 else 0

    return {
        '累计收益率': total_return,
        '年化收益率': annual_return,
        '最大回撤': max_drawdown,
        '夏普比率': sharpe,
        '胜率': win_rate,
        '交易次数': total_trades
    }

# 主流程
if __name__ == '__main__':
    price = generate_sample_data()
    signal, position = moving_average_crossover(price)
    equity, trades = run_backtest(price, signal)
    stats = compute_stats(equity, trades)
    print('=== 回测结果 ===')
    for k, v in stats.items():
        print(f'{k}: {v:.4f}' if isinstance(v, float) else f'{k}: {v}')

关键设计说明

回测主循环位于run_backtest函数中,采用逐日迭代方式:每日先检查信号,若产生买入信号且当前空仓则全仓买入;若产生卖出信号且当前持仓则清仓。成交价使用当日收盘价,这是最小框架的常见简化。权益曲线每日记录现金与持仓市值之和,完整反映策略净值变化。

均线交叉策略中,position表示每日应持有的状态(1或0),signal是状态变化点(1表示买入日,-1表示卖出日)。由于均线需要历史数据,前20个交易日无信号,属于正常预热期。

验证方法

直接运行脚本,观察控制台输出。判定标准:

  • 程序无异常退出,输出五项统计指标及交易次数;
  • 累计收益率与equity.iloc[-1]/equity.iloc[0]-1手算结果一致;
  • 最大回撤为负数或零,且值不超过1;
  • 夏普比率为有限数值(非NaN),胜率在0到1之间。

如需验证逻辑正确性,可将generate_sample_data的随机种子固定后,用相同数据手动计算前几笔交易,与trades列表对比。

最小回测框架数据流 价格序列 generate_sample_data() 均线交叉策略 moving_average_crossover() 回测主循环 run_backtest() 统计指标 compute_stats() 控制台输出 交易信号 signal (1/-1) 权益曲线 equity_curve

运行与验证

# 安装依赖(如未安装)
pip install numpy pandas

# 运行回测
python backtest_minimal.py

预期输出格式如下(数值随随机种子变化,但结构一致):

=== 回测结果 ===
累计收益率: 0.xxxx
年化收益率: 0.xxxx
最大回撤: -0.xxxx
夏普比率: x.xxxx
胜率: 0.xxxx
交易次数: N

将代码保存为backtest_minimal.py后执行即可。若需更换策略,只需修改moving_average_crossover返回的signal序列,其余框架代码无需改动。