1. 识别做市业务中的市场风险敞口

任务说明

本案例要求从做市交易记录中识别并量化市场风险敞口。你将处理一份包含多资产类别(股票、期权、债券)的做市持仓数据,计算三类核心风险因子敞口:价格敞口(Delta)、波动率敞口(Vega)和利率敞口(Rho),并按资产类别汇总分类。完成后的输出将直接用于后续案例的限额监控与对冲决策。

环境与依赖

本案例使用 Python 3.9+,依赖 numpy 和 pandas。若需复现期权定价计算,请安装 scipy(用于正态分布CDF/PDF)。无需其他外部数据源,所有输入数据在代码中内联定义。

核心实现

以下代码完成从原始持仓记录到风险敞口汇总的完整链路。关键设计:期权敞口使用Black-Scholes解析公式计算,股票和债券敞口按线性近似处理。债券利率敞口以修正久期近似,股票价格敞口即市值本身。

import numpy as np
import pandas as pd
from scipy.stats import norm

# ---------- 输入数据:做市持仓记录 ----------
# 字段:instrument, asset_class, position, underlying_price, 
#       strike, maturity_years, volatility, risk_free_rate, coupon_rate
positions = pd.DataFrame([
    # 股票持仓(价格敞口 = 市值)
    {"instrument": "AAPL", "asset_class": "equity", "position": 5000,
     "underlying_price": 175.0, "strike": np.nan, "maturity_years": np.nan,
     "volatility": np.nan, "risk_free_rate": 0.03, "coupon_rate": np.nan},
    {"instrument": "MSFT", "asset_class": "equity", "position": -2000,
     "underlying_price": 330.0, "strike": np.nan, "maturity_years": np.nan,
     "volatility": np.nan, "risk_free_rate": 0.03, "coupon_rate": np.nan},
    # 期权持仓(需要计算 Delta / Vega / Rho)
    {"instrument": "AAPL_OPT_C", "asset_class": "option", "position": 100,
     "underlying_price": 175.0, "strike": 170.0, "maturity_years": 0.5,
     "volatility": 0.25, "risk_free_rate": 0.03, "coupon_rate": np.nan},
    {"instrument": "MSFT_OPT_P", "asset_class": "option", "position": -50,
     "underlying_price": 330.0, "strike": 340.0, "maturity_years": 0.25,
     "volatility": 0.30, "risk_free_rate": 0.03, "coupon_rate": np.nan},
    # 债券持仓(利率敞口 = 修正久期 × 市值 × (-1))
    {"instrument": "US_T_2Y", "asset_class": "bond", "position": 1000000,
     "underlying_price": 99.5, "strike": np.nan, "maturity_years": 2.0,
     "volatility": np.nan, "risk_free_rate": 0.03, "coupon_rate": 0.025},
    {"instrument": "US_T_10Y", "asset_class": "bond", "position": -500000,
     "underlying_price": 98.0, "strike": np.nan, "maturity_years": 10.0,
     "volatility": np.nan, "risk_free_rate": 0.03, "coupon_rate": 0.035},
])

# ---------- Black-Scholes 希腊字母计算 ----------
def bs_greeks(S, K, T, r, sigma, option_type):
    """返回 (delta, vega, rho),vega 为每单位标的变动1%的敞口"""
    if T <= 0 or sigma <= 0:
        return 0.0, 0.0, 0.0
    d1 = (np.log(S / K) + (r + 0.5 * sigma**2) * T) / (sigma * np.sqrt(T))
    d2 = d1 - sigma * np.sqrt(T)
    if option_type == "call":
        delta = norm.cdf(d1)
        rho = K * T * np.exp(-r * T) * norm.cdf(d2) / 100  # 每1%利率变动
    else:  # put
        delta = norm.cdf(d1) - 1
        rho = -K * T * np.exp(-r * T) * norm.cdf(-d2) / 100
    vega = S * np.sqrt(T) * norm.pdf(d1) / 100  # 每1%波动率变动
    return delta, vega, rho

# ---------- 逐笔计算敞口 ----------
results = []
for _, row in positions.iterrows():
    if row["asset_class"] == "equity":
        # 价格敞口 = 持仓 × 价格(市值)
        price_exp = row["position"] * row["underlying_price"]
        vol_exp, rate_exp = 0.0, 0.0
    elif row["asset_class"] == "option":
        # 判断期权类型(从instrument名称末尾C/P)
        opt_type = "call" if row["instrument"].endswith("C") else "put"
        delta, vega, rho = bs_greeks(
            row["underlying_price"], row["strike"], row["maturity_years"],
            row["risk_free_rate"], row["volatility"], opt_type
        )
        # 合约乘数假设为100(标准美股期权)
        contract_mult = 100
        price_exp = row["position"] * contract_mult * delta * row["underlying_price"]
        vol_exp = row["position"] * contract_mult * vega
        rate_exp = row["position"] * contract_mult * rho
    else:  # bond
        # 修正久期近似:D_mod ≈ maturity / (1 + coupon_rate)
        mod_duration = row["maturity_years"] / (1 + row["coupon_rate"])
        bond_value = row["position"] * row["underlying_price"] / 100  # 价格按面值百分比
        price_exp = bond_value  # 价格敞口 = 市值
        rate_exp = -mod_duration * bond_value / 100  # 每1%利率变动
        vol_exp = 0.0
    results.append({
        "instrument": row["instrument"],
        "asset_class": row["asset_class"],
        "price_exposure": round(price_exp, 2),
        "vol_exposure": round(vol_exp, 2),
        "rate_exposure": round(rate_exp, 2),
    })

exposure_df = pd.DataFrame(results)

# ---------- 按资产类别汇总 ----------
summary = exposure_df.groupby("asset_class")[["price_exposure", "vol_exposure", "rate_exposure"]].sum()
summary.loc["total"] = summary.sum()

# ---------- 输出 ----------
print("=== 逐笔敞口明细 ===")
print(exposure_df.to_string(index=False))
print("\n=== 按资产类别汇总 ===")
print(summary.round(2).to_string())

运行方式

将上述代码保存为 exposure_identification.py,在安装依赖后执行:

pip install numpy pandas scipy
python exposure_identification.py

验证方法与判定标准

运行后应观察到以下特征,满足即判定通过:

  • 逐笔明细:AAPL股票价格敞口为 5000×175 = 875,000;MSFT为 -660,000;期权行有非零的Delta/Vega/Rho值;债券行利率敞口为负(多头)或正(空头)。
  • 汇总行:total 行的三类敞口等于各资产类别之和,且数值与手工计算一致(例如AAPL期权Call的Delta敞口 = 100×100×N(d1)×175,其中N(d1)≈0.62,约1,085,000)。
  • 符号正确性:空头持仓(负position)产生负敞口,多头为正;看跌期权Delta为负,价格敞口为负。

若输出中上述数值匹配、无NaN且汇总正确,则案例实现通过。

常见问题

  • 期权合约乘数:本案例假设100股/合约,若实际市场不同需调整 contract_mult。
  • 债券价格单位:输入价格99.5表示面值的99.5%,计算市值时已除以100换算。
  • Rho单位:代码中除以100表示每1%利率变动(而非1bp),与Vega的每1%波动率口径一致。