1. 这节究竟解决什么实际问题?¶
4.0 策略从哪里来? 的具体任务是:从经济机制而非历史曲线出发,提出可证伪的策略假设。 本节将明确输入数据、计算过程、输出及其在后续研究中的用途。
2. 前置知识¶
理解收益率、仓位、交易成本与基本技术指标。 对公式的理解应包括变量定义、单位、时间索引和适用条件。
3. 数据说明¶
默认固定历史样本。历史数据只能说明过去在既定假设下发生了什么,不能承诺未来收益。 运行前先检查 shape、日期范围、列名与缺失值。数据看起来能画图,不代表它适合回答研究问题。
4. 学习目标与完成标准¶
完成本节后,你应能用自己的话解释 4.0 策略从哪里来? 的输入、计算步骤、输出和局限;尤其能说明:从经济机制而非历史曲线出发,提出可证伪的策略假设。 你还应能独立改动一个参数并解释结果为什么变化,指出至少一个会让结论失真的假设。
5. 核心概念与公式¶
回测按照明确的成交和成本假设重放历史数据。它不能完整复现未来流动性、订单竞争和系统故障。
策略收益 = 滞后仓位 × 资产收益 − 换手 × 成本;每一项都要有明确单位。 看到公式时依次确认:每个符号的单位是什么?它属于哪个时点?分母会不会为零?它是描述性统计量、预测量,还是交易规则?
6. 从零实现¶
原始代码按照“准备输入 → 计算中间量 → 得到结果 → 解释输出”的顺序组织。每个单元应检查变量类型、索引、维度和代表性数值。中间变量用于核对公式实现和定位计算错误。
7. 成熟库实现¶
回测框架替你管理订单和记录,不会自动判断数据、成本或策略假设是否合理。 使用库函数前先阅读参数含义、默认值和返回对象;库输出一个数字,并不说明这个数字适合你的交易假设。
8. 两种实现的差异¶
从零实现便于核对公式、调试和识别隐含假设;成熟库适合在计算逻辑已验证后复用。两种实现依赖相同的数据口径与时点假设,使用库函数不会改变输入数据的质量。
9. 预期结果与检查范围¶
你应看到维度正确的 Series/DataFrame、日期单调递增的索引,以及能支持“从经济机制而非历史曲线出发,提出可证伪的策略假设。”这一任务的表格或图形。金融日收益率均值通常接近 0;若出现极大的连续收益、空表、全 NaN 或索引错位,应先排查数据和对齐逻辑,而不是立即相信结果。
10. 常见错误与诊断¶
信号未滞后、把手续费只扣一次、忽略空仓收益、年化频率错配、将净值曲线当显著性证明。
诊断顺序:先打印类型与 shape,再看 head()/tail(),接着检查日期和 NaN,最后才比较数值。这样能避免在错误的对象上反复改参数。
11. 研究偏差检查¶
检查未来函数、幸存者偏差、重复测试集调参、成交可得性和失败区间;必须报告基线。 结果至少应与一个简单基线比较,并明确哪些部分只是教学演示。
12. 三层练习与答案¶
- 基础练习:验证一个中间变量、补全一段小计算,并用一句话解释输出。
- 研究练习:改变窗口、样本区间、约束或标的,记录哪些结论仍成立。
- 开放项目:把本节方法嵌入模块项目,报告基线、失败结果和限制。
题目在 ../exercises/04_backtesting/00_strategy_thinking_exercises.ipynb;完整推导、可运行参考实现与结果检查在 ../solutions/04_backtesting/00_strategy_thinking_solutions.ipynb。请先独立作答,再打开答案。
13. 延伸阅读¶
延伸材料:Advances in Financial Machine Learning(Lopez de Prado)。阅读记录应包括研究假设、数据频率、样本范围和结论适用条件。
14. 下一节¶
下一节是 01_simple_backtest。本节的概念会成为理解它的输入,而不是孤立的名词。
1. 策略为什么能赚钱?——三大来源¶
市场不是无缘无故给你钱的。策略能赚钱,必须依赖某种可持续的优势。这些优势大致来自三个地方:
来源一:行为偏差(Behavioral Edge)¶
人类不是理性的。我们有很多可预测的、系统性的错误:
- 羊群效应:投资者跟随已有交易方向,可能强化价格趋势
- 过度反应:价格对信息的短期反应超过基本面变化,可能随后出现修正
- 锚定效应:「这股票以前涨到 200,现在 150,感觉便宜」(→ 错误定价)
- 损失厌恶:投资者对等额损失与收益的主观权重不同,可能影响持仓和卖出决策
量化策略的本质之一:系统性地捕捉人类的系统性错误。
来源二:风险溢价(Risk Premium)¶
有些收益是对「承担风险」的补偿,不是「免费的午餐」:
- 股票长期跑赢国债,因为你承担了公司倒闭的风险
- 小盘股长期跑赢大盘股,因为你承担了流动性风险
- 高波动率资产有更高收益,因为大多数人不喜欢波动
这类策略的预期收益可能来自风险补偿,因此必须同时评估对应的下行风险。
来源三:结构性优势(Structural Edge)¶
市场结构本身造成的定价偏差:
- 指数调仓效应:指数成分股调整时,被动基金必须买/卖,造成短期量价异常
- 流动性提供:做市商通过买卖价差赚钱,而非预测涨跌
- 季节性效应:「一月效应」「卖掉五月」等统计规律
** 关键判断问题:** 每次你想到一个策略,都要问自己:
「我的优势来自哪里?为什么别人没有发现这个,或者发现了为什么不愿意去做它?」
如果答不上来,这个策略很可能是随机噪声,或者已经被套利掉了。
2. 四大策略类型——核心逻辑与直觉¶
策略类型一:趋势跟随(Trend Following)¶
核心信念:「涨的会继续涨,跌的会继续跌。」
这听起来像是废话,但有非常扎实的学术依据——「动量效应」(Momentum Effect)在几乎所有资产类别和时间段都被验证过。
为什么趋势会持续?
- 好消息往往分批传播,不是一次性被市场消化
- 机构投资者仓位调整需要时间(不是一天能买完)
- 趋势吸引更多跟随者,形成自我强化
典型策略:
- 均线金叉:SMA20 上穿 SMA60 → 买入
- 突破策略:价格突破 52 周新高 → 买入
- MACD 信号:MACD 线上穿信号线 → 买入
适合的市场: 单边趋势行情(牛市/熊市)
不适合的市场: 震荡盘整市(会被反复打止损、假信号多)
策略类型二:均值回归(Mean Reversion)¶
核心信念:「价格偏离正常水平太远了,总会回来的。」
均值回归假设偏离长期水平的幅度越大,后续向该水平回归的概率或幅度越高;这个假设需要用样本外数据检验。
为什么会有均值回归?
- 市场的过度反应:坏消息出来,恐慌性抛售把价格砸过头了
- 统计均衡:历史上价格总是在一个中枢附近波动
- 套利者的存在:偏离太远时,「聪明钱」会入场修正
典型策略:
- RSI < 30 时买入(超卖,可能反弹)
- 价格触碰布林带下轨时买入
- 配对交易:两个高度相关的股票价差扩大时套利
适合的市场: 震荡区间市
不适合的市场: 单边趋势(「越涨越买」会追到顶,「越跌越买」会越套越深)
策略类型三:跨截面动量(Cross-Sectional Momentum)¶
核心信念:「同一时期表现最好的资产,短期内往往继续表现好。」
不是预测某一只股票绝对涨跌,而是预测相对排名。
做法:
- 每月末计算所有股票过去 12 个月(除最近 1 个月)的收益
- 买入排名前 20% 的「赢家」
- 卖出(做空)排名后 20% 的「输家」
这是诺贝尔奖级别的策略,由 Jegadeesh & Titman 在 1993 年首次系统记录。
策略类型四:统计套利(Statistical Arbitrage)¶
核心信念:「两个高相关的资产,价差应该稳定,偏离时可以套利。」
经典例子:可口可乐(KO)和百事可乐(PEP)。都是碳酸饮料公司,股价长期高度相关。如果某天 KO 跌、PEP 不跌,这可能是短暂偏离,买 KO + 卖空 PEP,等价差回归时获利。
关键特点: 策略收益与市场涨跌无关(市场中性),靠的是价差的均值回归。
策略类型对比总结¶
| 策略类型 | 核心信念 | 挣钱来源 | 适合市场 | 风险点 |
|---|---|---|---|---|
| 趋势跟随 | 动量持续 | 行为偏差(羊群效应) | 单边行情 | 震荡市假信号多 |
| 均值回归 | 偏离会修正 | 行为偏差(过度反应) | 区间震荡 | 趋势市扛不住 |
| 跨截面动量 | 相对强弱持续 | 行为偏差 + 机构滞后 | 股票多空 | 动量崩溃风险 |
| 统计套利 | 价差均值回归 | 结构性 + 统计规律 | 任意市场 | 相关性破裂 |
3. 从想法到策略:完整开发流程¶
灵感/假设
↓
逻辑自洽性检验(问自己:为什么这能赚钱?)
↓
初步代码实现
↓
样本内回测(In-Sample)——用历史数据验证
↓
样本外测试(Out-of-Sample)——用没见过的数据验证
↓
敏感性分析(参数稍微变化,策略是否还稳定?)
↓
加入真实交易成本(手续费、滑点)
↓
风险控制设计(最大仓位、止损规则)
↓
小资金实盘测试
每一步都有可能淘汰掉你的策略——这是正常的。 大多数策略死在「样本外测试」这一关。原因通常是:
- 过拟合(Overfitting):参数太多,历史数据「背」得太熟,但遇到新数据就失效
- Look-ahead Bias:不小心用了未来数据(下一节详细讲)
- 幸存者偏差(Survivorship Bias):只用了还在市场上的股票数据,忽略了已退市、破产的股票
import matplotlib.pyplot as plt
# 1. 设置系统自带的中文字体(这里使用黑体 SimHei)
plt.rcParams['font.sans-serif'] = ['SimHei'] # 如果你想用微软雅黑,可以改成 ['Microsoft YaHei']
# 2. 解决更换字体后,负号(-)显示为方块的问题
plt.rcParams['axes.unicode_minus'] = False
import numpy as np
import pandas as pd
# 演示:过拟合的可视化
# 用随机收益率数据展示样本内筛选结果与样本外结果的差异
np.random.seed(42)
n = 300
returns = pd.Series(np.random.normal(0.0003, 0.01, n)) # 纯随机价格变动
# 把数据切成两半
in_sample = returns[:150] # 样本内:用来开发策略
out_sample = returns[150:] # 样本外:用来验证策略
# 在样本内穷举 SMA 参数,找「最优」参数
best_return = -np.inf
best_params = None
all_in_returns = []
for fast in range(3, 20):
for slow in range(fast + 3, 40):
price = (1 + in_sample).cumprod()
sma_f = price.rolling(fast).mean()
sma_s = price.rolling(slow).mean()
pos = (sma_f > sma_s).shift(1).fillna(False).astype(int)
r = (pos * in_sample).sum()
all_in_returns.append(r)
if r > best_return:
best_return = r
best_params = (fast, slow)
# 用样本内「最优」参数跑样本外
f, s = best_params
price_full = (1 + returns).cumprod()
sma_f_full = price_full.rolling(f).mean()
sma_s_full = price_full.rolling(s).mean()
pos_full = (sma_f_full > sma_s_full).shift(1).fillna(False).astype(int)
strat_ret = pos_full * returns
cum_strat_in = (1 + strat_ret[:150]).cumprod()
cum_strat_out = (1 + strat_ret[150:]).cumprod() * cum_strat_in.iloc[-1]
cum_market = (1 + returns).cumprod()
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(13, 4))
ax1.hist(all_in_returns, bins=40, color='steelblue', alpha=0.7)
ax1.axvline(best_return, color='red', linestyle='--', label=f'「最优」参数 SMA({f},{s})')
ax1.set_title('样本内穷举:找到了「最优」参数', fontsize=12)
ax1.set_xlabel('策略累积收益')
ax1.legend()
ax1.grid(alpha=0.3)
ax2.plot(range(150), cum_strat_in.values, color='green', linewidth=1.5, label='样本内(用于寻参)')
ax2.plot(range(150, 300), cum_strat_out.values, color='red', linewidth=1.5, label='样本外(真实表现)')
ax2.plot(range(300), cum_market.values, color='gray', linewidth=1, linestyle='--', label='买入持有')
ax2.axvline(150, color='black', linestyle=':', linewidth=1.5, label='样本内/外分割点')
ax2.set_title(f'SMA({f},{s}) 过拟合演示\n样本内看起来很好,样本外回到起点', fontsize=11)
ax2.legend(fontsize=9)
ax2.grid(alpha=0.3)
plt.tight_layout()
plt.show()
print(' 注意:这是纯随机数据,不存在任何规律。')
print(' 参数数量和搜索次数增加后,历史回测可能得到异常好的结果,但样本外表现不一定能够保持。这就是过拟合。')
注意:这是纯随机数据,不存在任何规律。 参数数量和搜索次数增加后,历史回测可能得到异常好的结果,但样本外表现不一定能够保持。这就是过拟合。
import matplotlib.pyplot as plt
# 1. 设置系统自带的中文字体(这里使用黑体 SimHei)
plt.rcParams['font.sans-serif'] = ['SimHei'] # 如果你想用微软雅黑,可以改成 ['Microsoft YaHei']
# 2. 解决更换字体后,负号(-)显示为方块的问题
plt.rcParams['axes.unicode_minus'] = False
陷阱二:Look-ahead Bias(未来函数偏差)¶
这是回测中最常见、也最隐蔽的错误。
定义: 在生成交易信号时,不小心用了还没发生的数据。
举个栗子:
# 错误写法(Look-ahead Bias)
# 今天的信号用了今天的收盘价,但你在今天收盘后才知道收盘价
# 真实交易中,你在今天开盘时就要决定买不买
signal_today = rsi_today < 30 # 今天收盘计算的 RSI
position_today = signal_today # 错误:用当天收盘数据生成信号并按当天持仓计收益
# 正确写法(用 shift(1) 延迟一天)
signal_today = rsi.shift(1) < 30 # 昨天收盘计算的 RSI
position_today = signal_today # 今天开盘执行昨天的信号,这才合理
为什么难以发现? 代码可以正常运行,净值曲线也可能明显优于基线。异常高的夏普比率需要进一步检查数据对齐、信号滞后、成本和样本外稳定性,但不能仅凭一个阈值断定存在未来函数。
检查清单:
- 信号生成时用的数据,是否都是截止到昨天收盘的?
- 执行交易时,是否用了
shift(1)延迟信号? - 止损止盈是否用了当天的收盘价(而不能用高低价的中间某个价格)?
import yfinance as yf
# 演示 Look-ahead Bias 对回测结果的影响
data = yf.download('SPY', start='2020-01-01', end='2024-01-01', progress=False)
close = data['Close'].squeeze()
ret = close.pct_change()
# 用 RSI 为例
def rsi(price, period=14):
delta = price.diff()
gain = delta.clip(lower=0)
loss = -delta.clip(upper=0)
avg_g = gain.ewm(com=period-1, min_periods=period).mean()
avg_l = loss.ewm(com=period-1, min_periods=period).mean()
return 100 - 100/(1 + avg_g/avg_l)
rsi_val = rsi(close)
# 有 Look-ahead Bias 的版本(信号当天执行)
pos_biased = pd.Series(0.0, index=close.index)
pos_biased[rsi_val < 35] = 1.0 # 今天 RSI<35 → 今天就买入(用了今天才知道的数据)
pos_biased[rsi_val > 65] = -1.0
ret_biased = (pos_biased * ret).dropna()
# 正确版本(信号延迟一天执行)
pos_correct = pos_biased.shift(1).fillna(0) # 昨天的信号,今天才执行
ret_correct = (pos_correct * ret).dropna()
cum_b = (1 + ret_biased).cumprod()
cum_c = (1 + ret_correct).cumprod()
cum_m = (1 + ret.dropna()).cumprod()
fig, ax = plt.subplots(figsize=(13, 5))
ax.plot(cum_b.index, cum_b.values, label=f'有 Look-ahead Bias(夏普={ret_biased.mean()/ret_biased.std()*252**0.5:.2f})',
color='red', linewidth=2)
ax.plot(cum_c.index, cum_c.values, label=f'正确实现(夏普={ret_correct.mean()/ret_correct.std()*252**0.5:.2f})',
color='green', linewidth=2)
ax.plot(cum_m.index, cum_m.values, label='买入持有', color='gray', linewidth=1.5, linestyle='--')
ax.set_title('Look-ahead Bias 对回测结果的影响', fontsize=13)
ax.legend()
ax.grid(alpha=0.3)
plt.tight_layout()
plt.show()
print(' 两条线的差异,完全来自一个 shift(1)。')
print(' 没有 shift(1) 的版本使用了交易时点尚不可得的数据。')
两条线的差异,完全来自一个 shift(1)。 没有 shift(1) 的版本使用了交易时点尚不可得的数据。
# 默认使用固定样本,保证课程与 CI 不依赖实时接口。
USE_OFFLINE_DATA = True
if USE_OFFLINE_DATA:
from pathlib import Path
import sys
docs_root = next(parent for parent in [Path.cwd(), *Path.cwd().parents]
if (parent / 'utils' / 'offline_data.py').exists())
sys.path.insert(0, str(docs_root))
from utils.offline_data import configure_yfinance_offline
configure_yfinance_offline(yf)
else:
print('在线下载为扩展步骤;请核对数据字段、时区和下载日期。')
陷阱三:幸存者偏差(Survivorship Bias)¶
幸存者偏差示例: 只研究当前仍在指数中的公司,会遗漏破产、退市或被剔除的公司,从而高估历史策略表现。
在量化研究里:
- 如果你只分析当前还在标普500里的503只股票,你忽略了过去30年被踢出指数、甚至退市破产的数百只股票
- 这让历史数据天然「偏好」了最终成功的公司
- 用这种数据开发的策略,回测收益会被系统性高估
如何避免:
- 使用包含历史成分股(包括退市股票)的数据库
- 或者限定回测范围(只做指数成分股的策略,并用当时的成分股名单)
单只当前存续资产的示例同样可能存在样本选择问题。股票池策略还需要使用历史成分、退市证券和当时可得的证券状态。
5. 你的第一个「策略假设」应该长什么样?¶
好的策略假设要回答这三个问题:
当 [某个条件成立] 时,
买入 [某个资产],
因为 [某个行为/结构原因] 会导致 [预期的价格走向]。
例子:
| 假设 | 逻辑依据 |
|---|---|
| 「当 RSI < 30 时买入」 | 超卖说明市场过度恐慌,行为偏差→均值回归 |
| 「当 SMA20 上穿 SMA60 时买入」 | 短期动量超过长期趋势,趋势跟随 |
| 「当价格突破 52 周新高时买入」 | 新高被心理阻力压制,突破后解放筹码→趋势延续 |
| 「买最近 12 月涨幅最大的股票」 | 动量效应,机构资金流入有惯性 |
练习: 在下方 Markdown 单元格里,写下你自己的一个策略假设:
我的策略假设(在这里填写):
当 ________ 时, 买入/卖出 ________, 因为 ________ 会导致 ________。
小结¶
- 策略赚钱靠的是可持续的优势,不是运气
- 四种主要策略类型:趋势跟随、均值回归、跨截面动量、统计套利
- 每种策略都有适合和不适合的市场——没有万能策略
- 策略假设需要说明预期收益可能来自风险补偿、行为偏差还是市场摩擦
- 三大陷阱:过拟合、Look-ahead Bias、幸存者偏差
下一节 → 01_simple_backtest.ipynb(把「双均线趋势策略」完整实现一遍)
练习¶
- 回顾经典的「一月效应」,用代码验证 SPY 历史上 1 月份是否真的更容易上涨。
- 列举一个可能遭受「幸存者偏差」的研究场景,并说明如何避免它。
- 比较趋势跟随策略和均值回归策略在 2008 年、2020 年、2022 年的历史表现差异。
下一节 → 01_simple_backtest.ipynb