【Python量化组合管理实战 02】钱该怎么分?等权、波动率倒数与动态评分三种配权实测

本文你将得到什么:

  1. 一个统一的配权回测框架:权重每日用"截至昨日"的数据重估,杜绝前视
  2. 三种权重方案的完整实现与同台对比:等权 / 波动率倒数 / 动态评分
  3. 风险平价(ERC)的手写数值迭代------以及它在负相关下会"退化"的实测证据
  4. 一个反直觉结论:下跌市里追强势的"动态评分"反而亏得最少,但波动最高

痛点:等权是最省事的分法,也是最"一刀切"的分法

上一篇用等权组合把波动压了下来,但等权有个明显缺陷:波动 47% 的策略和波动 37% 的策略拿一样的钱------风险贡献严重失衡。机构里常听到的"风险平价""波动率倒数加权",说的就是按风险而非按钱数来分配。这些名字听着唬人,核心公式其实一行就能写下。这篇把三种方案放进同一个框架里实测,看它们到底差在哪。


环境准备

bash 复制代码
pip install mairui pandas numpy matplotlib

第一步:复用上一篇的三个策略收益流

数据与信号口径与上一篇完全一致(昨日收盘信号、shift(1) 无前视),直接复用:

python 复制代码
from mairui import Client
import pandas as pd
import numpy as np

UNIVERSE = [
    ("600519", "sma520", "MA5/20动量"),
    ("000001", "sma310", "MA3/10快速"),
    ("300750", "mom20", "20日动量"),
]

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

def strategy_daily_returns(df, kind):
    c = df["c"]
    if kind == "sma520":
        sig = (c.rolling(5).mean() > c.rolling(20).mean()).astype(float)
    elif kind == "sma310":
        sig = (c.rolling(3).mean() > c.rolling(10).mean()).astype(float)
    elif kind == "mom20":
        sig = (c / c.shift(20) - 1 > 0).astype(float)
    pos = sig.shift(1).fillna(0.0)
    ret = c.pct_change().fillna(0.0)
    return pos * ret

rets = {}
for code, kind, label in UNIVERSE:
    bars = client.stock_history(code=code, period="d", dividend="f")
    df = pd.DataFrame(bars).rename(columns={"t": "date"}).sort_values("date").reset_index(drop=True)
    for col in ("o", "h", "l", "c", "v"):
        df[col] = df[col].astype(float)
    rets[label] = strategy_daily_returns(df, kind)

R = pd.DataFrame(rets)

说明:这里用的是公开演示证书,为演示方便明文写入;实际项目请换成你自己的正式证书(申请配置见 SDK 文档 https://github.com/MaiRuiApi/mairui ),换证后代码无需修改。


第二步:三种权重函数,每种都是几行

python 复制代码
LOOKBACK = 20

def w_equal(win):
    """等权:各 1/3。"""
    return np.ones(win.shape[1]) / win.shape[1]

def w_inverse_vol(win):
    """波动率倒数:w_i ∝ 1/vol_i(过去20日)------风险平价的常用近似。"""
    vol = win.std().values
    w = 1.0 / np.maximum(vol, 1e-12)
    return w / w.sum()

def w_dynamic_score(win):
    """动态评分:低波动得分与近期强势得分各占 50%,保底 5% 防单腿清零。"""
    vol = win.std().values
    mom = (1 + win).prod().values - 1            # 近 20 日累计收益
    vol_s = 1 - (vol - vol.min()) / (np.ptp(vol) + 1e-12)   # 波动越小分越高
    mom_s = (mom - mom.min()) / (np.ptp(mom) + 1e-12)       # 涨得越猛分越高
    score = 0.5 * vol_s + 0.5 * mom_s
    w = np.maximum(score, 0.05)
    return w / w.sum()

第三步:统一回测框架(无前视是底线)

组合日收益 = w·r,但权重只能用截至昨日的窗口算------用今天的波动率算今天该配多少钱,就是未来函数:

python 复制代码
def run_weighted(R, weight_fn):
    """每日再平衡:w_t 用截至昨日(不含当日)的窗口计算。"""
    n = len(R)
    port = np.ones(n)
    w_last = None
    for t in range(1, n):
        win = R.iloc[max(0, t - LOOKBACK):t]
        w = weight_fn(win)
        if not np.all(np.isfinite(w)):
            w = np.ones(R.shape[1]) / R.shape[1]   # 冷启动窗口不足,退化等权
        w_last = w
        port[t] = port[t - 1] * (1 + float(np.dot(w, R.iloc[t].values)))
    return pd.Series(port), w_last

def perf(r, name=""):
    s = pd.Series(r, dtype=float)
    eq = (1 + s).cumprod()
    vol = s.std() * np.sqrt(252)
    sharpe = s.mean() / s.std() * np.sqrt(252) if s.std() > 0 else 0.0
    mdd = ((eq - eq.cummax()) / eq.cummax()).min()
    return dict(name=name, 累计收益=eq.iloc[-1] - 1, 年化波动=vol, 夏普=sharpe, 最大回撤=mdd)

for name, fn in [("等权", w_equal), ("波动率倒数", w_inverse_vol), ("动态评分", w_dynamic_score)]:
    port, w_last = run_weighted(R, fn)
    p = perf(port.pct_change(fill_method=None).fillna(0.0), name)
    print(f"{name}: 累计 {p['累计收益']:+.2%}  年化波动 {p['年化波动']:.2%}  "
          f"夏普 {p['夏普']:.2f}  最大回撤 {p['最大回撤']:.2%}")

输出示例(演示证样例数据,仅供演示):

复制代码
等权: 累计 -22.09%  年化波动 26.32%  夏普 -4.64  最大回撤 -22.41%
波动率倒数: 累计 -21.85%  年化波动 28.82%  夏普 -4.16  最大回撤 -22.14%
动态评分: 累计 -19.45%  年化波动 32.63%  夏普 -3.17  最大回撤 -19.77%

第四步:风险平价(ERC)------把它讲透,包括它失灵的时候

风险平价要求每个策略对组合总风险的贡献相等。用不动点迭代可以手写求解,不依赖任何优化库:

python 复制代码
def erc_weights(cov, iters=1000, tol=1e-12):
    """等风险贡献权重:迭代 w ∝ 1/MRC,返回权重与各腿风险贡献占比。"""
    sigma = np.asarray(cov, dtype=float)
    n = sigma.shape[0]
    w = np.ones(n) / n
    for _ in range(iters):
        mrc = sigma @ w                      # 边际风险贡献
        w_new = 1.0 / np.maximum(mrc, 1e-16)
        w_new /= w_new.sum()
        if np.max(np.abs(w_new - w)) < tol:
            w = w_new
            break
        w = w_new
    trc = w * (sigma @ w)                    # 总风险贡献
    return w, trc / trc.sum()

w_erc, trc_share = erc_weights(R.cov().values)
print("权重:", dict(zip(R.columns, w_erc.round(3))))
print("风险贡献占比:", dict(zip(R.columns, trc_share.round(3))))

输出示例:

复制代码
权重: {'MA5/20动量': 0.5, 'MA3/10快速': 0.0, '20日动量': 0.5}
风险贡献占比: {'MA5/20动量': 0.536, 'MA3/10快速': -0.0, '20日动量': 0.464}

注意 MA3/10 那一列:风险贡献是负的,权重被迭代逼成了 0 。原因是它与另外两个策略负相关(-0.176 / -0.157),长多约束下"每个策略风险贡献相等且为正"根本无解。这就是为什么工程上普遍用波动率倒数当风险平价的近似------它不需要解方程,且在策略相关性彼此接近时与 ERC 几乎等价。负相关腿反而应该在组合里放大(对冲属性),但那是带约束优化的范畴,属于进阶话题。


结果怎么读(诚实的部分)

  1. 没有万能赢家:动态评分亏得最少(-19.45%)但波动最高(32.63%);等权波动最低(26.32%)但收益垫底区间;波动率倒数夏普最好(-4.16)。选哪种取决于你要什么------压波动、博收益,还是风险调整后最优;
  2. 下跌市里"追强势"为什么没追在高点:动态评分 50% 权重给了近期收益,样例里有反弹段它跟上了,但它同时把波动放大了 6 个百分点------在震荡市里这套评分会反复追高杀低,别把单一行情段的表现当规律;
  3. ERC 的"退化"是真实世界的一部分:教科书假设相关性为正,实战中策略负相关很常见,看到 0 权重腿先别怀疑代码,先查相关性矩阵;
  4. 数字为演示证样例数据结果,仅演示方法;换正式证书即得真实行情下的对比。

常见坑

  1. 前视加权 :权重窗口含当日收益 = 未来函数。统一 R.iloc[max(0, t-LOOKBACK):t],右端开区间不含当日;
  2. 冷启动 NaN :前几日窗口不足 2 行,std() 返回 NaN,权重全 NaN、净值成一条平线------务必 np.isfinite 兜底退化等权;
  3. 单腿清零:评分加权不设保底时,某腿可能长期 0 权重,组合悄悄变成单策略。本文用 5% 保底;
  4. 把"波动率倒数"说成"风险平价":两者只在相关性彼此接近时近似相等,严谨的表述是"逆波动率加权是风险平价的常用近似"。

小结与下篇预告

  • 三种配权同台:等权最稳、动态评分在下跌反弹段占优、波动率倒数是风险平价的实用近似;
  • 风险平价的完整数学(等风险贡献)在负相关下可能无解,实测迭代收敛到 0 权重腿------先看相关性再谈风平;
  • 权重定好之后还有个隐含问题:涨着涨着权重就"漂"了,要不要定期拉回来?拉回来要花钱吗?下一篇用实测回答:每月调一次仓到底值不值。

免责声明

本文所有内容仅用于技术学习与交流,不构成任何投资建议。股市有风险,投资需谨慎。

相关推荐
aa小小1 小时前
从0到1:Windows 本地部署 Qwen3 + Flask 接口(RAG 学习路线第1周踩坑实录)
python
镜子AI1 小时前
镜子AI工程实践:用熵权法 + TOPSIS 做多方案选型排序(零依赖 Python)
开发语言·人工智能·python
Wang's Blog1 小时前
Java 项目实战:交友项目环信云通信全解——体系集成、执行链路与 REST API 实战
java·spring·交友
god_cvz1 小时前
第三章:12、经验沉淀与自优化
java·ai
小冯不疯1 小时前
查询旺店通货品策略实战教程:从源系统拉取到轻易云的端到端落地
java·大数据·数据库
西索斯coding1 小时前
Kimi K2.6 与 Qwen3.7 Max:跨文件修复评测与成本核算方法(2026)
开发语言·python·ai
梯度下降者1 小时前
桌面应用自动化python——uiautomation API 如何找元素
python·自动化·窗口操作·uiautomation·控件识别
wuyk5551 小时前
PyQt6-第10讲-PDF工具箱
python·microsoft·pdf·pyqt
SunnyDays10111 小时前
Java 实现 Excel 与 Markdown 相互转换:表格导出、格式设置与多表提取
java·excel转markdown·markdown转excel