合并二叉树进行期权定价 -- 潘登同学的Quant笔记
理论背景
合并二叉树对期权的定价是建立在单期模型向多期模型拓展的基础之上的,而多期模型与单期模型的最大区别在于: 单期模型只在最初做决策,而多期模型在多个时点连续作出投资决策; 为了将套利定价理论拓展到多期、动态的情形下,必须要满足的条件就是动态完备;
对于单期模型来说,资产个数至少大于状态的数量,市场才可能是完备的; 而在动态模型中,如果长存资产的数目不低于树中各个结点引出的直接后继节点数量的最大值,那么市场就是完备的;
合并二叉树
大家对二叉树都很熟悉,一个节点有两个子节点,子节点的个数与层数的关系就是$2^n$(从第0层算起), 那么最终的子节点个数就会呈指数级增加,而金融里面的资产价格变动往往是用乘积因子做变化的; 如从0时刻到1时刻,资产价格从$S_0$变化到$uS_0$或$dS_0$(u表示上升,d表示下降),到2时刻资产价格从$uS_0$变化到$uuS_0$或$udS_0$, 从$dS_0$变化到$udS_0$或$ddS_0$,可以发现其中的$udS_0$同时出现在1时刻的两个子节点上; 在不关心路径的前提下,可以将两个节点合并,如下图所示
但是有的期权不能这样,比如:回望期权,他是路径依赖的,取决于标的资产在期权有效期内的最低或最高价格;
叠期望定理
期权本质上是一种资产,但这个资产的payoff只有在最后一期才能知道(European options),所以要在第一期知道其期望就需要用到叠期望定理:
在t时刻对某随机变量$\tilde{x}$的期望应当严格地写成: $$ E[\tilde{x}|F_t]\quad , \quad F_t表示在t时刻拥有的信息 $$
叠期望定理表示为 $$ E_t[\tilde{x}] = E_t[E_{t+1}[\tilde{x}]] $$
Gisanov's Theorem(戈萨诺夫定理)
假设一个单位长度为$\triangle t$,在真实世界中的资产波动率为$\sigma$,在风险中性的概率下,在一个单位内股价回报率为$(u-1)$的概率为q,回报为$(d-1)$的概率为(1-q),运用公式$var(X) = E(X^2) - [E(X)]^2$ $$ q(u-1)^2 + (1-q)(d-1)^2 - [q(u-1)+(1-q)(d-1)]^2 = \sigma^2 \triangle t $$ 得到风险中性概率为 $$ q = \frac{e^{r \triangle t}-d}{u-d} $$ 注意上式的等号左边是风险中性世界的方差,等号右边却是真实世界的方差,这两个方差计算所用的概率都不一样,为什么能放到一起, Girsanov's Theorem告诉我们,在做概率测度变换的时候(如从真实世界概率转换到风险中性概率),资产价格收益率的均值一般会发生变化,但其波动率不变。
计算合并二叉树的关键点
对于European option的计算, 可以分为两步-正向过程和反向过程,正向过程是根据资产的波动率计算未来时刻资产价格,反向过程是根据最后一期的资产价格计算最后一期的期权价格,再用叠期望定理将期权一步步折现得到期权的现值; 接下来我们就用python实现以下过程:
模型参数
- $\sigma$: 隐含波动率 用246个交易日的roll average计算得到的序列的方差, 资产价格波动率 $\sigma$ 被定义为,使得在$\triangle t$ 的时长上计算的回报率波动标准差等于$\sigma\sqrt{\triangle t}$
- r: 无风险利率 用七天回购利率的246个交易日的roll average计算得到的序列的均值
- t: 一年的交易日数(246天)
- underlying_asset_price: 标的资产的现值
- k:行权价
中间结果
- u: 资产上涨的幅度
- d:资产下跌的幅度(假设d = 1/u)
- q: 风险中性概率
构建合并二叉树
构建二叉树节点
二叉树节点需要记录以下数据
- 当前的资产价格
- 当前的期权价格
- 当前的时刻
- 其子节点
- 其父节点
- 节点的名称(可有可无, 写了能分辨出是哪个节点)
class TreeNode():
'''
### @Author : Pan Deng
### @Time : 2022/10/02
'''
def __init__(self, key, s, left=None, right=None,time=0):
self.key = key
self.asset_price = s
self.options_price = None
self.left = left
self.right = right
self.time = time
self.leftparent = None
self.rightparent = None
构建合并二叉树
合并二叉树要有以下功能
- 根据期数将所有二叉树节点合并
- 进行正向传播计算资产价格
- 进行反向传播计算期权价格
所以二叉树需要有如下属性
- 标的资产的现值
- 上涨幅度,下跌幅度
- 行权价
- 风险中性概率
- 折现率
- 期权到期日
- 根节点(只要知道根节点就能知道后续的资产价格)
class CombineBinaryTree():
'''
### @Author : Pan Deng
### @Time : 2022/10/02
'''
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 # 是否是 Eurpean options
self.is_call = is_call # 是否是call options
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}',
u**(now-i)*d**(i)*self.underlying_asset_price,time=now,
u=now,d=i)
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)
然后就是一个工具方法,用于显示资产价格或期权价格,现在还没算到期权,后面会计算,计算完再调用该方法就能显示期权价格变化,关键点是
- 使用BFS从根节点进行搜索,将节点放入一个队列,依次从队列中取出节点,记录其资产价格,其子节点入队,直到队列为空;
- 将资产价格保存在一个矩阵中打印输出;
- 其中的
_calrow
方法是用于计算一个上三角矩阵,当元素按列放的时候,其行号的函数;
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
if item == 'options':
result[row,col] = round(temp.options_price,3)
self.all_options_price = result
print(f'{phase}: \n',result)
计算期权价格,关键点是
- 期权价值是从后往前计算的,所以要采用递归的方式进行;
- 对于American options的计算需要关注最优停时,也就是在每个地方判断一下当前行权的价值与未来行权的期望,选大的那个即可;
- 而对于Call options来说,根据Put-Call parity,American options是不会行权的,当然可以直接用European Options代替,但是为了严谨与完整,还是单独写了,后面有个验证的例子;
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)
完整代码
class TreeNode():
'''
### @Author : Pan Deng
### @Time : 2022/10/02
'''
def __init__(self, key, s, left=None, right=None,time=0):
self.key = key
self.asset_price = s
self.options_price = None
self.left = left
self.right = right
self.time = time
self.leftparent = None
self.rightparent = None
class CombineBinaryTree():
'''
### @Author : Pan Deng
### @Time : 2022/10/02
'''
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}',
u**(now-i)*d**(i)*self.underlying_asset_price,time=now,
u=now,d=i)
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
if item == 'options':
result[row,col] = round(temp.options_price,3)
self.all_options_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
算例
# 模型参数
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 # 行权价
print('European call options:')
tree = CombineBinaryTree(8,underlying_asset_price_0)
tree.BFS('asset','资产价格')
tree.cal_option_value()
tree.BFS('options','期权价格')
print('American call options:')
tree = CombineBinaryTree(8,underlying_asset_price_0,is_European=False)
tree.BFS('asset','资产价格')
tree.cal_option_value()
tree.BFS('options','期权价格')
print('European put options:')
tree = CombineBinaryTree(8,underlying_asset_price_1,is_call=False)
tree.BFS('asset','资产价格')
tree.cal_option_value()
tree.BFS('options','期权价格')
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','期权价格')
结果如下:
可以看出验证结果与理论一致....
文章写作不易,转载请注明出处....