对冲策略及Python实现 -- 潘登同学的Quant笔记
不成功的对冲思路
- 裸头寸(naked position):如果一个期权卖出者不做任何对冲,而只持有期权的空头,称其持有一个裸头寸; 显然存在极大的风险,如果标的资产的走势不利,期权卖出者的损失可能会远超期权卖价。
- 抵补头寸(covered position):期权卖出者可以在卖出期权的同时买入对应数量的标的资产。虽然这样做有效地规避了股价上涨的风险(以call option为例),但是仍然会面临股价下跌的风险;
- 止损策略(stop-loss strategy):具体来说,就是当卖掉的期权处在虚值状况时,持有裸头寸,而当卖掉的期权处在实值状况时,持有抵补头寸。这种方法的缺点有二:
- 尽管每次股票的买卖都发生在 K 元,但不同时点的交易金额的贴现值是不一样的;
- 因为股价涨跌的不可预测性,所以不断的买卖也会带来不小的成本(尤其是股价多次穿越期权执行价格的时候)。
Delta 对冲(Delta Hedge)
- 简单回顾:用Stock与Bond复制Derivation(复制法)
组合$(Stock,Bond)$ 比例为$(\triangle,B)$
$$ \begin{cases} C_u = \triangle uS_0 + Be^r \ C_d = \triangle dS_0 + Be^r \ \end{cases} $$ 直接解出 $$ \begin{cases} \triangle = \frac{C_u-C_d}{(u-d)S_0} \ B = \frac{uC_d-dC_u}{e^r(u-d)} \ \end{cases} $$ 由于这个组合完全复制了衍生品在 1 时刻的支付,由无套利的原理,这个组合 0 时刻的价格就应该等于衍生品 0 时刻的价格。因此,衍生品在 0 时刻的价格应为 $$ C_0 = \triangle S_0 + B = e^{-r}[\frac{e^{r}-d}{u-d}C_u + \frac{u-e^{r}}{u-d}C_d] $$
定义:Delta
- 是期权价格对标的资产价格的偏导数。它衡量了期权价格对标的资产价格变化的敏感性。 $$ \triangle = \frac{\partial{C}}{\partial{S}} $$
Delta 对冲
- 因此,为了对冲一份买入期权合约的空头(short position),只需要用$\triangle$股股票的多头(long position)和价值 B 的无风险债券的多头即可。这就是 Delta 对冲(delta hedge)。通过这种方式构造的包括期权空头、股票和债券多头的投资组合,其价值并不随股票价格的波动而波动。这样的组合叫做 Delta 中性(delta neutral)的组合。 $$ \Pi = \triangle S - C + B t \ \frac{\partial{\Pi}}{\partial{S}} = \frac{\partial{C}}{\partial{S}} - \frac{\partial{C}}{\partial{S}} = 0 $$ 所谓“对冲”,是指通过一个反向的头寸来对冲别的某个头寸。我们要对冲买入期权的空头,就需要一个买入期权的多头。而这个买入期权的多头实际上是用股票和债券复制出来的期权。这就是 Delta 对冲的核心含义——用复制的衍生品的头寸来抵消掉一个反向的衍生品头寸。
Delta 对冲的关键
- 将组合的$\triangle$调成0、
Delta对冲的特点
以看涨期权为例,当股价变高时,衍生品的$\triangle$会随之增大(股价远低于行权价时,股价上涨一点,衍生品的价格变动没那么明显),那么需要的买入用于对冲的股票就越多,那么Delta对冲就有一个很明显的特点:
- 追涨杀跌: 这里的追涨杀跌是完全理性的行为。但在特定的市场状况下,这会加大市场的波动。
因为每天的股价都在波动,人为构造的这个组合的$\triangle$也会随之改变,那么要做到Delta对冲,就必须
- 每天收盘的时候调仓,将自己的组合$\triangle$调为0,也称调至Delta中性;
动态对冲的实现有赖于市场的持续交易。市场参与者在买入或卖出衍生品之后,需要持续不断的交易来对冲掉衍生品头寸带来的风险。这个过程中,如果市场交易停止(比如因为 911 这样的重大突发事件),那么动态对冲就无法实现,将会让很多之前衍生品头寸变成裸头寸,极大增加市场参与者所面临的风险。因此,越是发生重大突发事件的时候,越需要确保具有充足的流动性可以完成参与者的交易。这是市场监管者在重大事件发生时的首要责任。否则,容易引发市场参与者的大面积破产,引发系统性金融危机。
Delta对冲也可以用于套利交易,具体操作是
- 如果投资者在市场上发现了一个被错误定价的衍生品,那就可以一方面在市场上买卖这个衍生品,同时又通过动态对冲构造出这个衍生品的现金流。这样,投资者就可以在市场提供的衍生品和自己构造的衍生品之间无风险套利。
组合的Delta
前面介绍了用标的资产来对冲某一衍生品的方法。这一方法可以拓展至对某个由衍生品组成的投资组合的对冲。这需要计算投资组合的 Delta。与单个衍生品定理相类似的,某个总价值为 $\Pi$ 的投资组合的 Delta,就是$\frac{\partial{\Pi}}{\partial{S}}$。一个包含 n 种期权的组合,其 Delta 可以由如下公式计算 $$ \triangle_{\Pi} = \sum_{i=1}^n w_i \triangle_i $$
Gamma对冲(Gamma Hedge)
一个组合的 Gamma($\Gamma$),是这个组合的 Delta 对标的资产价格的偏导数,也即组合价值对标的资产价格的二阶偏导数 $$ \Gamma = \frac{\partial{\triangle}}{\partial{S}} = \frac{\partial^2{C}}{\partial{S^2}} $$ $\Gamma$表示了$\triangle$变化的快慢,组合的 $\Gamma$ 越小,意味着组合的 Delta 对标的资产价格的变化越不敏感,因而变化越缓慢。这样,为了保持组合处在 Delta 中性状态所需的调整也不需要太频繁。相反,如果组合的 $\Gamma$ 很大,组合就很容易因为标的资产价格的变化而明显偏离 Delta 中性的状态。这样一来,如果不对组合做频繁调整,就会面临相当大的风险。
与组合的Delta与Gamma的关系与债券的久期与凸性的关系很类似,当股票价格从 $S$ 变为 $S'$时,Delta 对冲假设期权价格从 $C$ 变化到 $C'$。但事实上,期权价格是从 $C$ 变化到 $C''$。$C'$与 $C''$之间的差异就是“对冲误差”(hedging error)。对冲误差的大小取决于描述期权价格与股票价格关系的这根曲线的曲率(curvature)。组合的 Gamma 越大,这条曲线的曲率就越大,Delta 对冲的对冲误差就越大。所以,Gamma 有时被业内人员称为期权的曲率。
对于股票来说,他对自己的二阶导为0:
- 所以在调整$\Gamma$的时候,加减股票是没有用的;但是加减衍生品是可以的...
给出调$\Gamma$的思路
- 某个组合$\triangle_1= 0,\Gamma_1\neq 0$,加衍生品,将其调成中性$\Gamma_1+w_a\Gamma_a = 0$
- 因为加了衍生品,Delta就不等于0了,要加减股票将Delta再调至中性
- 重复上面的过程,总能将Delta与Gamma调至接近0
但是一般来说,去调Gamma是比较奢侈的,因为没有那么多衍生品适合用于调Gamma,所以相对于Gamma,Delta是更常见的,Delta基本收盘必做,Gamma则不一定。
我们可以将
- Delta 中性理解为,对两次组合调整之间所发生的股票价格的小变化的保护。
- 而 Gamma 中性则是对两次组合调整之间股票价格的大变化的保护。
Vega对冲(Vega Hedge)
组合的 Vega($V$)是组合价值对标的资产波动率的偏导数 $$ V = \frac{\partial{\Pi}}{\partial{\sigma}} $$
- 股票的$V$为0(现在股价与过去的波动率有关)
- 衍生品的$V$不一定是0
- 预期股票未来波动率越大,期权越值钱
- 预期股票未来波动率越小,期权越不值钱
那些 Vega 的绝对值很高的组合,对标的资产波动率的变化很敏感。由于标的资产本身的 Vega是 0,所以为了改变组合的 Vega,需要在组合中加入期权。类似前面的 Gamma;
其他希腊字母
$$ \Theta = \frac{\partial{\Pi}}{\partial{t}} \ \quad \ \Rho = \frac{\partial{\Pi}}{\partial{r}} \ \quad \ Speed = \frac{\partial{\Gamma}}{\partial{S}} \ \quad \ Charm = \frac{\partial{\triangle}}{\partial{t}} \ \quad \ Colour = \frac{\partial{\Gamma}}{\partial{t}} \ \quad \ Vanna = \frac{\partial{\triangle}}{\partial{\sigma}} \ \quad \ Volga = \frac{\partial{V}}{\partial{\sigma}} \ \quad \ $$
组合保险
投资者所需要的期权未必是存在的。比如,投资者拥有由一篮子股票组成的一个组合,希望能够对组合下跌的风险加以对冲。对于这种个性化的组合的期权在市场上恐怕很难找到。但前面的介绍提示我们,就算没有现成的期权在市场上交易,投资者完全也可以通过动态交易股票,构造出一个这样的期权来保护自己的组合。这就是组合保险(portfolio insurance)的思路。
组合保险本质上就是做Delta对冲, 保持手里的一篮子股票不动,通过买卖股债将Delta调至中性即可;
组合保险的几个特点
- 现实中,组合保险更容易通过股指期货来实现。在现实中的投资组合不太可能只包含一只股票,而往往由多只股票组成。这时,更加方便的方法是通过股指期货来进行对冲。但股指期货反映的是市场指数(如沪深 300、标普 500)的走势。市场指数的组成未必与投资经理自己组合的构成一致。此时,可以先计算出自己组合的 Beta。如果 Beta 为 1,则按照前面计算的数量进行对冲即可。
- 组合保险很可能放大市场波动。从前面的例子中可以看到,为了对冲股票头寸的下跌风险,需要提前卖出一部分股票头寸。在动态对冲的过程中,如果股票价格持续下跌,依照前面的组合保险策略,就需要不断卖出股票头寸,从而形成越跌越卖的格局。在有些时候,这会放大市场的波动。
- 组合保险表现为股票的交易策略
Python实现
基于之前多期二叉树的实现,在上面加一个计算Delta值的功能再简单不过,直接给出完整代码;
import numpy as np
# 模型参数
sigma = 0.26 # 隐含波动率 用246个交易日的roll average计算得到的序列的方差
# 资产价格波动率 σ 被定义为,使得在△t 的时长上计算的回报率波动标准差等于σ(△t)^(1/2)
r = 0.03 # 无风险利率 用七天回购利率的246个交易日的roll average计算得到的序列的均值
t = 246 # 一年的交易日数
r_delta_t = r / t # 日利率
underlying_asset_price_0 = 2.906
underlying_asset_price_1 = 2.5 # 2.906
u = np.exp(sigma * 1/t ** (1/2))
d = 1/u
q = (np.exp(r_delta_t)-d) / (u-d) # 风险中性概率
k = 2.8 # 行权价
class TreeNode():
def __init__(self, key, s, left=None, right=None,time=0):
self.key = key
self.asset_price = s
self.options_price = None
self.delta = 0 # 新加 因为最后一期的delta为0,所以不初始化为None
self.left = left
self.right = right
self.time = time
self.leftparent = None
self.rightparent = None
class CombineBinaryTree():
def __init__(self,total_time,underlying_asset_price,
is_European=True,is_call=True):
self.underlying_asset_price = underlying_asset_price
self.rootnode = TreeNode('underlying_asset_price',underlying_asset_price)
self.u = u
self.d = d
self.k = k
self.q = q # 向右节点传播的概率
self.discount_rate = r_delta_t # 折现率
self.total_time = total_time
self.is_European = is_European
self.is_call = is_call
self._build([self.rootnode],total_time)
def _build(self, node, total_time):
parent_node = node
now = parent_node[0].time+1
if now <= total_time:
children_node = [TreeNode(f'{now}-u ** {now} d ** {i}',
self.u**(now-i)*self.d**(i)*self.underlying_asset_price,time=now,)
for i in range(now+1)]
i,j = 0,0
while i < len(parent_node) and j < len(children_node):
parent_node[i].right = children_node[j]
children_node[j].leftparent = parent_node[i]
j += 1
parent_node[i].left = children_node[j]
children_node[j].rightparent = parent_node[i]
i += 1
self._build(children_node,total_time)
def BFS(self, item, phase='资产价格'):
'''
遍历打印node的某个item
'''
result = np.zeros((self.total_time+1,self.total_time+1))
queue = [self.rootnode]
i = 0
while queue:
temp = queue.pop(0)
i += 1
if temp.left:
if temp.right not in queue:
queue.append(temp.right)
if temp.left not in queue:
queue.append(temp.left)
row = self._calrow(i)
col = temp.time
if item == 'asset':
result[row,col] = round(temp.asset_price,3)
self.all_asset_price = result
elif item == 'options':
result[row,col] = round(temp.options_price,3)
self.all_options_price = result
# ----以下为新内容----- #
elif item == 'delta':
result[row,col] = round(temp.delta,3)
self.all_delta_price = result
print(f'{phase}: \n',result)
def _cal_option_value(self,node):
if node.options_price:
return node.options_price
elif node.time == self.total_time:
if self.is_call:
node.options_price = max(node.asset_price - self.k, 0)
else:
node.options_price = max(self.k - node.asset_price, 0)
return node.options_price
else:
if self.is_European:
node.options_price = 1/(1+self.discount_rate) * (self.q * self._cal_option_value(node.right) + (1 - self.q) * self._cal_option_value(node.left))
else:
if self.is_call:
# 一般来说美式买入期权是不会提前行权的, 这里只是为了保证完整性, 最后可以验证一下
node.options_price = max(node.asset_price - self.k, 1/(1+self.discount_rate) * (self.q * self._cal_option_value(node.right) + (1 - self.q) * self._cal_option_value(node.left)))
else:
node.options_price = max(self.k - node.asset_price, 1/(1+self.discount_rate) * (self.q * self._cal_option_value(node.right) + (1 - self.q) * self._cal_option_value(node.left)))
return node.options_price
def cal_option_value(self):
self._cal_option_value(self.rootnode)
def _calrow(self,num):
for i in range(1,num+1):
if num <= i:
break
else:
num -= i
return num - 1
## ------以下为更新内容------ ##
def cal_delta(self):
if not self.rootnode.options_price:
self.cal_option_value()
self._cal_delta(self.rootnode)
def _cal_delta(self,node):
if node.right: # 判断是否有下一时期 根据公式计算delta = (Cu-Cd)/(u-d)S0
node.delta = abs(node.right.options_price - node.left.options_price) / ((self.u - self.d)*node.asset_price)
self._cal_delta(node.right)
self._cal_delta(node.left)
return
print('European call options:')
tree = CombineBinaryTree(8,underlying_asset_price_0)
tree.BFS('asset','资产价格')
tree.cal_option_value()
tree.BFS('options','期权价格')
tree.cal_delta()
tree.BFS('delta','Delta数值')
print('American call options:')
tree = CombineBinaryTree(8,underlying_asset_price_0,is_European=False)
tree.BFS('asset','资产价格')
tree.cal_option_value()
tree.BFS('options','期权价格')
tree.cal_delta()
tree.BFS('delta','Delta数值')
print('European put options:')
tree = CombineBinaryTree(8,underlying_asset_price_1,is_call=False)
tree.BFS('asset','资产价格')
tree.cal_option_value()
tree.BFS('options','期权价格')
tree.cal_delta()
tree.BFS('delta','Delta数值')
print('American put options:')
tree = CombineBinaryTree(8,underlying_asset_price_1,is_European=False,is_call=False)
tree.BFS('asset','资产价格')
tree.cal_option_value()
tree.BFS('options','期权价格')
tree.cal_delta()
tree.BFS('delta','Delta数值')
最终可以看到,Delta确实是满足追涨杀跌的特性,与理论一致(最后一期的时候期权结束了,所以delta人为地设置为0)
而计算别的对冲的方式也很类似,不过想要很简单的得到特定的值,解析解很关键,某些需要用到B-S公式得出,幸运的是,B-S公式我们会!!!