【Python量化策略评估体系 #05】极端行情扛得住吗?2008/2015/2020 三场景压力测试

回测曲线一路向东北,夏普 1.8,你很满意。但放进 2008 年会怎样?2015 年连续跌停呢?2020 年疫情那种 V 型急跌呢?

不在极端行情里测过的策略,上线就是裸奔。

压力测试不是看历史最大回撤那么简单。本文教你用蒙特卡洛 + 三场景冲击,量化组合在极端行情下的尾部风险。

一、本文你将得到什么

  1. 三种典型极端场景的冲击设计:2008 金融危机、2015 股灾、2020 疫情。
  2. 蒙特卡洛模拟 1000 条路径,输出中位数、5% 分位、最大回撤、VaR、ES。
  3. 一段可直接运行的 Python 脚本,输入你的策略收益率就能跑压力测试。

二、为什么做压力测试

历史回测只告诉你「过去发生过什么」,压力测试告诉你「如果再次发生会怎样」:

  • 2008:系统性暴跌 + 流动性枯竭,资产相关性趋近于 1。
  • 2015:杠杆断裂 + 连续跌停,波动率和左尾同时放大。
  • 2020:突发黑天鹅 + 政策救市,急跌后快速反弹,V 型走势。

一个策略如果在三种场景下都死不掉,才有资格谈稳健。

三、压力测试设计思路

我们用历史日收益率分布作为「基础分布」,然后叠加三类冲击:

  1. 均值下挪:整体收益环境变差。
  2. 波动放大:标准差乘 2~3 倍。
  3. 结构冲击 :
    • 2008:随机加入 -5% 跳空(左尾)。
    • 2015:前 5 日连续 -7%。
    • 2020:前 1/3 急跌、中 1/3 震荡、后 1/3 反弹。

每个场景模拟 1000 条路径,观察组合净值分布。

四、代码实战

python 复制代码
import json
import random
import numpy as np
from mairui import Client

DEMO_LICENCE = "LICENCE-66D8-9F96-0C7F0FBCD073"  # 演示证书,请换成你自己的正式证书
CODE = "600519"
INIT_NAV = 100000.0   # 初始组合净值
POSITION_RATIO = 0.6  # 股票仓位 60%


def perturb_ohlc(bars, seed=2026091305):
    rng = random.Random(seed)
    out = []
    for b in bars:
        k = 1.0 + (rng.random() - 0.5) * 0.10
        nb = dict(b)
        for f in ("o", "c", "h", "l"):
            if f in nb and nb[f] is not None:
                nb[f] = round(float(nb[f]) * k, 2)
        out.append(nb)
    return out


def max_drawdown(nav_path):
    peak = np.maximum.accumulate(nav_path)
    dd = (nav_path - peak) / peak
    return float(dd.min())


def var_es(returns, alpha=0.05):
    sorted_r = np.sort(returns)
    idx = int(alpha * len(sorted_r))
    var = sorted_r[idx]
    es = sorted_r[:idx].mean()
    return float(var), float(es)


def scenario_paths(base_returns, scenario, n_path=1000):
    r = base_returns.copy()
    n = len(r)
    paths = []

    if scenario == "crisis_2008":
        mu_shift = -0.003
        vol_mult = 3.0
        for _ in range(n_path):
            shocks = np.random.normal(mu_shift, r.std() * vol_mult, n)
            jumps = np.random.rand(n) < 0.05
            shocks[jumps] -= 0.05
            nav = INIT_NAV * np.cumprod(1 + shocks * POSITION_RATIO)
            paths.append(nav)

    elif scenario == "crash_2015":
        for _ in range(n_path):
            shocks = np.random.normal(-0.002, r.std() * 2.5, n)
            if n >= 5:
                shocks[:5] = -0.07
            nav = INIT_NAV * np.cumprod(1 + shocks * POSITION_RATIO)
            paths.append(nav)

    elif scenario == "pandemic_2020":
        for _ in range(n_path):
            shocks = np.random.normal(0, r.std() * 2.0, n)
            t1, t2 = n // 3, 2 * n // 3
            shocks[:t1] += np.linspace(-0.04, -0.01, t1)
            shocks[t1:t2] += np.random.normal(0, 0.015, t2 - t1)
            shocks[t2:] += np.linspace(0.01, 0.04, n - t2)
            nav = INIT_NAV * np.cumprod(1 + shocks * POSITION_RATIO)
            paths.append(nav)

    return np.array(paths)


def summarize(paths):
    final_nav = paths[:, -1]
    total_returns = (final_nav - INIT_NAV) / INIT_NAV
    mdds = [max_drawdown(p) for p in paths]
    daily_returns = np.diff(paths, axis=1).flatten() / paths[:, :-1].flatten()
    var, es = var_es(daily_returns, 0.05)
    return {
        "期末净值中位数": round(float(np.median(final_nav)), 2),
        "期末净值 5% 分位": round(float(np.percentile(final_nav, 5)), 2),
        "总收益率中位数": f"{float(np.median(total_returns)) * 100:.2f}%",
        "总收益率 5% 分位": f"{float(np.percentile(total_returns, 5)) * 100:.2f}%",
        "最大回撤中位数": f"{float(np.median(mdds)) * 100:.2f}%",
        "最大回撤 95% 分位": f"{float(np.percentile(mdds, 95)) * 100:.2f}%",
        "日收益 VaR(5%)": f"{var * 100:.2f}%",
        "日收益 ES(5%)": f"{es * 100:.2f}%",
    }


np.random.seed(20260913)
client = Client(DEMO_LICENCE)
bars = perturb_ohlc(client.stock_history(code=CODE, period="d", dividend="f"))
closes = np.array([float(b["c"]) for b in bars])
base_returns = np.diff(closes) / closes[:-1]

scenarios = {
    "2008 金融危机(系统性暴跌 + 左尾跳空)": "crisis_2008",
    "2015 股灾(连续跌停 + 高波动)": "crash_2015",
    "2020 疫情(V 型急跌反弹)": "pandemic_2020",
}

result = {
    "基础日收益统计": {
        "样本数": int(len(base_returns)),
        "均值": f"{float(base_returns.mean()) * 100:.4f}%",
        "标准差": f"{float(base_returns.std()) * 100:.4f}%",
    },
    "压力测试结果": {},
}

for name, key in scenarios.items():
    paths = scenario_paths(base_returns, key, n_path=1000)
    result["压力测试结果"][name] = summarize(paths)

print(json.dumps(result, ensure_ascii=False, indent=2))

五、运行结果(演示环境)

json 复制代码
{
  "基础日收益统计": {
    "样本数": 49,
    "均值": "0.0936%",
    "标准差": "3.8243%"
  },
  "压力测试结果": {
    "2008 金融危机(系统性暴跌 + 左尾跳空)": {
      "期末净值中位数": 75266.25,
      "期末净值 5% 分位": 34923.93,
      "总收益率中位数": "-24.73%",
      "总收益率 5% 分位": "-65.08%",
      "最大回撤中位数": "-46.00%",
      "最大回撤 95% 分位": "-22.86%",
      "日收益 VaR(5%)": "-11.75%",
      "日收益 ES(5%)": "-14.64%"
    },
    "2015 股灾(连续跌停 + 高波动)": {
      "期末净值中位数": 72580.76,
      "期末净值 5% 分位": 38035.42,
      "总收益率中位数": "-27.42%",
      "总收益率 5% 分位": "-61.96%",
      "最大回撤中位数": "-38.51%",
      "最大回撤 95% 分位": "-20.64%",
      "日收益 VaR(5%)": "-9.27%",
      "日收益 ES(5%)": "-11.69%"
    },
    "2020 疫情(V 型急跌反弹)": {
      "期末净值中位数": 98450.61,
      "期末净值 5% 分位": 58464.43,
      "总收益率中位数": "-1.55%",
      "总收益率 5% 分位": "-41.54%",
      "最大回撤中位数": "-32.82%",
      "最大回撤 95% 分位": "-16.00%",
      "日收益 VaR(5%)": "-7.72%",
      "日收益 ES(5%)": "-9.69%"
    }
  }
}

关键结论:

  • 2008 / 2015:组合大概率腰斩,5% 分位下亏损超过 60%,说明单一高仓位策略在极端行情下非常脆弱。
  • 2020:中位数接近回本,但 5% 分位仍亏损 41%,说明 V 型反弹对大多数人只是「幸存者偏差」,尾部风险不可忽视。

六、常见坑与注意

  1. 不要把历史最大回撤当压力测试。历史最大回撤只是过去发生过的最坏情况,不代表未来。
  2. 仓位是核心变量。示例里股票仓位 60%,如果你仓位 90%,所有损失数字会再放大 1.5 倍。
  3. 不要只跑一个场景。三种场景分别测试系统性暴跌、流动性危机、黑天鹅 V 型,漏掉任何一种都可能踩雷。
  4. 演示证数据窗口有限。正式环境换自己的 licence 后,可用更长的历史窗口做 bootstrapping,模拟结果会更稳定。

七、小结

压力测试的价值不是预测未来,而是:

  • 让你知道「如果 2008 重来,我会亏多少」。
  • 让你在上线前决定是否降仓位、加对冲、或重写策略。
  • 让你在客户问起时,有数字而不是有信仰。

下篇预告:【Python量化策略评估体系 #06】策略上线前 8 项必查:一份可落地的评估 checklist。


了解更多

免责声明:本文仅为技术方法演示,不构成投资建议。历史表现不代表未来收益,投资有风险,入市需谨慎。

相关推荐
十年Java程序媛1 小时前
Java 接口和抽象类对比|Java8 新特性,抛弃老旧八股,正确选型
java·spring boot·后端
IT_陈寒1 小时前
SpringBoot自动配置差点让我加班到凌晨
前端·人工智能·后端
用户8356290780511 小时前
使用 Python 对 Excel 工作表进行排序
后端·python
程序员cxuan1 小时前
腾讯又来一王炸,开源版 WorkBuddy 太夯了!
人工智能·后端·程序员
用户8356290780512 小时前
使用 Python 合并 PowerPoint 演示文稿
后端·python
掘金者阿豪2 小时前
数据同步软件技术实践:从异构数据库迁移到实时数据流转,解析电科金仓KFS架构设计
后端
ServBay2 小时前
不会写代码,如何搭建自己的 AI 自动化工作流
后端·aigc·ai编程
天远大数据2 小时前
从调用账单到业务回执:Agent单位成本核算实践
人工智能·后端
沐言人生2 小时前
又一个神级考公脑库-22,375篇真题终于按考点整理了
后端·面试