3、风险因子建模:价格风险因子(跳变、滑点)、流动性风险因子(买卖价差、深度衰减)、相关性风险因子(跨资产传染)
做市策略的压力测试,说白了就是一场「极限生存挑战」。你得先搞清楚,到底哪些东西能把你的策略搞死。我个人习惯把风险因子分成三类:价格上的、流动性上的、以及相关性上的。这三类因子,就像三把悬在头顶的剑,每一把落下来,都够你喝一壶的。
3.1 价格风险因子:跳变与滑点
价格风险因子,是压力测试里最直观、也最容易被低估的一块。很多人觉得价格波动嘛,无非就是涨涨跌跌。但真正致命的,是「跳变」和「滑点」这两个东西。
3.1.1 跳变风险
跳变,就是价格在一瞬间从一个点位跳到另一个点位,中间没有任何成交。为什么会这样?通常是因为重大消息发布、流动性瞬间枯竭,或者干脆就是交易所的撮合引擎抽风了。
我在项目中遇到过最夸张的一次,是某个小币种在凌晨三点,因为一条假新闻,价格直接从 0.5 美元跳到了 0.05 美元。我的做市策略还在傻乎乎地挂着双边订单,结果瞬间被吃掉了所有买单,亏损惨重。
建模跳变风险,我建议用「跳跃扩散过程」来模拟。简单来说,就是在常规的布朗运动基础上,叠加一个泊松跳跃过程。代码实现起来也不复杂:
import numpy as np
def jump_diffusion(S0, mu, sigma, lam, jump_mean, jump_std, T, N):
"""
模拟跳跃扩散过程
S0: 初始价格
mu: 漂移率
sigma: 波动率
lam: 跳跃强度(平均每秒跳跃次数)
jump_mean: 跳跃幅度的均值
jump_std: 跳跃幅度的标准差
T: 总时间
N: 时间步数
"""
dt = T / N
prices = np.zeros(N)
prices[0] = S0
for i in range(1, N):
# 常规扩散部分
dW = np.random.normal(0, np.sqrt(dt))
# 跳跃部分
dJ = 0
if np.random.poisson(lam * dt) > 0:
dJ = np.random.normal(jump_mean, jump_std)
prices[i] = prices[i-1] * (1 + mu * dt + sigma * dW + dJ)
return prices
嗯,这里要注意:跳跃强度 lam 这个参数,千万别拍脑袋定。我建议用历史数据里的极端事件频率来校准。比如过去一年里,价格单日波动超过 5% 的次数,除以总交易天数,就是一个比较靠谱的基准值。
3.1.2 滑点风险
滑点,就是你的订单成交价格,和你预期的价格之间的偏差。在流动性好的市场里,滑点可能只有零点几个 tick。但在压力场景下,滑点可以大到让你怀疑人生。
我记得有一次做回测,策略在正常行情下年化收益 30%,滑点设了 0.01%。结果一上实盘,遇到一次小规模闪崩,实际滑点达到了 0.5%,当天就把一个月的利润全吐回去了。
建模滑点,我习惯用「市场冲击模型」。一个比较实用的方法是:
def slippage_model(order_size, order_book_depth, volatility):
"""
估算滑点
order_size: 订单数量
order_book_depth: 订单簿深度(前N档的总量)
volatility: 当前波动率
"""
base_slippage = order_size / order_book_depth
# 波动率越高,滑点越大
vol_adjustment = 1 + 2 * volatility
return base_slippage * vol_adjustment
3.2 流动性风险因子:买卖价差与深度衰减
流动性风险,是做市商的「命门」。你想想看,做市商赚的就是买卖价差,如果价差突然变大,或者深度突然变浅,你的盈利模型就全变了。
3.2.1 买卖价差
买卖价差,就是买一价和卖一价之间的差值。正常市场里,价差可能只有 0.01%。但在压力场景下,价差可以扩大到 1% 甚至更高。
建模价差,我建议用「自回归条件异方差模型」来拟合。说白了,就是价差本身也有波动率,而且这个波动率会聚集——大价差之后往往跟着更大的价差。
def spread_simulation(current_spread, spread_vol, shock):
"""
模拟价差变化
current_spread: 当前价差
spread_vol: 价差的波动率
shock: 外部冲击(比如新闻事件)
"""
# 价差均值回归特性
mean_spread = 0.0001 # 0.01%
# 冲击会放大价差
new_spread = current_spread + 0.3 * (mean_spread - current_spread) + \
spread_vol * np.random.normal() + shock
return max(new_spread, 0.00001) # 价差不能为负
3.2.2 深度衰减
深度,就是订单簿上各个价位的挂单量。深度衰减,指的是在压力场景下,订单簿上的挂单量急剧减少。这比价差扩大更可怕——价差大至少还能成交,深度没了,你连成交都做不到。
我记得有一次,某个交易所的做市商集体撤单,导致一个主流币种的订单簿深度在 10 秒内下降了 80%。我的策略还在按正常深度计算风险敞口,结果一执行就发现根本平不了仓。
建模深度衰减,我建议用「指数衰减模型」:
def depth_decay(initial_depth, decay_rate, time_elapsed):
"""
模拟深度衰减
initial_depth: 初始深度
decay_rate: 衰减速率
time_elapsed: 经过的时间(秒)
"""
return initial_depth * np.exp(-decay_rate * time_elapsed)
这个模型的关键在于 decay_rate 的设定。我一般会取历史数据里,深度下降最快的那 5% 的时段,计算平均衰减速率,然后把这个值作为压力场景的输入。
3.3 相关性风险因子:跨资产传染
相关性风险,是压力测试里最容易被忽视、但破坏力最大的因子。很多人觉得,我做的是 BTC 的做市,跟 ETH 有什么关系?嗯,关系大了去了。
在正常市场里,不同资产之间的相关性可能很低。但在危机时刻,相关性会急剧上升——所有资产一起跌,这就是所谓的「系统性风险」。我见过最典型的例子是 2020 年 3 月 12 日,比特币和标普 500 指数的相关性从 0.1 飙到了 0.8。
建模相关性风险,我建议用「动态条件相关模型」。这个模型的核心思想是:相关性不是固定的,它会随着市场状态变化而变化。
def dynamic_correlation(rho_0, market_vol, shock):
"""
动态相关性模型
rho_0: 基准相关性
market_vol: 市场波动率
shock: 外部冲击
"""
# 市场波动率越高,相关性越强
rho = rho_0 + 0.5 * (market_vol - 0.2) + shock
# 相关性不能超过 [-1, 1] 的范围
return np.clip(rho, -1, 1)
另外,跨资产传染还有一个更隐蔽的形式——「流动性传染」。比如,当 BTC 的流动性枯竭时,做市商为了回笼资金,会抛售 ETH,导致 ETH 的流动性也出问题。这种连锁反应,在压力测试里必须建模进去。
我个人习惯的做法是:建立一个「流动性传染矩阵」,记录每个资产对另一个资产流动性的影响系数。这个系数可以通过历史数据里的「流动性同步下降」事件来估算。
# 流动性传染矩阵示例
contagion_matrix = {
'BTC': {'BTC': 1.0, 'ETH': 0.3, 'SOL': 0.1},
'ETH': {'BTC': 0.4, 'ETH': 1.0, 'SOL': 0.2},
'SOL': {'BTC': 0.2, 'ETH': 0.3, 'SOL': 1.0}
}
这个矩阵的意思是:当 BTC 的流动性下降 10% 时,ETH 的流动性会额外下降 3%(因为 0.3 * 10%)。你想想看,如果多个资产同时出问题,这个传染效应会像滚雪球一样越滚越大。
好了,三类风险因子都讲完了。最后用一张图来总结一下它们之间的关系:
这张图展示了三类风险因子之间的「共振」关系。在压力测试里,你不能孤立地测试每一个因子,而要把它们组合起来。比如,流动性枯竭会导致价格跳变,价格跳变会推高相关性,相关性上升又会引发跨资产传染——这是一个闭环。
我个人习惯的做法是:先单独测试每个因子的极端场景,然后再做「组合压力测试」,把三个因子同时推到极限。只有通过了这种「地狱模式」的测试,你的做市策略才算真正有了抗风险能力。