策略回测是什么

作业题:「回测是什么?」

1、回测就是用历史数据当"模拟考场",假装在过去按你的买卖规则真金白银地交易了一遍。

2、它能告诉你这套规则历史上到底是赚是亏、最大亏过多少,把抽象的想法变成具体的收益曲线和胜率数字。

3、 回测只是"体检"不是"算命",过去灵验不代表未来必胜,它的真价值是在投入真钱之前帮你排雷、建立直觉。

比如 2024~2025 年,你每天都按「MA5 > MA20 就持有」操作------回测会告诉你:如果当时真这么干了,账户曲线长什么样。

如何模拟交易?

bash 复制代码
# ========== 第1步:下载数据并算双均线 ==========
raw = yf.download(TICKER, period=PERIOD, progress=False, multi_level_index=False)
df = raw[['Close']].dropna().copy()
df.columns = ['Close']

df['MA5'] = df['Close'].rolling(5).mean()
df['MA20'] = df['Close'].rolling(20).mean()
df['signal'] = (df['MA5'] > df['MA20']).astype(int)  # 收盘算出的「理论信号」

# ========== 第2步:信号推迟一天,避免用未来数据 ==========
df['position'] = df['signal'].shift(1).fillna(0).astype(int)  # 今天实际仓位

# ========== 第3步:标记买入、卖出日 ==========
df['position_change'] = df['position'].diff().fillna(0)  # 仓位变化:0→1买,1→0卖
df['action'] = ''
df.loc[df['position_change'] > 0, 'action'] = '买入'
df.loc[df['position_change'] < 0, 'action'] = '卖出'

trades = df[df['action'] != '']
print(f'标的 {TICKER},共 {len(df)} 个交易日')
print(f'模拟交易:买入 { (df["action"]=="买入").sum() } 次,卖出 { (df["action"]=="卖出").sum() } 次')
print('\n最近几次调仓:')
display(trades[['Close', 'MA5', 'MA20', 'position', 'action']].tail(6))
bash 复制代码
# ========== 价格+均线 + 持仓时间条 ==========
fig, axes = plt.subplots(2, 1, figsize=(14, 7), sharex=True,
                         gridspec_kw={'height_ratios': [2.5, 1]})

axes[0].plot(df.index, df['Close'], color='gray', linewidth=1, label='收盘价')
axes[0].plot(df.index, df['MA5'], color='tab:orange', linewidth=1.2, label='MA5')
axes[0].plot(df.index, df['MA20'], color='tab:blue', linewidth=1.5, label='MA20')
axes[0].set_ylabel('价格')
axes[0].set_title(f'{TICKER}:双均线策略 ------ 什么时候持仓?', fontsize=14)
axes[0].legend(loc='upper left')
axes[0].grid(True, alpha=0.3)

axes[1].fill_between(df.index, 0, df['position'], step='post', alpha=0.4, color='green')
axes[1].set_ylim(-0.1, 1.2)
axes[1].set_yticks([0, 1])
axes[1].set_yticklabels(['空仓', '持仓'])
axes[1].set_xlabel('日期')
axes[1].set_ylabel('仓位')
axes[1].grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

print('绿色区域 = 持仓(买入后、卖出前)| 空白 = 空仓')

策略收益率,分为"日收益率"和"累计净值"两步

第一步:计算"当天的策略收益率"

计算公式:策略当日收益率 = 仓位(0 或 1) × 股票当日收益率

举个例子(假设数据):

假设某天(比如 2025-10-14),position = 1(持仓),AAPL 当天股价涨了 +2%(即 ret = 0.02)。

那么这一天的 策略收益率 = 1 × 0.02 = +2%(满仓吃到全部涨幅)。

假设另一天(比如 2025-12-19),position = 0(空仓),AAPL 当天股价暴跌 -5%。

那么这一天的 策略收益率 = 0 × (-0.05) = 0%(因为空仓,暴跌和你无关)。

关键点:空仓的日子里,不管股票涨跌多凶,你的策略收益率都是 0。这正是策略能躲过大跌、控制回撤的根本原因。

第二步:计算"累计收益率(净值曲线)"

计算公式:今日策略净值 = 昨日策略净值 × (1 + 今日策略收益率)

第三步:为什么这么算?

策略收益率 = 只有"持仓那天"的股票涨跌才算数;"空仓那天"股票就算涨停,你的收益也是 0。把所有"持仓日的涨跌"连乘起来,就是你的策略总收益。

bash 复制代码
# ========== 算日收益率 ==========
df['ret'] = df['Close'].pct_change().fillna(0)  # 股票本身每天涨跌

# ========== 策略收益:只有持仓日才吃到涨跌 ==========
df['strategy_ret'] = df['position'] * df['ret']

# ========== 基准1:买入持有(一直满仓)==========
df['buyhold_ret'] = df['ret']

# ========== 基准2:同期持有大盘 SPY ==========
spy = yf.download(BENCHMARK, period=PERIOD, progress=False, multi_level_index=False)[['Close']]
spy.columns = ['SPY_Close']
df = df.join(spy, how='inner')  # 按日期对齐,只保留两边都有数据的行
df['market_ret'] = df['SPY_Close'].pct_change().fillna(0)

# ========== 累计净值:从 1 元钱出发连乘 ==========
df['nav_strategy'] = (1 + df['strategy_ret']).cumprod()
df['nav_buyhold'] = (1 + df['buyhold_ret']).cumprod()
df['nav_market'] = (1 + df['market_ret']).cumprod()

total_strategy = df['nav_strategy'].iloc[-1] - 1
total_buyhold = df['nav_buyhold'].iloc[-1] - 1
total_market = df['nav_market'].iloc[-1] - 1

print('=== 样本期累计收益(不含手续费,仅供学习)===')
print(f'  双均线策略 ({TICKER}): {total_strategy:+.2%}')
print(f'  买入持有 ({TICKER}):     {total_buyhold:+.2%}')
print(f'  买入持有 ({BENCHMARK} 大盘): {total_market:+.2%}')

回测结果可视化

看图时只问自己三件事:

1、策略曲线 整体往上 还是往下?

2、策略有没有 跑赢 买入持有?有没有 跑输 大盘?

3、中间有没有 大坑(后面 4.6 会讲回撤)?

bash 复制代码
# ========== 三条净值曲线对比(回测高潮图)==========
fig, ax = plt.subplots(figsize=(14, 6))

ax.plot(df.index, df['nav_strategy'], linewidth=2.2, color='tab:purple',
        label=f'双均线策略 ({TICKER})')
ax.plot(df.index, df['nav_buyhold'], linewidth=1.8, color='tab:blue', alpha=0.85,
        label=f'买入持有 ({TICKER})')
ax.plot(df.index, df['nav_market'], linewidth=1.8, color='tab:gray', linestyle='--',
        label=f'买入持有 ({BENCHMARK} 大盘)')

ax.axhline(1.0, color='black', linewidth=0.6, linestyle=':', alpha=0.5)
ax.set_title(f'回测净值曲线:策略 vs 标的 vs 大盘({PERIOD})', fontsize=14)
ax.set_xlabel('日期')
ax.set_ylabel('净值(起点=1)')
ax.legend(loc='upper left')
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
bash 复制代码
# ========== 近12个月局部放大 ==========
recent = df.last('12M') if len(df) > 200 else df.tail(200)

plt.figure(figsize=(14, 5))
plt.plot(recent.index, recent['nav_strategy'], linewidth=2, label='双均线策略')
plt.plot(recent.index, recent['nav_buyhold'], linewidth=1.6, label=f'买入持有 {TICKER}')
plt.plot(recent.index, recent['nav_market'], linewidth=1.6, linestyle='--', label=f'买入持有 {BENCHMARK}')
plt.title('近 12 个月:策略 vs 基准(局部)', fontsize=14)
plt.xlabel('日期')
plt.ylabel('净值')
plt.legend()
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()

什么是胜率?

你完整做完的每一笔「买进→卖出」,赚的多还是亏的多?

胜率(一轮轮交易的胜率)= 赢的次数 ÷(赢 + 输)

bash 复制代码
# ========== 统计胜率:一轮「买入→卖出」算一局 ==========
wins, losses = 0, 0       # 赢、输次数
entry_price = None        # 记住买入价
records = []              # 存每轮结果

for date, row in df.iterrows():  # 按天遍历整张表
    if row['action'] == '买入':
        entry_price = row['Close']   # 记录买入当天的收盘价
    elif row['action'] == '卖出' and entry_price is not None:
        pnl = row['Close'] / entry_price - 1   # 本轮收益率
        if pnl > 0:
            wins += 1
            outcome = '赢'
        else:
            losses += 1
            outcome = '输'
        records.append({
            '卖出日': date.strftime('%Y-%m-%d'),
            '买入价': round(entry_price, 2),
            '卖出价': round(row['Close'], 2),
            '本轮收益': f'{pnl:+.2%}',
            '结果': outcome,
        })
        entry_price = None   # 本轮结束,清空买入价

total_rounds = wins + losses
win_rate = wins / total_rounds if total_rounds > 0 else np.nan

print(f'完整交易回合:{total_rounds} 轮')
print(f'  赢:{wins} 次')
print(f'  输:{losses} 次')
print(f'  胜率:{win_rate:.1%}' if total_rounds > 0 else '  暂无完整买卖回合')

if records:
    display(pd.DataFrame(records).tail(8))

什么是最大回撤?

回撤越大,心理压力通常越大 ------ 风险真实存在。

想象你的净值曲线是一座山:

你先爬到一个 山顶(历史最高点)

然后一路往下走,掉到某个 谷底

从山顶到谷底,跌了多少比例 ------ 这一段就叫 回撤

整段样本里 最深的那一次,叫 最大回撤(Max Drawdown)

bash 复制代码
# ========== 最大回撤:从历史最高点最多跌了多少 ==========
def max_drawdown(nav_series):
    """输入净值序列,返回 (最大回撤比例, 每日回撤序列)。"""
    peak = nav_series.cummax()           # 到每一天为止的历史最高净值
    drawdown = nav_series / peak - 1     # 当前净值相对峰值的跌幅
    return drawdown.min(), drawdown

mdd_strategy, dd_strategy = max_drawdown(df['nav_strategy'])
mdd_buyhold, dd_buyhold = max_drawdown(df['nav_buyhold'])

print('=== 最大回撤(样本期内最深一次「从山顶滑落」)===')
print(f'  双均线策略: {mdd_strategy:.2%}')
print(f'  买入持有 ({TICKER}): {mdd_buyhold:.2%}')

fig, ax = plt.subplots(figsize=(14, 5))
ax.fill_between(df.index, dd_strategy * 100, 0, alpha=0.35, color='tab:purple', label='策略回撤 %')
ax.plot(df.index, dd_strategy * 100, color='tab:purple', linewidth=1)
ax.set_title(f'策略回撤示意图(最大回撤 = {mdd_strategy:.2%})', fontsize=14)
ax.set_xlabel('日期')
ax.set_ylabel('相对历史高点的跌幅 (%)')
ax.legend()
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()