【Python量化波动率实战 06】一键输出风险看板:波动率仪表盘工程化

0 阅读8分钟

系列前五篇把零件都造好了:波动率怎么算、EWMA 怎么追、仓位怎么定、异象怎么验、分散怎么量。收官这篇做工程化拼装:把波动率、回撤、相关性、目标仓位四块仪表拼成一张 2×2 风险看板,越限自动告警,每天收盘后一键刷新。


本文你将得到什么

  1. 一张四面板风险看板的完整实现:波动率仪表 + 回撤曲线 + 滚动相关 + 目标仓位。
  2. 每块仪表的越限规则与告警清单输出(CSV)。
  3. 一个容易被忽略的验收动作:0 告警时如何确认"看板没坏"。
  4. 系列六篇的总收束:波动率从概念到决策再到工程的完整链路。

一、看板设计:四个面板,四条阈值线

面板1 组合年化波动率(20日)  越限线 = 目标波动率 20%
面板2 组合回撤(%)           预警线 = -10%
面板3 滚动平均两两相关(20日) 越限线 = 0.60
面板4 波动率目标仓位(%)     来自 #03 的规则:min(20% / 当前波动, 100%)

所有指标每天收盘后用截至当日的数据刷新(面板 4 用昨日波动率定今日仓位,延续无前视纪律)。任何一日越限,就在告警清单里记一条:日期、指标、读数、阈值。告警清单落成 CSV,可以直接接邮件或 IM 推送。


二、一个必要的验收动作

看板最怕的不是报错,而是永远 0 告警的假安静——阈值写错、指标算错、数据断更,都可能表现为"一切正常"。老规矩:第一次上线,人为收紧阈值跑一遍,确认告警真的会响。本样本最新读数全部未越限(0 告警),把阈值收紧后立刻触发了几十条——逻辑没坏,只是行情确实平静。这个动作花两分钟,换来对监控本身的信任。


可运行代码

pip install mairui pandas numpy matplotlib
from mairui import Client
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import pandas as pd
import numpy as np
import random

plt.rcParams["font.sans-serif"] = ["SimHei", "Microsoft YaHei"]  # 中文字体
plt.rcParams["axes.unicode_minus"] = False

LICENCE = "LICENCE-66D8-9F96-0C7F0FBCD073"  # 演示证书,请换成你自己的正式证书
client = Client(LICENCE)

TRADING_DAYS = 244
VOL_WIN = 20
TARGET_VOL = 0.20     # 波动率目标线 20%
CORR_WIN = 20
CORR_ALERT = 0.60     # 相关越限阈值
MDD_ALERT = -0.10     # 回撤预警线 -10%
POOL = ["600519", "601318", "600036", "300750", "601088"]
SEED = 20261013


def synth_ohlc(bars, seed):
    # 演示证书返回固定样例数据;用确定性合成路径模拟行情起伏(慢变波动率+波动聚集)。
    # 换成你自己的正式证书后删掉本函数调用即可,其余逻辑零改动。
    rng = random.Random(seed)
    g = 0.0
    out, prev_c = [], None
    for b in bars:
        g = 0.9 * g + rng.gauss(0, 0.25)
        sigma = 0.015 * float(np.exp(g))
        if prev_c is None:
            c = float(b["c"]); o = c * (1 + rng.gauss(0, 0.3 * sigma))
        else:
            c = prev_c * (1 + rng.gauss(0, sigma))
            o = prev_c * (1 + rng.gauss(0, 0.3 * sigma))
        h = max(o, c) * (1 + abs(rng.gauss(0, 0.5 * sigma)))
        l = min(o, c) * (1 - abs(rng.gauss(0, 0.5 * sigma)))
        out.append({"t": b["t"], "c": round(c, 2)})
        prev_c = c
    return out


# 数据与指标
R = {}
for code in POOL:
    bars = client.stock_history(code, "d", "n", st="20260701", et="20261012")
    bars = synth_ohlc(bars, seed=SEED + int(code))
    closes = pd.DataFrame(bars)["c"]
    R[code] = pd.Series(np.log(closes / closes.shift(1)).to_numpy())
R = pd.DataFrame(R)
dates = pd.to_datetime(pd.DataFrame(synth_ohlc(
    client.stock_history(POOL[0], "d", "n", st="20260701", et="20261012"),
    seed=SEED + int(POOL[0])))["t"])

port = R.mean(axis=1)                                    # 等权组合日收益
vol = port.rolling(VOL_WIN).std(ddof=1) * np.sqrt(TRADING_DAYS)
nav = (1 + port.fillna(0)).cumprod()
dd = nav / np.maximum.accumulate(nav) - 1                # 回撤
weight = (TARGET_VOL / vol).clip(upper=1.0).shift(1)     # 目标仓位(无前视)

n = R.shape[1]
iu = np.triu_indices(n, k=1)
rho = [np.nanmean(R.iloc[i - CORR_WIN:i].corr().to_numpy()[iu])
       if i >= CORR_WIN else np.nan for i in range(len(R))]

# 告警清单
alerts = []
for i in range(VOL_WIN, len(port)):
    if not np.isnan(vol.iloc[i]) and vol.iloc[i] > TARGET_VOL:
        alerts.append((str(dates.iloc[i].date()), "port_vol", round(float(vol.iloc[i]), 4), TARGET_VOL))
    if not np.isnan(rho[i]) and rho[i] > CORR_ALERT:
        alerts.append((str(dates.iloc[i].date()), "avg_corr", round(float(rho[i]), 4), CORR_ALERT))
    if dd.iloc[i] < MDD_ALERT:
        alerts.append((str(dates.iloc[i].date()), "drawdown", round(float(dd.iloc[i]), 4), MDD_ALERT))
A = pd.DataFrame(alerts, columns=["date", "metric", "value", "threshold"])
if len(A):
    print(f"告警合计 {len(A)} 条: " + "、".join(f"{k}={v}" for k, v in A["metric"].value_counts().items()))
else:
    print("告警合计 0 条(各项指标均未越限,属正常状态)")
print(f"最新交易日 {dates.iloc[-1].date()} | 年化波动 {vol.iloc[-1]*100:.2f}% | "
      f"回撤 {dd.iloc[-1]*100:.2f}% | 平均相关 {rho[-1]:.4f} | "
      f"建议仓位 {(0 if np.isnan(weight.iloc[-1]) else weight.iloc[-1]*100):.1f}%")

# 四面板成图
fig, axes = plt.subplots(2, 2, figsize=(13, 8))
x = dates.to_numpy()
ax = axes[0][0]
ax.plot(x, vol * 100, color="#1f77b4", lw=1.2)
ax.axhline(TARGET_VOL * 100, color="red", ls="--", lw=1, label=f"目标 {TARGET_VOL*100:.0f}%")
ax.set_title("组合年化波动率(20日)"); ax.legend(fontsize=8)
ax = axes[0][1]
ax.fill_between(x, dd * 100, 0, color="#d62728", alpha=0.35)
ax.axhline(MDD_ALERT * 100, color="orange", ls="--", lw=1, label=f"预警 {MDD_ALERT*100:.0f}%")
ax.set_title("组合回撤(%)"); ax.legend(fontsize=8)
ax = axes[1][0]
ax.plot(x, rho, color="#2ca02c", lw=1.2)
ax.axhline(CORR_ALERT, color="red", ls="--", lw=1, label=f"越限 {CORR_ALERT}")
ax.set_title("滚动平均两两相关(20日)"); ax.legend(fontsize=8)
ax = axes[1][1]
ax.plot(x, weight * 100, color="#9467bd", lw=1.2)
ax.set_title("波动率目标仓位(%)")
for a in axes.ravel():
    a.tick_params(labelsize=7); a.grid(alpha=0.25)
fig.suptitle(f"风险看板({POOL[0]} 等 {n} 只等权)", fontsize=13)
fig.tight_layout(rect=(0, 0, 1, 0.96))
fig.savefig("risk_dashboard.png", dpi=120)
A.to_csv("risk_alerts.csv", index=False, encoding="utf-8-sig")
print("[done] risk_dashboard.png + risk_alerts.csv 已保存")

说明:演示证书返回固定样例数据,代码用确定性合成路径模拟行情起伏;换成你自己的正式证书后删掉 synth_ohlc 调用即为真实行情,看板逻辑零改动。涨红跌绿为 A 股惯例,回撤面板用红色面积填充表示亏损区间。


运行结果

告警合计 0 条(各项指标均未越限,属正常状态)
最新交易日 2025-12-01 | 年化波动 10.00% | 回撤 -1.83% | 平均相关 -0.0770 | 建议仓位 100.0%
[done] risk_dashboard.png + risk_alerts.csv 已保存

(示例基于 50 个交易日的演示样本测算,结果为教学演示而非普遍规律;演示证书返回就近样例数据,申请正式证书可拉取全市场全量历史做更长样本验证。)


怎么读这组数字

  1. 最新状态一条读完:波动 10.00%(目标线下方一半)、回撤 -1.83%、相关 -0.0770、建议仓位 100%——四项全部健康,0 告警是"真安静"。注意建议仓位按 #03 规则本样本始终顶在 100%:波动率从未越过 20% 目标,clip 上限一直在生效。这不是 bug,是规则的正常输出;换成波动剧烈的样本,仓位线就会自己动起来。
  2. 回撤面板是最直观的一块:红色面积从 -8% 一路收窄到 -1.83%,配 -10% 预警线一眼判断"离告警多远"。好看板的标准就是如此——不用读数字,色块和虚线的距离就是风险距离。
  3. 0 告警要验收过才可信:如第二节所说,把阈值临时收紧(比如波动告警降到 14%、相关告警降到 0)再跑一遍,告警清单立刻多出几十条——逻辑有效性的最好证据。上线后每月做一次这种"演习",防止监控本身悄悄失效。

三个容易踩的坑

  1. 中文变方框:matplotlib 默认字体不含中文字形,必须指定 SimHei / Microsoft YaHei 并设置 axes.unicode_minus=False,否则负号也会一起消失。
  2. 告警直接打印到控制台就完事:控制台没人看。告警清单落 CSV 只是第一步,实战要接上邮件 / IM 推送;同时给每条告警带上阈值,否则收到"波动 0.24"还得猜是高是低。
  3. 看板只做不做演练:监控系统的最大风险是"从未响过"。定期收紧阈值做灵敏度演习,确认告警链路活着——这是所有生产监控的通用验收动作,不只是量化看板。

小结与系列回顾

至此《Python量化波动率实战》六篇完结,串起来是一条完整链路:

#01 会算:日收益 -> 年化 -> 滚动 -> 区间估计器
#02 追得上:EWMA 给近期行情更大话语权
#03 用得上:波动率目标仓位,机械定仓
#04 敢验证:低波动异象分层实验(含诚实的负结果)
#05 量分散:相关 / 有效 N / 分散化比率
#06 成工程:四面板看板 + 越限告警

波动率不是论文里的概念,而是每天收盘后可以自动计算、自动打分、自动告警的工程指标。把本系列的脚本接上定时任务和一个稳定的数据源,你就拥有了一套自己的风险仪表盘。


免责声明

本文仅为技术方法演示,不构成任何投资建议。文中示例数据仅用于教学,历史表现不代表未来收益。投资有风险,决策需谨慎。

代码与文档可参考 GitHub:github.com/MaiRuiApi