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

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


本文你将得到什么

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

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

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

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


二、一个必要的验收动作

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


可运行代码

bash 复制代码
pip install mairui pandas numpy matplotlib
python 复制代码
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 股惯例,回撤面板用红色面积填充表示亏损区间。


运行结果

text 复制代码
告警合计 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量化波动率实战》六篇完结,串起来是一条完整链路:

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

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


免责声明

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

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

相关推荐
花间相见2 小时前
【计算基础|Redis02】【上手】五大数据类型命令逐条实操
数据库·redis·后端
GISer_Jing2 小时前
AI Agent秋招面试万字通关指南|架构、ReAct、上下文工程、RAG、评测、生产运维全解
后端·ai·langchain
Moment2 小时前
AI 编程时代,聊聊我的 Mac mini 双屏桌面
前端·后端
学长毕设3 小时前
基于SpringBoot的游乐园管理系统-附源码
java·spring boot·后端
橙序员小站4 小时前
Windows 安装包反复报错?一次排查补上了 5 个 CI 检查点
后端
陈随易4 小时前
Cloudflare 出钱,Vite 团队挑人,44 个维护者分 30 万美元
前端·后端·程序员
SimonKing4 小时前
震惊:怎么回事,opencode一轮对话耗尽免费额度
java·后端·程序员
张彦峰ZYF4 小时前
功能齐了不等于能上线:智能工单系统的AI回归测试、安全防护、成本可观测与CI/CD生产部署
后端·企业智能工单系统·评测线·安全线·成本线·部署线
Rain的Java大神之路4 小时前
LeetCode 37 解数独面试攻略:回溯 + 位掩码 + MRV,从暴力到毫秒级求解
java·后端·leetcode