0%

Wing-Model Volatility Skew Manager

Wing-Model

期权隐含波动率的Wing-Model模型是由Orc提供给期权做市商的一套管理波动率的模型,本质上是一个分段的一元二次方程和线性扩展。Wing-Model通过四个区间的端点𝑑𝑐(1 + 𝑑𝑠𝑚)、𝑑𝑐、𝑢𝑐和𝑢𝑐(1 + 𝑢𝑠𝑚),将波动率微笑曲线分成六部分。

wing

wing-model的抛物线拟合部分为分段的一元二次函数,

\[\begin{equation} \begin{aligned} vol(x) &= vc + x \times sc + x^2 \times pc \\ vol(x) &= vc + x \times sc + x^2 \times cc \end{aligned} \end{equation}\]

数据分析显示由于A股listed-option的行权价格较少,一般直接使用二段的抛物线方程拟合即可。国内的一些期权做市软件里面GUI界面嵌的是四段的拟合函数,直接使用dynamic skew manager的方式拟合平滑曲线,从软件设置里面直接没有构建swimming skew这些功能。


在抛物线(parabola)区间,wing-model的校对函数(方程)为关于\(\mathrm{sc}, \mathrm{pc} / \mathrm{cc}\)的二元一次线性方程,凸函数存在全局最优化解,光滑区间(smoothing ranges)同理。

wing-model本质上是一元二次分段函数,wing-model的符号表达式在数值分界点连续、一阶导数连续、二阶导数不连续,由方程式符号表达式即可确定,所以在数值拟合(凸优化求解)过程不需要考虑连续性约束条件设置。


Butterfly Arbitrage

根据Jim Gatheral在Arbitrage-free SVI volatility surfaces提到的无套利观点和算法对wing-model进行公式推导分析

Absence of this arbitrage corresponds to the existence of a risk-neutral martingale measure and the classical definition of no static arbitrage, as developed in [12] or [8]. In this section, we consider only one slice of the implied volatility surface, i.e. the map \(k \to \omega (k, t)\) for a given fixed maturity \(t > 0\).

  • A slice is said to be free of butterfly arbitrage if the corresponding density is non-negative.

A slice is free of butterfly arbitrage if and only if \(g(k) \geq 0\) for all \(k \in \mathbb{R}\) and \(\lim_{k \to \infty} d_{+}(k) \to \infty\) \[ g(k):=(1-\frac{k\omega'(k)}{2\omega(k)})^2 - \frac{\omega'(k)^2}{4}(\frac{1}{\omega(k)}+\frac{1}{4})+\frac{\omega''(k)}{2} \]


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time : 2020/5/22 11:12 PM
# @Author : 稻草人
# @contact : dybeta2021@163.com
# @File : wing_model.py
# @Desc : orc wing model
from functools import partial

from numpy import ndarray, array, arange, zeros, ones, argmin, minimum, maximum, clip
from numpy.linalg import norm
from numpy.random import normal
from scipy.interpolate import interp1d
from scipy.optimize import minimize


class WingModel(object):
@staticmethod
def skew(moneyness: ndarray, vc: float, sc: float, pc: float, cc: float, dc: float, uc: float, dsm: float,
usm: float) -> ndarray:
"""

:param moneyness: converted strike, moneyness
:param vc:
:param sc:
:param pc:
:param cc:
:param dc:
:param uc:
:param dsm:
:param usm:
:return:
"""
assert -1 < dc < 0
assert dsm > 0
assert 1 > uc > 0
assert usm > 0
assert 1e-6 < vc < 10 # 数值优化过程稳定
assert -1e6 < sc < 1e6
assert dc * (1 + dsm) <= dc <= 0 <= uc <= uc * (1 + usm)

# volatility at this converted strike, vol(x) is then calculated as follows:
vol_list = []
for x in moneyness:
# volatility at this converted strike, vol(x) is then calculated as follows:
if x < dc * (1 + dsm):
vol = vc + dc * (2 + dsm) * (sc / 2) + (1 + dsm) * pc * pow(dc, 2)
elif dc * (1 + dsm) < x <= dc:
vol = vc - (1 + 1 / dsm) * pc * pow(dc, 2) - sc * dc / (2 * dsm) + (1 + 1 / dsm) * (
2 * pc * dc + sc) * x - (pc / dsm + sc / (2 * dc * dsm)) * pow(x, 2)
elif dc < x <= 0:
vol = vc + sc * x + pc * pow(x, 2)
elif 0 < x <= uc:
vol = vc + sc * x + cc * pow(x, 2)
elif uc < x <= uc * (1 + usm):
vol = vc - (1 + 1 / usm) * cc * pow(uc, 2) - sc * uc / (2 * usm) + (1 + 1 / usm) * (
2 * cc * uc + sc) * x - (cc / usm + sc / (2 * uc * usm)) * pow(x, 2)
elif uc * (1 + usm) < x:
vol = vc + uc * (2 + usm) * (sc / 2) + (1 + usm) * cc * pow(uc, 2)
else:
raise ValueError("x value error!")
vol_list.append(vol)
return array(vol_list)

@classmethod
def loss_skew(cls, params: [float, float, float], x: ndarray, iv: ndarray, vega: ndarray, vc: float, dc: float,
uc: float, dsm: float, usm: float):
"""

:param params: sc, pc, cc
:param x:
:param iv:
:param vega:
:param vc:
:param dc:
:param uc:
:param dsm:
:param usm:
:return:
"""
sc, pc, cc = params
vega = vega / vega.max()
value = cls.skew(x, vc, sc, pc, cc, dc, uc, dsm, usm)
return norm((value - iv) * vega, ord=2, keepdims=False)

@classmethod
def calibrate_skew(cls, x: ndarray, iv: ndarray, vega: ndarray, dc: float = -0.2, uc: float = 0.2, dsm: float = 0.5,
usm: float = 0.5, is_bound_limit: bool = False,
epsilon: float = 1e-16, inter: str = "cubic"):
"""

:param x: moneyness
:param iv:
:param vega:
:param dc:
:param uc:
:param dsm:
:param usm:
:param is_bound_limit:
:param epsilon:
:param inter: cubic inter
:return:
"""

vc = interp1d(x, iv, kind=inter, fill_value="extrapolate")([0])[0]

# init guess for sc, pc, cc
if is_bound_limit:
bounds = [(-1e3, 1e3), (-1e3, 1e3), (-1e3, 1e3)]
else:
bounds = [(None, None), (None, None), (None, None)]
initial_guess = normal(size=3)

args = (x, iv, vega, vc, dc, uc, dsm, usm)
residual = minimize(cls.loss_skew, initial_guess, args=args, bounds=bounds, tol=epsilon, method="SLSQP")
assert residual.success
return residual.x, residual.fun

@staticmethod
def sc(sr: float, scr: float, ssr: float, ref: float, atm: ndarray or float) -> ndarray or float:
return sr - scr * ssr * ((atm - ref) / ref)

@classmethod
def loss_scr(cls, x: float, sr: float, ssr: float, ref: float, atm: ndarray, sc: ndarray) -> float:
return norm(sc - cls.sc(sr, x, ssr, ref, atm), ord=2, keepdims=False)

@classmethod
def fit_scr(cls, sr: float, ssr: float, ref: float, atm: ndarray, sc: ndarray,
epsilon: float = 1e-16) -> [float, float]:
init_value = array([0.01])
residual = minimize(cls.loss_scr, init_value, args=(sr, ssr, ref, atm, sc), tol=epsilon, method="SLSQP")
assert residual.success
return residual.x, residual.fun

@staticmethod
def vc(vr: float, vcr: float, ssr: float, ref: float, atm: ndarray or float) -> ndarray or float:
return vr - vcr * ssr * ((atm - ref) / ref)

@classmethod
def loss_vc(cls, x: float, vr: float, ssr: float, ref: float, atm: ndarray, vc: ndarray) -> float:
return norm(vc - cls.vc(vr, x, ssr, ref, atm), ord=2, keepdims=False)

@classmethod
def fit_vcr(cls, vr: float, ssr: float, ref: float, atm: ndarray, vc: ndarray,
epsilon: float = 1e-16) -> [float, float]:
init_value = array([0.01])
residual = minimize(cls.loss_vc, init_value, args=(vr, ssr, ref, atm, vc), tol=epsilon, method="SLSQP")
assert residual.success
return residual.x, residual.fun

@classmethod
def wing(cls, x: ndarray, ref: float, atm: float, vr: float, vcr: float, sr: float, scr: float, ssr: float,
pc: float, cc: float, dc: float, uc: float, dsm: float, usm: float) -> ndarray:
"""
wing model

:param x:
:param ref:
:param atm:
:param vr:
:param vcr:
:param sr:
:param scr:
:param ssr:
:param pc:
:param cc:
:param dc:
:param uc:
:param dsm:
:param usm:
:return:
"""
vc = cls.vc(vr, vcr, ssr, ref, atm)
sc = cls.sc(sr, scr, ssr, ref, atm)
return cls.skew(x, vc, sc, pc, cc, dc, uc, dsm, usm)


class ArbitrageFreeWingModel(WingModel):
@classmethod
def calibrate(cls, x: ndarray, iv: ndarray, vega: ndarray, dc: float = -0.2, uc: float = 0.2, dsm: float = 0.5,
usm: float = 0.5, is_bound_limit: bool = False, epsilon: float = 1e-16, inter: str = "cubic",
level: float = 0, method: str = "SLSQP", epochs: int = None, show_error: bool = False,
use_constraints: bool = False) -> ([float, float, float], float):
"""

:param x:
:param iv:
:param vega:
:param dc:
:param uc:
:param dsm:
:param usm:
:param is_bound_limit:
:param epsilon:
:param inter:
:param level:
:param method:
:param epochs:
:param show_error:
:param use_constraints:
:return:
"""
vega = clip(vega, 1e-6, 1e6)
iv = clip(iv, 1e-6, 10)

# init guess for sc, pc, cc
if is_bound_limit:
bounds = [(-1e3, 1e3), (-1e3, 1e3), (-1e3, 1e3)]
else:
bounds = [(None, None), (None, None), (None, None)]

vc = interp1d(x, iv, kind=inter, fill_value="extrapolate")([0])[0]
constraints = dict(type='ineq', fun=partial(cls.constraints, args=(x, vc, dc, uc, dsm, usm), level=level))
args = (x, iv, vega, vc, dc, uc, dsm, usm)
if epochs is None:
if use_constraints:
residual = minimize(cls.loss_skew, normal(size=3), args=args, bounds=bounds, constraints=constraints,
tol=epsilon, method=method)
else:
residual = minimize(cls.loss_skew, normal(size=3), args=args, bounds=bounds, tol=epsilon, method=method)

if residual.success:
sc, pc, cc = residual.x
arbitrage_free = cls.check_butterfly_arbitrage(sc, pc, cc, dc, dsm, uc, usm, x, vc)
return residual.x, residual.fun, arbitrage_free
else:
epochs = 10
if show_error:
print("calibrate wing-model wrong, use epochs = 10 to find params! params: {}".format(residual.x))

if epochs is not None:
params = zeros([epochs, 3])
loss = ones([epochs, 1])
for i in range(epochs):
if use_constraints:
residual = minimize(cls.loss_skew, normal(size=3), args=args, bounds=bounds,
constraints=constraints,
tol=epsilon, method="SLSQP")
else:
residual = minimize(cls.loss_skew, normal(size=3), args=args, bounds=bounds, tol=epsilon,
method="SLSQP")
if not residual.success and show_error:
print("calibrate wing-model wrong, wrong @ {} /10! params: {}".format(i, residual.x))
params[i] = residual.x
loss[i] = residual.fun
min_idx = argmin(loss)
sc, pc, cc = params[min_idx]
loss = loss[min_idx][0]
arbitrage_free = cls.check_butterfly_arbitrage(sc, pc, cc, dc, dsm, uc, usm, x, vc)
return (sc, pc, cc), loss, arbitrage_free

@classmethod
def constraints(cls, x: [float, float, float], args: [ndarray, float, float, float, float, float],
level: float = 0) -> float:
"""蝶式价差无套利约束

:param x: guess values, sc, pc, cc
:param args:
:param level:
:return:
"""
sc, pc, cc = x
moneyness, vc, dc, uc, dsm, usm = args

if level == 0:
pass
elif level == 1:
moneyness = arange(-1, 1.01, 0.01)
else:
moneyness = arange(-1, 1.001, 0.001)

return cls.check_butterfly_arbitrage(sc, pc, cc, dc, dsm, uc, usm, moneyness, vc)

"""蝶式价差无套利约束条件
"""

@staticmethod
def left_parabolic(sc: float, pc: float, x: float, vc: float) -> float:
"""

:param sc:
:param pc:
:param x:
:param vc:
:return:
"""
return pc - 0.25 * (sc + 2 * pc * x) ** 2 * (0.25 + 1 / (vc + sc * x + pc * x * x)) + (
1 - 0.5 * x * (sc + 2 * pc * x) / (vc + sc * x + pc * x * x)) ** 2

@staticmethod
def right_parabolic(sc: float, cc: float, x: float, vc: float) -> float:
"""

:param sc:
:param cc:
:param x:
:param vc:
:return:
"""
return cc - 0.25 * (sc + 2 * cc * x) ** 2 * (0.25 + 1 / (vc + sc * x + cc * x * x)) + (
1 - 0.5 * x * (sc + 2 * cc * x) / (vc + sc * x + cc * x * x)) ** 2

@staticmethod
def left_smoothing_range(sc: float, pc: float, dc: float, dsm: float, x: float, vc: float) -> float:
a = - pc / dsm - 0.5 * sc / (dc * dsm)

b1 = -0.25 * ((1 + 1 / dsm) * (2 * dc * pc + sc) - 2 * (pc / dsm + 0.5 * sc / (dc * dsm)) * x) ** 2
b2 = -dc ** 2 * (1 + 1 / dsm) * pc - 0.5 * dc * sc / dsm + vc + (1 + 1 / dsm) * (2 * dc * pc + sc) * x - (
pc / dsm + 0.5 * sc / (dc * dsm)) * x ** 2
b2 = (0.25 + 1 / b2)
b = b1 * b2

c1 = x * ((1 + 1 / dsm) * (2 * dc * pc + sc) - 2 * (pc / dsm + 0.5 * sc / (dc * dsm)) * x)
c2 = 2 * (-dc ** 2 * (1 + 1 / dsm) * pc - 0.5 * dc * sc / dsm + vc + (1 + 1 / dsm) * (2 * dc * pc + sc) * x - (
pc / dsm + 0.5 * sc / (dc * dsm)) * x ** 2)
c = (1 - c1 / c2) ** 2
return a + b + c

@staticmethod
def right_smoothing_range(sc: float, cc: float, uc: float, usm: float, x: float, vc: float) -> float:
a = - cc / usm - 0.5 * sc / (uc * usm)

b1 = -0.25 * ((1 + 1 / usm) * (2 * uc * cc + sc) - 2 * (cc / usm + 0.5 * sc / (uc * usm)) * x) ** 2
b2 = -uc ** 2 * (1 + 1 / usm) * cc - 0.5 * uc * sc / usm + vc + (1 + 1 / usm) * (2 * uc * cc + sc) * x - (
cc / usm + 0.5 * sc / (uc * usm)) * x ** 2
b2 = (0.25 + 1 / b2)
b = b1 * b2

c1 = x * ((1 + 1 / usm) * (2 * uc * cc + sc) - 2 * (cc / usm + 0.5 * sc / (uc * usm)) * x)
c2 = 2 * (-uc ** 2 * (1 + 1 / usm) * cc - 0.5 * uc * sc / usm + vc + (1 + 1 / usm) * (2 * uc * cc + sc) * x - (
cc / usm + 0.5 * sc / (uc * usm)) * x ** 2)
c = (1 - c1 / c2) ** 2
return a + b + c

@staticmethod
def left_constant_level() -> float:
return 1

@staticmethod
def right_constant_level() -> float:
return 1

@classmethod
def _check_butterfly_arbitrage(cls, sc: float, pc: float, cc: float, dc: float, dsm: float, uc: float, usm: float,
x: float, vc: float) -> float:
"""检查是否存在蝶式价差套利机会,确保拟合time-slice iv-curve 是无套利(无蝶式价差静态套利)曲线

:param sc:
:param pc:
:param cc:
:param dc:
:param dsm:
:param uc:
:param usm:
:param x:
:param vc:
:return:
"""
# if x < dc * (1 + dsm):
# return cls.left_constant_level()
# elif dc * (1 + dsm) < x <= dc:
# return cls.left_smoothing_range(sc, pc, dc, dsm, x, vc)
# elif dc < x <= 0:
# return cls.left_parabolic(sc, pc, x, vc)
# elif 0 < x <= uc:
# return cls.right_parabolic(sc, cc, x, vc)
# elif uc < x <= uc * (1 + usm):
# return cls.right_smoothing_range(sc, cc, uc, usm, x, vc)
# elif uc * (1 + usm) < x:
# return cls.right_constant_level()
# else:
# raise ValueError("x value error!")

if dc < x <= 0:
return cls.left_parabolic(sc, pc, x, vc)
elif 0 < x <= uc:
return cls.right_parabolic(sc, cc, x, vc)
else:
return 0

@classmethod
def check_butterfly_arbitrage(cls, sc: float, pc: float, cc: float, dc: float, dsm: float, uc: float, usm: float,
moneyness: ndarray, vc: float) -> float:
"""

:param sc:
:param pc:
:param cc:
:param dc:
:param dsm:
:param uc:
:param usm:
:param moneyness:
:param vc:
:return:
"""
con_arr = []
for x in moneyness:
con_arr.append(cls._check_butterfly_arbitrage(sc, pc, cc, dc, dsm, uc, usm, x, vc))
con_arr = array(con_arr)
if (con_arr >= 0).all():
return minimum(con_arr.mean(), 1e-7)
else:
return maximum((con_arr[con_arr < 0]).mean(), -1e-7)

参考