第十四章:冲击成本模拟:蒙特卡洛模拟方法、场景生成、压力测试
冲击成本这东西,说白了就是个「黑箱」。你下了一笔大单,市场到底会怎么走?没人能给你一个确定答案。我做了这么多年量化,最深的体会就是:别指望精确预测,要学会模拟各种可能性。
这一章,我们就来聊聊怎么用蒙特卡洛方法给冲击成本建模。嗯,说白了就是——用大量随机场景,把不确定性变成可量化的风险。
14.1 为什么需要蒙特卡洛模拟?
传统的冲击成本模型,比如Almgren-Chriss,给的是一个期望值。但实际交易中,你更关心的是:最坏情况下会亏多少?
我遇到过这样一个案例:一个客户做ETF套利,按Almgren模型算出来冲击成本只有2个基点,觉得稳了。结果那天市场流动性骤降,实际冲击成本飙到了15个基点,直接亏掉了一周的利润。
为什么会这样?因为传统模型假设市场状态是稳定的。但真实市场会变——流动性会枯竭,波动率会飙升,订单簿会瞬间失衡。这些极端情况,只有蒙特卡洛模拟才能捕捉到。
14.2 蒙特卡洛模拟的基本框架
我个人习惯把蒙特卡洛模拟拆成三步:
- 场景生成——模拟市场状态(波动率、流动性、订单簿形状)
- 交易执行——在每种场景下模拟你的订单执行过程
- 统计汇总——收集所有场景的冲击成本,分析分布
下面这张图展示了整个流程:
14.3 场景生成:怎么模拟市场状态?
场景生成是整个模拟的基石。你想想看,如果场景本身就不靠谱,后面的分析全是白搭。
我一般会生成三类场景:
14.3.1 波动率场景
波动率是冲击成本的核心驱动因素。高波动率意味着市场深度更浅,滑点更大。
常用的生成方法:
- 历史抽样法:从过去N天的波动率数据中随机抽取。简单,但可能遗漏极端情况。
- 参数化模型:假设波动率服从某种分布(如对数正态),然后生成随机数。我习惯用GARCH模型来生成,因为它能捕捉波动率聚集效应。
- 跳跃扩散过程:在随机波动率的基础上,加入跳跃项。用来模拟「黑天鹅」事件。
14.3.2 流动性场景
流动性比波动率更难模拟。为什么?因为流动性变化是非连续的——可能瞬间从「充裕」变成「枯竭」。
我常用的方法:
- 基于订单簿特征的生成:从历史订单簿中提取「买卖价差」、「深度斜率」、「挂单量」等特征,然后对这些特征进行联合抽样。
- 状态转移模型:把流动性分为「高、中、低」三个状态,用马尔可夫链模拟状态切换。我在做ETF做市时发现,流动性状态往往有很强的自相关性——一旦进入低流动性状态,通常会持续一段时间。
# 伪代码:流动性状态转移模拟
states = ['high', 'medium', 'low']
transition_matrix = [
[0.85, 0.12, 0.03], # high -> high, medium, low
[0.10, 0.80, 0.10], # medium -> high, medium, low
[0.02, 0.08, 0.90] # low -> high, medium, low
]
current_state = 'high'
for i in range(252): # 模拟一年
prob = transition_matrix[states.index(current_state)]
current_state = np.random.choice(states, p=prob)
# 根据状态生成对应的流动性参数
14.3.3 订单簿形状场景
这个比较细,但很重要。不同股票的订单簿形状差异很大——大盘股通常是「浅而宽」,小盘股是「深而窄」。
我建议用参数化订单簿模型:
| 参数 | 含义 | 典型范围 |
|---|---|---|
| α(深度衰减率) | 挂单量随价格远离中间价衰减的速度 | 0.5 ~ 2.0 |
| β(价差弹性) | 价差对订单量的敏感度 | 0.1 ~ 0.5 |
| γ(不平衡度) | 买卖两侧挂单量的不对称程度 | -0.3 ~ 0.3 |
生成场景时,我会从历史数据中估计这些参数的分布,然后随机抽样。嗯,这里要注意——参数之间往往有相关性。比如深度衰减率高的股票,价差弹性通常也大。所以要用Copula或者Cholesky分解来保持相关性结构。
14.4 压力测试:最坏情况分析
蒙特卡洛模拟跑完,你会得到冲击成本的完整分布。但真正有价值的是分布的尾部——也就是最坏的那5%或1%的情况。
我把它叫做「压力测试场景」。具体做法:
- 定义压力条件:比如波动率跳升3倍、流动性下降80%、买卖价差扩大5倍。
- 生成极端场景:在蒙特卡洛模拟中,强制让某些参数进入极端状态。
- 计算条件冲击成本:只统计这些极端场景下的冲击成本。
14.5 实战案例:某中盘股的冲击成本模拟
拿一个实际案例来说。某中盘股,日均成交额2亿,我们计划交易500万。
模拟参数设置:
- 模拟次数:5000次
- 波动率场景:70%历史抽样 + 30%GARCH(1,1)
- 流动性场景:三状态马尔可夫链
- 订单簿形状:参数化模型,参数从历史数据估计
模拟结果:
| 统计量 | 冲击成本(bps) |
|---|---|
| 均值 | 8.3 |
| 中位数 | 7.1 |
| 95%分位数 | 18.6 |
| 99%分位数 | 27.4 |
| 最大值 | 42.1 |
看到这个结果,你可能会说:「均值才8.3个基点,还行啊。」但注意看99%分位数——27.4个基点,是均值的3倍多。这意味着每100次交易,就有1次冲击成本超过27个基点。
这就是蒙特卡洛模拟的价值——它告诉你风险不是平均的,而是有厚尾的。
14.6 代码实现要点
最后给点代码层面的建议。蒙特卡洛模拟的核心是「快」——5000次模拟,每次都要生成场景、执行交易、计算成本,性能很关键。
# 核心优化技巧
# 1. 向量化操作,避免循环
# 2. 预分配内存
# 3. 用numba或cython加速热点函数
import numpy as np
from numba import jit
@jit(nopython=True)
def simulate_one_scenario(vol, spread, depth, order_size):
"""单次场景模拟(用numba加速)"""
# 生成价格路径
price_path = np.random.randn(100) * vol
# 模拟成交
executed_price = price_path + spread/2 + order_size/depth
# 计算冲击成本
impact = (executed_price - price_path[0]) / price_path[0]
return impact
# 批量运行
n_scenarios = 5000
results = np.zeros(n_scenarios)
for i in range(n_scenarios):
vol = generate_vol_scenario()
spread = generate_spread_scenario()
depth = generate_depth_scenario()
results[i] = simulate_one_scenario(vol, spread, depth, 500000)
好了,蒙特卡洛模拟的核心思路就这些。记住:模拟不是预测,而是理解风险的全貌。下次做交易决策前,不妨先跑个5000次模拟,看看你的策略在极端情况下能不能扛得住。