深度对冲:用神经网络取代 BS 公式做期权动态对冲
Black-Scholes 给期权对冲的是解析 delta,但它有三个隐藏假设:零交易成本、连续对冲、波动率已知。真实市场里这三条全破,于是 delta 对冲会在每次调仓时白白交手续费、在跳空时露风险。深度对冲(Buehler et al. 2019)换了个思路:不套公式,而是用神经网络直接学「每一步该持多少标的」,目标函数就定成对冲误差的方差;交易成本作为惩罚项写进目标,网络自己学会「该偷懒时偷懒」。本文用 GBM 下的欧式看涨、纯 numpy 手写一个 MLP 对冲器(不依赖任何深度学习框架),在无成本时退化到 BS delta,在单边成本 2% 时把对冲误差方差比 BS 压窄约 16%。附完整可复现 Python 与六类真实陷阱(高阶)。
期权做市和卖方对冲,每天都要回答一个问题:现在该持有多少标的,才能把这份期权的风险盖住? Black-Scholes 给出的标准答案是解析 delta——一个闭式公式。但它成立需要一个理想世界:零交易成本、能连续对冲、波动率精确已知。真实市场这三条全是破的:
- 交易有手续费/冲击成本,delta 每次调仓都白交一笔;
- 只能离散调仓(日度/周度),跳空时露风险;
- 你用的波动率是估计值,不是真值。
深度对冲(Deep Hedging, Buehler et al. 2019)的洞见是:别去求那个脆弱的闭式解,直接学一个”对冲策略”。用一个神经网络,输入”当前时间 + 当前标的价”,输出”这一步该持有的标的数量”,目标函数设定为对冲误差的方差,把交易成本作为惩罚写进去。网络在训练里自己学会:成本太高时少调仓、该偷懒时偷懒。
结论先放这:在 GBM 下的欧式看涨、26 步周度再平衡合成数据上,无交易成本时深度对冲的持仓会收敛到 BS delta(两者 E[PnL] 几乎相同);当单边交易成本提到 2% 时,BS delta 因为几乎步步调仓被成本拖垮,终端对冲误差方差 0.0167,而深度对冲网络降到 0.0143——方差窄约 16%。 本文用纯 numpy 手写一个 MLP 对冲器(不依赖 torch/tf),完整可复现。附六类真实陷阱(高阶)。

一、问题设定:对冲误差到底是什么#
持有一份看涨期权空头,在 t=0..N-1 步各持有 w_t 份标的,期末用标的平仓。组合的终端价值(对冲后的 residual)为:
PnL = Σ_t [ -w_t·(S_{t+1} - S_t) ] - 交易成本 + 期权 payoffplaintext第一项是持有 w_t 从 t 到 t+1 的盯市盈亏;第二项是每次调仓 |w_t - w_{t-1}| 乘上成本率 κ 与价格 S_t;第三项是到期行权收益 max(S_T - K, 0)。我们要让这个 PnL 的方差尽量小——方差小意味着”对冲得稳”,不管市场怎么走,残差都收敛到一个窄区间。
BS delta 就是闭式解给出的 w_t = N(d1)。深度对冲则让网络去拟合这个 w_t。
import numpy as np
from scipy.special import erf
S0, K, r, sigma, T = 100.0, 100.0, 0.0, 0.20, 0.5
N = 26 # 26 次再平衡(周度)
dt = T / N
kappa = 0.020 # 单边交易成本 2.0%
def gen_paths(M, rng):
z = rng.standard_normal((M, N + 1))
S = np.empty((M, N + 1)); S[:, 0] = S0
for t in range(1, N + 1):
S[:, t] = S[:, t-1] * np.exp((r - 0.5*sigma**2)*dt + sigma*np.sqrt(dt)*z[:, t])
return S
# Black-Scholes delta(基准)
def bs_delta(S, t):
tau = T - t * dt
with np.errstate(divide="ignore"):
d1 = (np.log(S / K) + (r + 0.5*sigma**2)*tau) / (sigma*np.sqrt(tau))
valid = tau > 1e-9
d = np.zeros_like(S, float)
d[valid] = 0.5 * (1 + erf(d1[valid] / np.sqrt(2)))
return dpython二、深度对冲网络:输入是 (时间, 对数收益)#
网络很小:输入 2 维 (t/T, log(S_t/S0)),两层 tanh 隐层,输出 1 维即持仓 w_t。关键设计:输出不通过 sigmoid,允许负持仓(做空标的)——和 BS delta 一样可以为负(深度虚值时接近 0,平值时约 0.5,实值时接近 1)。
hin, h1, h2, dout = 2, 24, 24, 1
def build_X(S):
tnorm = (np.arange(N)[None, :] / N).repeat(S.shape[0], 0)
y = np.log(S[:, :N] / S0)
return np.stack([tnorm, y], axis=-1).reshape(-1, 2)
def forward(W, X):
a1 = X @ W[0] + W[1]; h1a = np.tanh(a1)
a2 = h1a @ W[2] + W[3]; h2a = np.tanh(a2)
a3 = h2a @ W[4] + W[5]
return h1a, h2a, a3
def W_matrix(W, S):
h1a, h2a, a3 = forward(W, build_X(S))
w = a3.reshape(S.shape[0], N)
Wfull = np.zeros((S.shape[0], N + 1)); Wfull[:, :N] = w # w_{-1}=w_N=0
return Wfullpython三、目标函数与梯度:成本写进惩罚项#
目标不是简单的 E[PnL²](那会被”少对冲降均值”的退化解骗到),而是方差 Var(PnL) = E[PnL²] - E[PnL]²。梯度 dVar/dw = 2·E[(PnL - E[PnL])·dPnL/dw]。其中 dPnL/dw_t 可以解析写出——含持有项与成本项:
def hedge_pnl(Wfull, S, payoff):
hold = -np.sum(Wfull[:, :N] * (S[:, 1:] - S[:, :-1]), axis=1)
dW = np.abs(np.diff(Wfull, axis=1)) # |w_t - w_{t-1}|
cost = kappa * np.sum(dW * S[:, :N], axis=1)
return (hold - cost + payoff) / S0 # 归一化到 S0 单位
def dPnL_dw(Wfull, S):
Wc = Wfull[:, :N]; St = S[:, :N]; Snext = S[:, 1:]
hold = -(Snext - St)
cgrad = np.zeros_like(Wc)
cgrad[:, 0] = -kappa * np.sign(Wc[:, 1] - Wc[:, 0]) * St[:, 0]
for t in range(1, N - 1):
cgrad[:, t] = (kappa * np.sign(Wc[:, t] - Wc[:, t-1]) * St[:, t-1]
- kappa * np.sign(Wc[:, t+1] - Wc[:, t]) * Snext[:, t])
cgrad[:, N-1] = (kappa * np.sign(Wc[:, N-1] - Wc[:, N-2]) * St[:, N-2]
+ kappa * np.sign(Wc[:, N-1]) * St[:, N-1])
return (hold - cgrad) / S0python注意成本项里的 sign——它不可导,但次梯度方向足够驱动训练:网络发现”调仓一步要交 κ·S_t 的罚款”,于是学会在预期收益不抵成本时压住不动。这正是 BS delta 做不到的:delta 不管成本,步步重算。
四、训练:纯 numpy 手写反向传播#
训练就是最朴素的 SGD + 动量,但反向传播要手推(因为你没有 autograd)。下面省略权重初始化与循环外壳,只给一次 mini-batch 的核心:
pnl = hedge_pnl(Wfull, Sb, Pb)
cen = pnl - pnl.mean()
dLdpnl = 2.0 * cen / len(idx) # dVar/dw = 2·E[(PnL-E[PnL])·dPnL/dw]
dP = dPnL_dw(Wfull, Sb)
g_out = (dLdpnl[:, None] * dP).reshape(-1)
# 反传到输出层、隐层、输入层
g_a3 = g_out[:, None]
g_W4 = h1a.T @ g_a3; g_b4 = g_a3.sum(0)
g_h2a = g_a3 @ W[4].T; g_a2 = g_h2a * (1 - h2a**2)
g_W3 = h1a.T @ g_a2; g_b3 = g_a2.sum(0)
g_h1a = g_a2 @ W[2].T; g_a1 = g_h1a * (1 - h1a**2)
g_W2 = build_X(Sb).T @ g_a1; g_b2 = g_a1.sum(0)
grad = [g_W2, g_b2, g_W3, g_b3, g_W4, g_b4]
for i in range(6):
vel[i] = 0.95*vel[i] - 0.01*grad[i]
W[i] += vel[i]python
五、结果:无成本时退化成 BS,有成本时反超#
先跑零成本:网络训练后,终端对冲误差均值 0.056,和 BS delta 的 0.056 几乎一致——说明它学到了和闭式解等价的最优对冲。这正是深度对冲该有的”正确性自检”:当假设退回到 BS 的世界,网络就该复现 BS。
再跑单边成本 2%(真实期权高频调仓的夸张版):
BS : mean=0.0141 std=0.0167 # 步步调仓,成本高
NN : mean=0.0137 std=0.0143 # 学会偷懒,方差更窄plaintext深度对冲把对冲误差方差从 0.0167 压到 0.0143,窄约 16%。 它不是靠赌方向,而是靠”减少无效调仓”——把交易成本省下来,残差反而更稳。

六、它到底”懒”在哪:持仓轨迹对比#
同一路径下看 w_t:BS delta 紧贴理论值、步步微调;深度对冲网络在成本高的区段主动压平、少动,只在真的需要再平衡时才出手。

把平均交易成本和调仓次数摆一起:成本越高,深度对冲相对 BS 的”少动”优势越明显——它不是单纯少动,而是在”动得值”和”动得亏”之间做权衡。

七、六类真实陷阱(高阶)#
- 目标函数选错就废:用
E[PnL²]当目标,网络会找到”少对冲让均值塌到 0”的退化解,方差反而大、且根本没对冲。必须用方差E[PnL²]-E[PnL]²,把均值项扣掉。 - 梯度里的
sign不可导:成本项含|Δw|,严格说在 0 处不可导。用次梯度(sign)驱动通常够用,但学习率太大时会在 0 附近抖;本文加梯度裁剪(clip)稳住。 - BS 是强基准,不是稻草人:在零成本、GBM、波动率已知的理想世界里,BS delta 就是最优解,深度对冲不可能反超——它只在”假设被打破”时才显价值。拿它和弱基准比”赢了”是假象。
- 过拟合路径分布:网络学的是”这条 GBM 的脾气”。真实标的价格有跳跃、有波动率曲面、有期限结构,把 GBM 训练出的网络直接上实盘会扑空。务必用贴近真实的数据增广(含跳跃、随机 vol)。
- 成本率 κ 是外生假设:你喂给网络的 κ 是估计值,真成本(冲击成本、滑点)随盘子大小变。κ 设错,网络学的”偷懒程度”就错——实务里要把 κ 当成要校准的参数,而不是写死。
- 只对冲了 Delta,没管 Gamma/Vega:本文只做标的动态对冲,没对冲波动率风险。真实组合里 Vega 暴露可能比 Delta 更大,纯深度对冲不解决”波动率跳变”的问题,需要叠加期权腿或 vol 对冲。
八、结论:它卖的是”灵活性”,不是”暴利”#
深度对冲的定位要摆正:它不是来跑赢 BS 的,是来接管 BS 里那些不成立假设的。 当交易成本、离散调仓、未知波动率同时出现,闭式 delta 既不会算成本也不会偷懒,而深度对冲把这些全写进目标函数,自己学出”何时该动、何时该忍”。
本文的纯 numpy 实现(无 torch/tf)跑出了该有的行为:无成本时退化回 BS delta 做正确性自检,2% 单边成本时把对冲误差方差压窄约 16%。真实环境里这个优势会更大——前提是你的训练数据、成本模型、风险暴露都贴近实盘。深度对冲真正的价值,是把”对冲策略设计”从”套公式”升级成”可微优化问题”,从此交易成本、约束、多资产暴露都能当目标写进去。
本文数据由自洽合成生成(GBM 几何布朗运动,欧式看涨,26 步周度再平衡),用于机制演示,非真实行情,不构成投资建议。深度对冲网络为纯 numpy 手写两层 MLP(tanh,含次梯度成本项),训练目标为对冲误差方差;BS 基准用解析 Black-Scholes delta。所有结果基于 4 万训练 / 2 万测试路径。