Skip to content

4.8 合成数据回测

概念详解

回测最大的敌人是过拟合。历史数据只有一份,任何聪明的算法都能在"上帝视角"下挑出"看起来很好"的参数组合,但放到未来,这些参数大概率失效。合成数据回测(Synthetic Data Backtesting)是解决过拟合的一把关键武器——它能创造出任意多组"长得像真实数据"的虚拟价格序列,让策略在几百上千条虚拟时间线上反复测试,真正检验"参数是否捕获了某种可重复的规律"。

名词解释:合成数据

合成数据(Synthetic Data)是用统计模型(而非真实市场)生成的价格序列。合成数据有三个核心目的:(1)增加样本量,提升参数稳定性测试的可信度;(2)测试策略对极端市场(暴跌/暴涨/波动率突变)的鲁棒性;(3)避免过拟合历史噪声,因为合成数据的多样性远超历史。

名词解释:Monte Carlo 模拟

Monte Carlo(蒙特卡洛)模拟是用随机抽样方法估计复杂系统行为的数值方法。在量化回测中,主要指用大量随机价格路径估计策略绩效的分布(均值、方差、最大回撤分布、VaR 等),而不是只看一条历史曲线。

合成数据的 4 个层次

层次模型自由度适用场景
L1:基础随机游走几何布朗运动 (GBM)μ, σ入门测试、随机基准对比
L2:随机波动率Hestonμ, σ_0, κ, θ, ξ, ρ波动率波动建模、波动率策略
L3:跳跃扩散Jump Diffusion / BatesL2 + (λ, μ_J, σ_J)黑天鹅事件、期权定价
L4:综合模型2-Factor Heston + JumpL3 + 多因子多因子策略、衍生品对冲

为什么要测试多重合成数据?

测试维度单条历史曲线多条合成曲线
过拟合风险高(只看一次)低(看成百上千次)
极端事件覆盖受限于历史可以强制覆盖(volatility shock)
参数稳定性难以判断可视化参数高原
计算成本高(每组需重跑回测)

关键洞察:当你只测一条历史曲线却得到了很高的夏普,有两种可能:

  1. 你的策略真正发现了 alpha(罕见,发生概率 1/100 ~ 1/1000)。
  2. 你运气好,参数刚好匹配了这段历史的噪声(常见,即使随机策略也有 30% 概率做到)。

合成数据回测不能 100% 区分这两种情况,但能把"运气好"概率压到接近 0。

数学原理

1. 几何布朗运动 (GBM)

最基础的股票价格模型:

dSt=μStdt+σStdWt

离散化形式(用于数值模拟):

St+Δt=Stexp((μ12σ2)Δt+σΔtZt)

其中 ZtN(0,1) 是标准正态。

GBM 的局限性:

  • 波动率常数(不变),但真实市场波动率是均值回归的
  • 没有跳跃(连续轨迹),无法模拟闪崩/黑天鹅

2. Heston 随机波动率模型

允许波动率自身的随机过程:

{dSt=μStdt+vtStdWtSdvt=κ(θvt)dt+ξvtdWtvdWSdWv=ρdt

参数含义:

  • vt:瞬时方差(波动的平方)
  • κ:kappa,均值回归速度
  • θ:theta,长期均值方差
  • ξ:xi,波动率的波动率(Vol of Vol)
  • ρ:rho,价格波动与波动率的相关性(典型为负)

Heston 模型能生成"波动率集群"(volatility clustering),即大涨大跌后市继续动——这是真实市场最显著的特征之一。

3. Jump Diffusion 跳跃扩散

在 GBM 基础上加 Poisson 跳跃:

dSt=μStdt+σStdWt+St(eJt1)dNt

其中 Nt 是强度为 λ 的 Poisson 过程,跳跃幅度 JtN(μJ,σJ2)

典型参数(参考历史经验):

  • 跳跃强度 λ: 年化 5-15 次(每年 5-15 次大事件)
  • 跳跃幅度均值 μJ: -0.05(平均下跌 5%)
  • 跳跃幅度波动 σJ: 0.08

4. Bates 模型(Heston + Jump)

Heston 与 Jump 的组合:

{dSt=μStdt+vtStdWtS+St(eJt1)dNtdvt=κ(θvt)dt+ξvtdWtv

Bates 模型是衍生品定价(SVJ 期权)的标准模型,适合对冲基金级别的策略研究。

5. 统计特征验证

为判断合成数据"像不像"历史数据,常用以下统计量:

统计量含义历史样本
均值收益率趋势日均 0.05%
波动率风险日 1.5%
偏度分布对称性负偏(下跌多)
峰度厚尾性>3(厚尾)
自相关(1阶)序列相关性接近 0(弱式有效)
波动率自相关波动率聚集0.3-0.5(强)
Hurst 指数长记忆性0.5(无)

Python实战

📌 案例:4 种合成数据生成器 + 统计特征验证

下面是 4 种合成数据生成器的完整实现,加上历史数据 vs 合成数据的统计特征对比:

PYTHON174 行 · 5.7 KB
📄此处有展示代码174 行 · 5.7 KB展开 ▼
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 路径上的对比——这是检验策略稳健性的标准方法:

PYTHON94 行 · 3.3 KB
📄此处有展示代码94 行 · 3.3 KB展开 ▼
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 组合成数据测策略稳健性)。该片段为教学展示(未包含独立运行的输入数据),可在实战练习中结合真实数据调用。

📌 案例:合成数据回测与历史数据回测的对比

PYTHON49 行 · 2.2 KB
📄此处有展示代码49 行 · 2.2 KB展开 ▼
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)

合成数据最大的价值之一是构造压力场景——这在历史数据中无法直接观测:

PYTHON53 行 · 1.7 KB
📄此处有展示代码53 行 · 1.7 KB展开 ▼
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`(用合成数据构造历史未发生的极端场景)。该片段为教学展示(未包含独立运行的输入数据),可在实战练习中结合真实数据调用。

常见误区

  1. "合成数据=预测未来":错。合成数据只是用现有统计特征生成"合理"的虚拟路径,不保证未来真的这样发生。它的价值是测策略稳健性,不是预测具体路径。

  2. "GBM 足够好":大多数情况不够。GBM 无法模拟波动率聚集(真实市场最显著的特征),所以双均线等"波动率敏感"策略在 GBM 上测试会高估表现。

  3. "参数应该匹配历史完美":错。如果合成数据的统计特征与历史 100% 匹配,反而说明你过拟合了历史——真实的未来统计特征会有偏离。允许 5-10% 的偏离。

  4. "100 组仿真太少":100 组是入门,认真做应该 1000-10000 组。每组仿真也需要 1000+ 天才有统计显著性。

  5. "跳跃只在 Heston 上加":可以。也可以直接在 GBM 上加跳跃(就是 Jump Diffusion 模型),不需要先学 Heston。

  6. "只看 Sharpe 均值":错。夏普的分布比夏普均值更关键——均值 1.5 但 5% 分位 -0.5 的策略,可能比均值 0.8 但 5% 分位 0.5 的策略更危险。

  7. "合成数据能解决过拟合":错。它只能降低过拟合概率,不能消除过拟合。真正解决过拟合还需要:严格的样本外测试、参数稳健性分析、理论逻辑支撑。

合成数据 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 上效果好说明策略在"理想环境"下有效,但不能保证在"复杂现实"中仍有效。

实战练习

  1. 实现 4 种数据生成器并对比统计特征:完成本节的 4 个生成器,生成 1000 天数据,用 compute_statistics 比较四组的均值、波动率、偏度、峰度、最大回撤。画出 4 条价格曲线叠加图。

  2. Monte Carlo 测试双均线稳健性:对双均线策略 (fast=5/10/20,slow=30/50/100),在 Heston 合成数据上做 9 组参数 × 100 次 Monte Carlo 测试,输出每组参数对应的"夏普分布箱型图"。

  3. 构造 3 种压力场景:用 Jump Diffusion 设计"黑天鹅场景"——例如"1 年内连续 5 次 -8% 大跌"。在该场景下用双均线策略测试,观察最大回撤和净值曲线的形态。

  4. 参数校准:用 Tushare 拉取 SPY/000001.SH 的 2015-2024 年日线数据,统计年化均值/波动率/偏度/峰度,然后用 Heston 模型生成相同统计特征的合成数据(参数最优化:找出一组 κ/θ/ξ/ρ 使合成数据与历史数据最匹配)。

  5. 构建"夏普分布风控":在你的策略中添加一个判断:在 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)上表现好就盲目相信。永远要看分布,不要看单点。