作业题:「回测是什么?」
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()
