1. 核心概念
1.1 什么是回测?
回测就是用历史数据,假装自己过去真的按照一套交易规则执行了一遍。
它的作用不是预测未来,而是帮助我们检查:
- 这个策略过去有没有赚钱;
- 赚钱过程是否稳定;
- 中间最大亏损有多深;
- 策略是否跑赢简单的买入持有或大盘基准。
我对这句话印象最深:
回测是体检,不是算命。
也就是说,回测结果好不代表未来一定赚钱,只能说明这个策略在历史样本里表现如何。
2. 买入持有和双均线策略
2.1 买入持有是什么?
买入持有就是一开始买入,然后中间不操作,一直拿到最后。
例如:
text
第一天买入 NVDA
中间不管涨跌都不卖
最后一天看总收益
买入持有通常用来当作基准。如果一个策略频繁买卖,但最后还不如一直拿着,那么这个策略就需要重新评估。
2.2 双均线策略是什么?
双均线策略是用两条移动平均线判断趋势。
本次代码中使用的是:
python
MA5 = 最近 5 天平均收盘价
MA20 = 最近 20 天平均收盘价
交易规则是:
text
MA5 > MA20:短期趋势强于长期趋势,明天持仓
MA5 <= MA20:短期趋势弱于长期趋势,明天空仓
代码里最关键的一句是:
python
df['position'] = df['signal'].shift(1).fillna(0).astype(int)
这里使用 shift(1) 是为了避免"偷看未来"。因为今天的均线信号要等今天收盘后才能知道,所以只能从下一个交易日开始执行。
3. 关键代码理解
3.1 计算每日收益率
python
df['ret'] = df['Close'].pct_change().fillna(0)
这行代码计算股票每天的涨跌幅。
3.2 计算策略收益
python
df['strategy_ret'] = df['position'] * df['ret']
含义是:
position = 1时,策略持仓,吃到当天涨跌;position = 0时,策略空仓,当天收益为 0。
3.3 计算累计净值
python
df['nav_strategy'] = (1 + df['strategy_ret']).cumprod()
cumprod() 的作用是把每天的收益连续乘起来,得到从 1 元钱开始的账户净值变化。
4. 最大回撤
最大回撤表示账户净值从历史最高点到后面最低点之间,最大跌了多少。
计算逻辑是:
python
peak = nav_series.cummax()
drawdown = nav_series / peak - 1
最大回撤不是看最后赚没赚钱,而是看过程中最痛苦的时候亏了多少。
所以一个策略即使最后赚钱,如果中间最大回撤太大,也可能很难坚持执行。
5. 本次作业
挑战 1:把 TICKER 改成 NVDA,截一张策略 vs 大盘净值图,写一句话:跑赢还是跑输?
python
# ========== 第四章通关作业:策略回测 ==========
import sys
import warnings
from pathlib import Path
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import yfinance as yf
warnings.filterwarnings('ignore')
if hasattr(sys.stdout, 'reconfigure'):
sys.stdout.reconfigure(encoding='utf-8')
if hasattr(sys.stderr, 'reconfigure'):
sys.stderr.reconfigure(encoding='utf-8')
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False
TICKER = 'NVDA'
BENCHMARK = 'SPY'
OUTPUT_DIR = Path(__file__).resolve().parent
def max_drawdown(nav_series):
"""返回最大回撤比例。"""
peak = nav_series.cummax()
drawdown = nav_series / peak - 1
return drawdown.min(), drawdown
def run_backtest(period):
"""运行双均线策略回测,并返回带净值和回撤的数据。"""
raw = yf.download(TICKER, period=period, progress=False, multi_level_index=False)
if raw.empty:
raise RuntimeError(f'{TICKER} 在 {period} 周期没有下载到数据')
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)
df['position'] = df['signal'].shift(1).fillna(0).astype(int)
df['ret'] = df['Close'].pct_change().fillna(0)
df['strategy_ret'] = df['position'] * df['ret']
df['buyhold_ret'] = df['ret']
spy = yf.download(BENCHMARK, period=period, progress=False, multi_level_index=False)[['Close']]
if spy.empty:
raise RuntimeError(f'{BENCHMARK} 在 {period} 周期没有下载到数据')
spy.columns = ['SPY_Close']
df = df.join(spy, how='inner')
df['market_ret'] = df['SPY_Close'].pct_change().fillna(0)
df['nav_strategy'] = (1 + df['strategy_ret']).cumprod()
df['nav_buyhold'] = (1 + df['buyhold_ret']).cumprod()
df['nav_market'] = (1 + df['market_ret']).cumprod()
df['mdd_strategy'], df['drawdown_strategy'] = max_drawdown(df['nav_strategy'])
df['mdd_buyhold'], df['drawdown_buyhold'] = max_drawdown(df['nav_buyhold'])
return df
def save_nav_chart(df, period):
"""保存策略 vs 标的 vs 大盘净值图。"""
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'{TICKER} 回测净值曲线:策略 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()
chart_path = OUTPUT_DIR / f'homework_{TICKER}_{period}_nav_comparison.png'
plt.savefig(chart_path, dpi=150)
plt.close(fig)
return chart_path
def summarize(period, df):
"""打印单个周期的关键回测结果。"""
total_strategy = df['nav_strategy'].iloc[-1] - 1
total_buyhold = df['nav_buyhold'].iloc[-1] - 1
total_market = df['nav_market'].iloc[-1] - 1
mdd_strategy = df['mdd_strategy'].iloc[-1]
mdd_buyhold = df['mdd_buyhold'].iloc[-1]
print(f'\n=== {period} 回测结果 ===')
print(f'双均线策略 ({TICKER}) 累计收益:{total_strategy:+.2%}')
print(f'买入持有 ({TICKER}) 累计收益:{total_buyhold:+.2%}')
print(f'买入持有 ({BENCHMARK} 大盘) 累计收益:{total_market:+.2%}')
print(f'双均线策略最大回撤:{mdd_strategy:.2%}')
print(f'买入持有最大回撤:{mdd_buyhold:.2%}')
return mdd_strategy
def main():
print('第四章作业:NVDA 双均线策略回测 [OK]')
df_1y = run_backtest('1y')
chart_path = save_nav_chart(df_1y, '1y')
print(f'\n作业1:策略 vs 大盘净值图已保存:{chart_path}')
strategy_end = df_1y['nav_strategy'].iloc[-1]
market_end = df_1y['nav_market'].iloc[-1]
race_answer = '跑赢' if strategy_end > market_end else '跑输'
print(f'一句话:1y 周期里,NVDA 双均线策略相对 SPY 大盘是{race_answer}。')
summarize('1y', df_1y)
if __name__ == '__main__':
main()

运行结果:
text
双均线策略 (NVDA) 累计收益:-12.45%
买入持有 (NVDA) 累计收益:+22.21%
买入持有 (SPY 大盘) 累计收益:+19.87%
双均线策略最大回撤:-28.41%
买入持有最大回撤:-20.21%
结论:
1 年周期里,NVDA 双均线策略相对 SPY 大盘是跑输的。
挑战 2:对比 PERIOD='1y' 和 '5y',最大回撤哪个更深?我更能接受哪种?
根据运行结果:
text
1y 双均线策略最大回撤:-28.41%
5y 双均线策略最大回撤:-52.50%
结论:
5y 的最大回撤更深。
如果只看最大回撤,我更能接受 1y 的结果,因为它从历史高点跌下来的幅度更小。5y 虽然样本更长,但过程中经历过更深的下跌,对执行策略的心理压力更大。
挑战 3:用三句话向朋友解释"回测是什么"
- 回测就是用历史数据,假装自己过去真的按照一套规则买入、持仓和卖出。
- 它能帮我们看清策略过去的收益、回撤和风险,但不能保证未来还会赚钱。
- 所以回测更像是给策略做体检,而不是预测未来。
6. 学习理解
量化策略不能只看"最后赚了多少",还要看过程中有没有大幅亏损,以及是否真的跑赢了简单基准。以后看策略时,我应该同时关注收益、最大回撤、基准比较等。