主题切换
4.8 合成数据回测
概念详解
回测最大的敌人是过拟合。历史数据只有一份,任何聪明的算法都能在"上帝视角"下挑出"看起来很好"的参数组合,但放到未来,这些参数大概率失效。合成数据回测(Synthetic Data Backtesting)是解决过拟合的一把关键武器——它能创造出任意多组"长得像真实数据"的虚拟价格序列,让策略在几百上千条虚拟时间线上反复测试,真正检验"参数是否捕获了某种可重复的规律"。
名词解释:合成数据
合成数据(Synthetic Data)是用统计模型(而非真实市场)生成的价格序列。合成数据有三个核心目的:(1)增加样本量,提升参数稳定性测试的可信度;(2)测试策略对极端市场(暴跌/暴涨/波动率突变)的鲁棒性;(3)避免过拟合历史噪声,因为合成数据的多样性远超历史。
名词解释:Monte Carlo 模拟
Monte Carlo(蒙特卡洛)模拟是用随机抽样方法估计复杂系统行为的数值方法。在量化回测中,主要指用大量随机价格路径估计策略绩效的分布(均值、方差、最大回撤分布、VaR 等),而不是只看一条历史曲线。
合成数据的 4 个层次
| 层次 | 模型 | 自由度 | 适用场景 |
|---|---|---|---|
| L1:基础随机游走 | 几何布朗运动 (GBM) | μ, σ | 入门测试、随机基准对比 |
| L2:随机波动率 | Heston | μ, σ_0, κ, θ, ξ, ρ | 波动率波动建模、波动率策略 |
| L3:跳跃扩散 | Jump Diffusion / Bates | L2 + (λ, μ_J, σ_J) | 黑天鹅事件、期权定价 |
| L4:综合模型 | 2-Factor Heston + Jump | L3 + 多因子 | 多因子策略、衍生品对冲 |
为什么要测试多重合成数据?
| 测试维度 | 单条历史曲线 | 多条合成曲线 |
|---|---|---|
| 过拟合风险 | 高(只看一次) | 低(看成百上千次) |
| 极端事件覆盖 | 受限于历史 | 可以强制覆盖(volatility shock) |
| 参数稳定性 | 难以判断 | 可视化参数高原 |
| 计算成本 | 低 | 高(每组需重跑回测) |
关键洞察:当你只测一条历史曲线却得到了很高的夏普,有两种可能:
- 你的策略真正发现了 alpha(罕见,发生概率 1/100 ~ 1/1000)。
- 你运气好,参数刚好匹配了这段历史的噪声(常见,即使随机策略也有 30% 概率做到)。
合成数据回测不能 100% 区分这两种情况,但能把"运气好"概率压到接近 0。
数学原理
1. 几何布朗运动 (GBM)
最基础的股票价格模型:
离散化形式(用于数值模拟):
其中
GBM 的局限性:
- 波动率常数(不变),但真实市场波动率是均值回归的
- 没有跳跃(连续轨迹),无法模拟闪崩/黑天鹅
2. Heston 随机波动率模型
允许波动率自身的随机过程:
参数含义:
:瞬时方差(波动的平方) : kappa,均值回归速度: theta,长期均值方差: xi,波动率的波动率(Vol of Vol): rho,价格波动与波动率的相关性(典型为负)
Heston 模型能生成"波动率集群"(volatility clustering),即大涨大跌后市继续动——这是真实市场最显著的特征之一。
3. Jump Diffusion 跳跃扩散
在 GBM 基础上加 Poisson 跳跃:
其中
典型参数(参考历史经验):
- 跳跃强度
: 年化 5-15 次(每年 5-15 次大事件) - 跳跃幅度均值
: -0.05(平均下跌 5%) - 跳跃幅度波动
: 0.08
4. Bates 模型(Heston + Jump)
Heston 与 Jump 的组合:
Bates 模型是衍生品定价(SVJ 期权)的标准模型,适合对冲基金级别的策略研究。
5. 统计特征验证
为判断合成数据"像不像"历史数据,常用以下统计量:
| 统计量 | 含义 | 历史样本 |
|---|---|---|
| 均值收益率 | 趋势 | 日均 0.05% |
| 波动率 | 风险 | 日 1.5% |
| 偏度 | 分布对称性 | 负偏(下跌多) |
| 峰度 | 厚尾性 | >3(厚尾) |
| 自相关(1阶) | 序列相关性 | 接近 0(弱式有效) |
| 波动率自相关 | 波动率聚集 | 0.3-0.5(强) |
| Hurst 指数 | 长记忆性 | 0.5(无) |
Python实战
📌 案例:4 种合成数据生成器 + 统计特征验证
下面是 4 种合成数据生成器的完整实现,加上历史数据 vs 合成数据的统计特征对比:
此处有展示代码展开 ▼
python
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from typing import Tuple, Dict
# 设置全局参数
np.random.seed(42)
N_DAYS = 2520 # 10 年日线
def gbm_generator(n_days: int = N_DAYS,
mu: float = 0.0004,
sigma: float = 0.02,
s0: float = 100.0) -> pd.Series:
"""L1: 几何布朗运动(GBM)
μ = 年化日收益 0.04%,σ = 年化日波动 31.6%
"""
daily_returns = np.random.normal(
loc=mu - 0.5 * sigma**2,
scale=sigma,
size=n_days
)
prices = s0 * np.exp(np.cumsum(daily_returns))
return pd.Series(prices, name='GBM')
def heston_generator(n_days: int = N_DAYS,
mu: float = 0.0004,
s0: float = 100.0,
v0: float = 0.04**2, # 初始年化方差
kappa: float = 2.0, # 均值回归速度
theta: float = 0.04**2, # 长期方差
xi: float = 0.3, # vol of vol
rho: float = -0.7) -> pd.Series:
"""L2: Heston 随机波动率模型
使用 Euler-Maruyama 离散化:
v_{t+1} = v_t + κ(θ - v_t)Δt + ξ √(v_t) √Δt Z_v
S_{t+1} = S_t exp((μ - 0.5v_t)Δt + √(v_t)Δt (ρ Z_v + √(1-ρ²) Z_s))
"""
dt = 1 / 252
sqrt_dt = np.sqrt(dt)
v = np.zeros(n_days)
s = np.zeros(n_days)
v[0] = v0
s[0] = s0
for t in range(1, n_days):
z_v = np.random.normal()
z_s = np.random.normal()
v[t] = v[t-1] + kappa * (theta - v[t-1]) * dt + xi * np.sqrt(max(v[t-1], 0)) * sqrt_dt * z_v
v[t] = max(v[t], 1e-8) # 保证方差非负
drift = (mu - 0.5 * v[t-1]) * dt
diffusion_s = np.sqrt(v[t-1] * dt) * (rho * z_v + np.sqrt(1 - rho**2) * z_s)
s[t] = s[t-1] * np.exp(drift + diffusion_s)
return pd.Series(s, name='Heston')
def jump_diffusion_generator(n_days: int = N_DAYS,
mu: float = 0.0004,
sigma: float = 0.015,
s0: float = 100.0,
jump_lambda: float = 0.05, # 年化 5% 概率
jump_mu: float = -0.05,
jump_sigma: float = 0.08) -> pd.Series:
"""L3: Jump Diffusion(Merton 模型)
在 GBM 基础上加 Poisson 跳跃,捕捉黑天鹅事件
"""
dt = 1 / 252
daily_returns = np.zeros(n_days)
for t in range(1, n_days):
# GBM 扩散部分
z = np.random.normal()
diffusion = (mu - 0.5 * sigma**2) * dt + sigma * np.sqrt(dt) * z
# 跳跃部分(以 Poisson 频率出现)
n_jumps = np.random.poisson(jump_lambda * dt)
if n_jumps > 0:
jumps = sum(np.random.normal(jump_mu, jump_sigma) for _ in range(n_jumps))
else:
jumps = 0
daily_returns[t] = diffusion + jumps
prices = s0 * np.exp(np.cumsum(daily_returns))
return pd.Series(prices, name='JumpDiff')
def bates_generator(n_days: int = N_DAYS,
mu: float = 0.0004,
s0: float = 100.0,
v0: float = 0.04**2,
kappa: float = 2.0,
theta: float = 0.04**2,
xi: float = 0.3,
rho: float = -0.7,
jump_lambda: float = 0.05,
jump_mu: float = -0.05,
jump_sigma: float = 0.08) -> pd.Series:
"""L4: Bates 模型(Heston + Jump,2-factor)
最高保真度:同时支持随机波动率和跳跃
"""
dt = 1 / 252
sqrt_dt = np.sqrt(dt)
v = np.zeros(n_days)
s = np.zeros(n_days)
v[0] = v0
s[0] = s0
for t in range(1, n_days):
z_v = np.random.normal()
# Heston 波动更新
v[t] = v[t-1] + kappa * (theta - v[t-1]) * dt + xi * np.sqrt(max(v[t-1], 0)) * sqrt_dt * z_v
v[t] = max(v[t], 1e-8)
# 价格更新 + 跳跃
z_s = np.random.normal()
n_jumps = np.random.poisson(jump_lambda * dt)
jump_sum = sum(np.random.normal(jump_mu, jump_sigma) for _ in range(n_jumps)) if n_jumps else 0
drift = (mu - 0.5 * v[t-1]) * dt
diffusion_s = np.sqrt(v[t-1] * dt) * (rho * z_v + np.sqrt(1 - rho**2) * z_s)
s[t] = s[t-1] * np.exp(drift + diffusion_s + jump_sum)
return pd.Series(s, name='Bates')
def compute_statistics(prices: pd.Series) -> Dict[str, float]:
"""计算一组价格序列的核心统计特征"""
returns = prices.pct_change().dropna()
vol = prices.diff().apply(np.log).dropna().std() * np.sqrt(252)
return {
'均值(年化)': f"{returns.mean() * 252 * 100:.2f}%",
'波动(年化)': f"{vol * 100:.2f}%",
'偏度': f"{returns.skew():.2f}",
'峰度': f"{returns.kurtosis():.2f}",
'最大回撤': f"{(prices / prices.cummax() - 1).min() * 100:.2f}%",
'夏普(0%)': f"{(returns.mean() / returns.std()) * np.sqrt(252):.2f}",
}
# === 演示运行 ===
print("=" * 60)
print("4 种合成数据生成器性能测试")
print("=" * 60)
gbm_series = gbm_generator()
print(f"GBM 生成 {len(gbm_series)} 个点:OK")
heston_series = heston_generator()
print(f"Heston 生成 {len(heston_series)} 个点:OK")
jd_series = jump_diffusion_generator()
print(f"JumpDiff 生成 {len(jd_series)} 个点:OK")
bates_series = bates_generator()
print(f"Bates 生成 {len(bates_series)} 个点:OK")
print("\n" + "=" * 60)
print("统计特征对比")
print("=" * 60)
stats = {
'GBM': compute_statistics(gbm_series),
'Heston': compute_statistics(heston_series),
'JumpDiff': compute_statistics(jd_series),
'Bates': compute_statistics(bates_series),
}
stats_df = pd.DataFrame(stats).T
print(stats_df)点击展开可浏览运行结果
============================================================
4 种合成数据生成器性能测试
============================================================
GBM 生成 2520 个点:OK
Heston 生成 2520 个点:OK
JumpDiff 生成 2520 个点:OK
Bates 生成 2520 个点:OK
============================================================
统计特征对比
============================================================
均值(年化) 波动(年化) 偏度 峰度 最大回撤 夏普(0%)
GBM 26.43% 2239.34% 0.09 0.06 -44.08% 0.84
Heston -1.92% 3849.87% 0.05 13.93 -27.01% -0.35
JumpDiff -0.89% 1742.96% -44.07 2117.84 -17.55% -0.17
Bates -1.83% 3879.80% 0.09 7.32 -33.53% -0.17📌 案例:双均线策略在 4 种数据上的对比
下面是双均线策略在 4 种合成数据 + 100 组 Monte Carlo 路径上的对比——这是检验策略稳健性的标准方法:
此处有展示代码展开 ▼
python
def double_ma_strategy(prices: pd.Series,
fast: int = 10,
slow: int = 50) -> pd.Series:
"""简单双均线策略:返回每日持仓(1=持仓,0=空仓)"""
fast_ma = prices.rolling(fast).mean()
slow_ma = prices.rolling(slow).mean()
signal = (fast_ma > slow_ma).astype(int).shift(1).fillna(0)
return signal
def backtest_with_costs(prices: pd.Series, signal: pd.Series,
fee_rate: float = 0.0003,
slippage: float = 0.0005) -> Dict[str, float]:
"""带交易成本的简单回测"""
returns = prices.pct_change().fillna(0)
positions = signal
# 策略收益
strategy_returns = returns * positions
# 换手率(每根 K 线仓位变化的绝对值除以 2 = 实际交易)
turnover = positions.diff().abs().fillna(positions.iloc[0]) / 2
# 扣除交易成本(单边费率:佣金+滑点)
cost_returns = turnover * (fee_rate + slippage) * 2
strategy_returns = strategy_returns - cost_returns
# 计算绩效
equity = (1 + strategy_returns).cumprod()
total_return = equity.iloc[-1] - 1
annual_return = (1 + total_return) ** (252 / len(equity)) - 1
annual_vol = strategy_returns.std() * np.sqrt(252)
sharpe = annual_return / annual_vol if annual_vol > 0 else 0
max_dd = (equity / equity.cummax() - 1).min()
return {
'total_return': total_return,
'annual_return': annual_return,
'annual_vol': annual_vol,
'sharpe': sharpe,
'max_drawdown': max_dd,
'turnover': turnover.sum() / len(turnover),
}
def monte_carlo_test(generator_func, **gen_kwargs):
"""Monte Carlo 测试:用 100 组合成数据测策略稳健性"""
n_simulations = 100
results = []
for sim in range(n_simulations):
prices = generator_func(**gen_kwargs)
signal = double_ma_strategy(prices, fast=10, slow=50)
result = backtest_with_costs(prices, signal)
results.append(result)
results_df = pd.DataFrame(results)
summary = {
'夏普 - 均值': results_df['sharpe'].mean(),
'夏普 - 中位数': results_df['sharpe'].median(),
'夏普 - 标准差': results_df['sharpe'].std(),
'夏普 - 最小': results_df['sharpe'].min(),
'夏普 - 最大': results_df['sharpe'].max(),
'夏普 - 5%分位数': results_df['sharpe'].quantile(0.05),
'夏普 - 95%分位数': results_df['sharpe'].quantile(0.95),
'胜率(>0)': (results_df['total_return'] > 0).mean(),
'平均最大回撤': results_df['max_drawdown'].mean(),
}
return summary, results_df
print("\n" + "=" * 70)
print("Monte Carlo 仿真:双均线策略在 4 种合成数据上的稳健性")
print("=" * 70)
summary, _ = monte_carlo_test(gbm_generator)
print("\nGBM (100 组仿真):")
for k, v in summary.items():
print(f" {k}: {v:.3f}")
summary, _ = monte_carlo_test(heston_generator)
print("\nHeston (100 组仿真):")
for k, v in summary.items():
print(f" {k}: {v:.3f}")
summary, _ = monte_carlo_test(jump_diffusion_generator)
print("\nJumpDiff (100 组仿真):")
for k, v in summary.items():
print(f" {k}: {v:.3f}")
summary, _ = monte_carlo_test(bates_generator)
print("\nBates (100 组仿真):")
for k, v in summary.items():
print(f" {k}: {v:.3f}")点击展开可浏览运行结果
📘 本段代码定义了 3 个函数/类:函数 `double_ma_strategy`(简单双均线策略:返回每日持仓(1=持仓,0=空仓))、函数 `backtest_with_costs`(带交易成本的简单回测)、函数 `monte_carlo_test`(Monte Carlo 测试:用 100 组合成数据测策略稳健性)。该片段为教学展示(未包含独立运行的输入数据),可在实战练习中结合真实数据调用。
📌 案例:合成数据回测与历史数据回测的对比
此处有展示代码展开 ▼
python
def verify_synthetic_vs_historical():
"""验证合成数据的统计特征是否匹配历史数据"""
# 1. 加载一段真实历史数据(以 SPY 为例,2014-2024)
# 这里模拟一段历史数据(实际使用时用 yfinance 或 tushare)
np.random.seed(123)
n_days = 2520
dates = pd.bdate_range('2014-01-01', periods=n_days)
# 模拟 SPY 的真实数据(均值 8% 年化,波动 15%)
hist_returns = np.random.normal(0.0004, 0.0098, n_days)
hist_prices = 200 * np.exp(np.cumsum(hist_returns))
hist_series = pd.Series(hist_prices, index=dates, name='SPY_Historical')
# 2. 用相同参数生成 100 组合成数据
n_synthetic = 100
synth_stats = {k: [] for k in ['ann_return', 'ann_vol', 'max_dd']}
for i in range(n_synthetic):
synth = heston_generator(
n_days=n_days, mu=0.0004, v0=0.015**2,
kappa=2.0, theta=0.015**2, xi=0.3, rho=-0.7
)
stats = compute_statistics(synth)
synth_stats['ann_return'].append(float(stats['均值(年化)'].rstrip('%')) / 100)
synth_stats['ann_vol'].append(float(stats['波动(年化)'].rstrip('%')) / 100)
synth_stats['max_dd'].append(float(stats['最大回撤'].rstrip('%')) / 100)
# 3. 比较
hist_stats = compute_statistics(hist_series)
print("=" * 50)
print("历史数据 vs Heston 合成数据对比")
print("=" * 50)
print(f"{'指标':<15} {'历史':>12} {'合成均值':>12} {'合成标准差':>12}")
print("-" * 50)
print(f"{'年化收益':<15} {hist_stats['均值(年化)']:>12} "
f"{np.mean(synth_stats['ann_return'])*100:>10.2f}% "
f"{np.std(synth_stats['ann_return'])*100:>10.2f}%")
print(f"{'年化波动':<15} {hist_stats['波动(年化)']:>12} "
f"{np.mean(synth_stats['ann_vol'])*100:>10.2f}% "
f"{np.std(synth_stats['ann_vol'])*100:>10.2f}%")
print(f"{'最大回撤':<15} {hist_stats['最大回撤']:>12} "
f"{np.mean(synth_stats['max_dd'])*100:>10.2f}% "
f"{np.std(synth_stats['max_dd'])*100:>10.2f}%")
print("\n结论:合成数据统计特征应与历史数据在均值±2σ范围内。")
print(" 如果差异过大,需要重新拟合模型参数。")
verify_synthetic_vs_historical()点击展开可浏览运行结果
📘 本段代码定义了 1 个函数/类:函数 `verify_synthetic_vs_historical`(验证合成数据的统计特征是否匹配历史数据)。该片段为教学展示(未包含独立运行的输入数据),可在实战练习中结合真实数据调用。
📌 案例:压力测试(Stress Testing)
合成数据最大的价值之一是构造压力场景——这在历史数据中无法直接观测:
此处有展示代码展开 ▼
python
def stress_test_scenarios():
"""用合成数据构造历史未发生的极端场景"""
scenarios = {
'正常市场(基线)': {
'mu': 0.0004, 'sigma': 0.015,
'jump_lambda': 0.0,
},
'高波动率(震荡市)': {
'mu': 0.0002, 'sigma': 0.03,
'jump_lambda': 0.0,
},
'低波动率(慢牛市)': {
'mu': 0.0006, 'sigma': 0.008,
'jump_lambda': 0.0,
},
'频繁黑天鹅': {
'mu': 0.0003, 'sigma': 0.012,
'jump_lambda': 0.20, # 年化 20% 概率
'jump_mu': -0.08, # 平均跌 8%
'jump_sigma': 0.05,
},
'单日熔断': {
'mu': 0.0003, 'sigma': 0.012,
'jump_lambda': 0.50, # 极高跳跃频率
'jump_mu': -0.15, # 单日 -15%
'jump_sigma': 0.05,
},
}
print("=" * 65)
print(f"{'场景':<20} {'夏普':>8} {'最大回撤':>10} {'年化收益':>10}")
print("-" * 65)
for name, params in scenarios.items():
# 每场景跑 50 次 Monte Carlo
sharpes, dds, rets = [], [], []
for _ in range(50):
prices = jump_diffusion_generator(**params)
signal = double_ma_strategy(prices)
res = backtest_with_costs(prices, signal)
sharpes.append(res['sharpe'])
dds.append(res['max_drawdown'])
rets.append(res['annual_return'])
print(f"{name:<20} {np.mean(sharpes):>8.2f} "
f"{np.mean(dds):>10.2%} {np.mean(rets):>10.2%}")
print("\n解读:")
print("- 策略在不同市场环境下表现差异极大")
print("- 如果某场景下最大回撤>30%,实盘应主动降仓位")
stress_test_scenarios()点击展开可浏览运行结果
📘 本段代码定义了 1 个函数/类:函数 `stress_test_scenarios`(用合成数据构造历史未发生的极端场景)。该片段为教学展示(未包含独立运行的输入数据),可在实战练习中结合真实数据调用。
常见误区
"合成数据=预测未来":错。合成数据只是用现有统计特征生成"合理"的虚拟路径,不保证未来真的这样发生。它的价值是测策略稳健性,不是预测具体路径。
"GBM 足够好":大多数情况不够。GBM 无法模拟波动率聚集(真实市场最显著的特征),所以双均线等"波动率敏感"策略在 GBM 上测试会高估表现。
"参数应该匹配历史完美":错。如果合成数据的统计特征与历史 100% 匹配,反而说明你过拟合了历史——真实的未来统计特征会有偏离。允许 5-10% 的偏离。
"100 组仿真太少":100 组是入门,认真做应该 1000-10000 组。每组仿真也需要 1000+ 天才有统计显著性。
"跳跃只在 Heston 上加":可以。也可以直接在 GBM 上加跳跃(就是 Jump Diffusion 模型),不需要先学 Heston。
"只看 Sharpe 均值":错。夏普的分布比夏普均值更关键——均值 1.5 但 5% 分位 -0.5 的策略,可能比均值 0.8 但 5% 分位 0.5 的策略更危险。
"合成数据能解决过拟合":错。它只能降低过拟合概率,不能消除过拟合。真正解决过拟合还需要:严格的样本外测试、参数稳健性分析、理论逻辑支撑。
合成数据 vs 历史数据 互动对比表
| 维度 | 单一历史曲线 | 100 组合成(MC 测试) | 1000 组合成(压力测试) |
|---|---|---|---|
| 过拟合风险 | ★★★★ 极高 | ★★ 低 | ★ 很低 |
| 极端事件覆盖 | ★ 受限 | ★★★ 中 | ★★★★★ 全 |
| 计算成本 | 极低 | 中等 | 高 |
| 实现复杂度 | 低 | 中 | 高 |
| 参数稳健性测试 | 无法测 | 可看分布 | 充分测 |
| 解释力 | 真实但窄 | 统计性 | 统计性 |
| 推荐使用时机 | 教学 / 概念验证 | 策略稳健性初筛 | 上线前最终验证 |
色块含义:
- 绿色:在该维度上表现优秀/风险低
- 黄色:表现一般/中等风险
- 红色:表现较差/风险高
小测验
题目 1:为什么合成数据能"防止过拟合"?
A. 因为合成数据比真实数据更准确 B. 因为它生成了很多种"合理"的虚拟路径,参数稳定才算真有效 C. 因为它永远不会出现与历史不同的走势 D. 因为它是机器学习算法生成的数据
查看答案与解析
答案:B
合成数据通过 Monte Carlo 生成大量路径,让策略在几百上千次随机现实中测试。如果策略在多数路径上都能保持正向夏普,说明参数确实捕获了某种规律(而非噪声)。
A 错:合成数据并不"更准确",只是更多样。 C 错:合成数据可以且应该偏离历史。 D 错:合成数据是统计模型生成的,与机器学习算法无关。
题目 2:Heston 模型相比 GBM 最大的进步是什么?
A. 预测未来价格更准确 B. 模拟波动率自身的随机过程(波动率聚集) C. 计算速度更快 D. 实现代码更短
查看答案与解析
答案:B
Heston 用一个独立的随机微分方程建模"波动率的波动率",能生成"动量-反弹"风格的真实市场特征(波动率聚集、均值回归)。
A 错:Heston 是仿真模型,不做预测。 C 错:Heston 反而比 GBM 慢(每步多一个 SDE)。 D 错:Heston 代码更长。
题目 3:对双均线策略,在哪种合成数据上的测试最严苛?
A. GBM(波动率恒定) B. Heston(随机波动率) C. Jump Diffusion(罕见跳跃) D. Bates(Heston + Jump)
查看答案与解析
答案:D
Bates 模型同时包含随机波动率和跳跃,覆盖最多市场场景。在 Heston + Jump 上都稳健的策略,在真实市场中相对最可靠。GBM 是最简单的"理想化"环境,在 GBM 上效果好说明策略在"理想环境"下有效,但不能保证在"复杂现实"中仍有效。
实战练习
实现 4 种数据生成器并对比统计特征:完成本节的 4 个生成器,生成 1000 天数据,用
compute_statistics比较四组的均值、波动率、偏度、峰度、最大回撤。画出 4 条价格曲线叠加图。Monte Carlo 测试双均线稳健性:对双均线策略 (fast=5/10/20,slow=30/50/100),在 Heston 合成数据上做 9 组参数 × 100 次 Monte Carlo 测试,输出每组参数对应的"夏普分布箱型图"。
构造 3 种压力场景:用 Jump Diffusion 设计"黑天鹅场景"——例如"1 年内连续 5 次 -8% 大跌"。在该场景下用双均线策略测试,观察最大回撤和净值曲线的形态。
参数校准:用 Tushare 拉取 SPY/000001.SH 的 2015-2024 年日线数据,统计年化均值/波动率/偏度/峰度,然后用 Heston 模型生成相同统计特征的合成数据(参数最优化:找出一组 κ/θ/ξ/ρ 使合成数据与历史数据最匹配)。
构建"夏普分布风控":在你的策略中添加一个判断:在 100 组合成路径上,如果 5% 分位的夏普 < 0.5,自动禁止该策略实盘。
延伸阅读
- Advances in Financial Machine Learning — Marcos López de Prado, 第 12 章详细讨论如何用合成数据测试策略稳健性。
- Hull, J. (2017). Options, Futures, and Other Derivatives. 第 28-29 章:完整的 GBM / Heston / Jump Diffusion 推导。
- Stochastic Volatility Modeling — Lorenzo Bergomi, 第 3-5 章:随机波动率模型的实践。
- Gatheral, J. (2006). The Volatility Surface — Heston 模型在衍生品市场的实战应用。
- QuantStack
quantstats: https://github.com/ranaroussi/quantstats —— 可视化合成 vs 历史绩效对比的便捷工具。 - NumPy 官方文档:https://numpy.org/doc/stable/reference/random/ —— 多维正态随机抽样的最佳实践。
本章要点
- 合成数据回测是反过拟合的核心工具:它不能预测未来,但能让策略在成百上千种"合理"虚拟路径上反复测试,过滤掉"纯运气"。
- 4 个层次的合成模型:GBM(基础)→ Heston(随机波动率) → Jump Diffusion(黑天鹅) → Bates(综合)。复杂度递增,真实性也递增。
- 统计特征验证不可少:均值、波动率、偏度、峰度、最大回撤——合成数据要在这些维度上与历史数据匹配(允许 5-10% 偏离)。
- Monte Carlo 仿真是工业标准:100 组入门,1000 组认真做,10000 组上线前。夏普分布比夏普均值更关键——要看 5% / 95% 分位数。
- 压力测试补充历史盲区:历史数据没有熔断级别的单日暴跌,合成数据可以构造。但要保持"合理"——跳跃 15% 单日已经极端。
- 合成数据不是"完美替代历史":它只是工具。真正解决过拟合还需要:严格样本外、参数稳健性、理论逻辑支撑。
- 风险点:不要把合成数据当成"预测未来",更不要因为在某模型(如 GBM)上表现好就盲目相信。永远要看分布,不要看单点。