期权专题9:雪球期权(一)(BSM+蒙特卡罗定价)

   

目录

1. 前期介绍

1.1 定价思路

1.2 雪球要素

1.3 标的定价

2. 代码复现

2.1 价格路径

2.1.1 单次路径

2.1.2 多次路径

2.2 雪球定价

2.2.1 加入要素

2.2.2 估值定价

3. 完整代码


  雪球期权是近两年来备受青睐的衍生品投资标的之一,几个月前就想写一篇关于它的文章。但是因为其定价模型确实比较复杂,无论在思路和代码复现上都存在一定的难度,直到今日方才鼓起勇气一试。由于自我认知上的局限,分享的内容可能不够准确或者全面,望读者见谅,同时对有疑惑的地方欢迎探讨交流。

1. 前期介绍

1.1 定价思路

    雪球期权本质上是障碍期权的组合,关于其一些基础性的介绍此处省略(网上相关介绍太多)。本文选用蒙特卡罗模拟法来实现雪球定价的净值化,定价的核心思路主要是以下三点:

1. 使用蒙特卡罗模拟出雪球标的未来可能出现的路径。

2. 根据雪球要素,计算每一条路径的折现净值。

3. 计算所有路径下折现净值的均值,即作为雪球定价的净值。

    同时,还需要格外注意的一点是,雪球定价存在2种状态:一种是发生敲入状态下的,一种是未发生敲入状态下的。

1.2 雪球要素

  本文选用经典雪球作为案例,其要素如下:

雪球要素
挂钩标的中证500指数(399905.SZ)
存续期12个月
封闭期3
敲入线1.03
敲出线0.75
敲入观察频率每日
敲出观察频率每月
票息(年化)20%

  同时,为了简化定价模型的复杂程度,本文做出以下假设:

1. 假设波动率的数值为常数,值为22%(近五年指数年化波动率均值)。

2. 假设无风险利率为2.5%,一年的交易日为252天。

3. 假设不考虑股指升贴水,展期带来的损益。

4. 假设不考虑交易过程中所有的摩擦成本。

1.3 标的定价

    假设标的满足对数正态分布的条件,将微分方程引入BSM模型,可以推导出雪球所挂钩标的对应的定价公式。

    使用蒙特卡罗的思路,对上述公式进行逐逐步的模拟迭代,可以获得未来标的可能出现的价格路径。

2. 代码复现

   为了计算的速度以及整体过程的简洁性,考虑将数据抽象出来,使用矩阵的思路来复现整个过程。考虑到文章的适读性,本章节将诸多步骤进行细化,尽可能将过程解释清楚,读者可根据自身理解,对各个小节的内容进行选择性的详略阅读。

2.1 价格路径

2.1.1 单次路径

  此小节为步骤的细化分享,可选择性阅读。

  首先设置基本的参数,得到初始的路径矩阵。

import numpy as np

# 设置参数,分别是无风险利率,波动率和剩余期限
r = 0.025
vol = 0.22
residual_day = 252
# 设置初始值,需要模拟的路径数量
St = 1
path_num = residual_day
# 创建矩阵,并把初始值设为1,将时间差折算成年
s_path = np.zeros((path_num, 1))
s_path[0] = 1
Tt = 1/252 # 折算成年的时间差,此处以交易日为单位

  得到的结果如下:

    s_path是一个252行,1列的矩阵。接下来,根据初始值1,迭代矩阵的第二个数据,考虑到随机数可能产生异常的数值,因此需要对迭代的数据进行涨跌停的限制。

# 由初始值迭代第二个数据
N = np.random.standard_normal(1)  # 标准正态分布随机数
ST = s_path[0] * np.exp((r - 0.5 * vol ** 2) * Tt + vol * np.sqrt(Tt) * N)
# 进行涨跌停的限制
max_st = s_path[0] * 1.1
min_st = s_path[0] * 0.9
new_st = np.where(ST < min_st,  min_st, ST)
new_st = np.where(ST > max_st, max_st, new_st)
s_path[1] = new_st

对应得到的结果是:

  通过上述代码进行循环迭代,得到单次路径的所有数据:

for i in range(1,path_num):
    N = np.random.standard_normal(1)  # 标准正态分布随机数
    ST = s_path[i-1] * np.exp((r - 0.5 * vol ** 2) * Tt + vol * np.sqrt(Tt) * N)
    # 进行涨跌停的限制
    max_st = s_path[i-1] * 1.1
    min_st = s_path[i-1] * 0.9
    new_st = np.where(ST < min_st, min_st, ST)
    new_st = np.where(ST > max_st, max_st, new_st)
    s_path[i] = new_st

   当前即可得到对应单次的完整路径,对应的结果为:

  将s_path对应得到的数据进行可视化:

# 单次路径可视化
import matplotlib.pyplot as plt

plt.rcParams['font.sans-serif'] = ['Microsoft YaHei']
plt.rcParams['axes.unicode_minus'] = False
x = [x+1 for x in range(path_num)]
plt.plot(x,s_path,label='单次路径')
plt.legend()
plt.show()

  对应等到的结果为:

2.1.2 多次路径

    由于单次的路径存在较大的随机性,因此需要进行多次的模拟,此处暂时设定模拟次数为5000次。如果在单次路径的基础上添加循环继续模拟其他的路径,速度相对会比较慢,因此考虑还是使用矩阵来进行迭代。

   首先设立基本参数,得到基础的路径矩阵。

import numpy as np

# 设置参数,分别是无风险利率,波动率和剩余期限
r = 0.025
vol = 0.22
residual_day = 252
# 设置初始值,需要模拟的路径数量
St = 1
path_num = residual_day
times = 5000 # 模拟次数
# 创建矩阵,并把初始值设为1,将时间差折算成年
s_path = np.zeros((path_num, times))
s_path[0] = 1
Tt = 1/252 # 折算成年的时间差,此处以交易日为单位

  对应得到的结果为:

   此处的s_path是一个252行(代表日期),5000列(代表路径数量)。接下来,演示一下第二列价格的生成方式。

N = np.random.standard_normal(times)  # 标准正态分布随机数
ST = s_path[0] * np.exp((r - 0.5 * vol ** 2) * Tt + vol * np.sqrt(Tt) * N)
# 进行涨跌停的限制
max_st = s_path[0] * 1.1
min_st = s_path[0] * 0.9
new_st = np.where(ST < min_st,  min_st, ST)
new_st = np.where(ST > max_st, max_st, new_st)
s_path[1] = new_st

  对应得到的解惑为:

   到此处,个人感觉基本将路径生成的思路说清楚了。后续不再进行赘述,开始封装一个函数,来实现路径模拟的功能。

import numpy as np
import matplotlib.pyplot as plt


def get_s_path(St, r, vol, residual_day, times):
    '''
    模拟标的未来可能出现的价格路径
    :param St: int,标的初始价格,例如1
    :param r: int, 无风险利率,例如0.02
    :param vol: int, 波动率,例如0.02
    :param residual_day: int, 剩余期限(单位为天),例如252
    :param times: int, 模拟次数,例如5000
    :return:  价格路径对应的矩阵
    '''
    path_num = residual_day
    s_path = np.zeros((path_num, times))
    s_path[0] = St
    Tt = 1 / 252
    for i in range(1, path_num):
        N = np.random.standard_normal(times)  # 标准正态分布随机数
        ST = s_path[i - 1] * np.exp((r - 0.5 * vol ** 2) * Tt + vol * np.sqrt(Tt) * N)
        # 进行涨跌停的限制
        max_st = s_path[i - 1] * 1.1
        min_st = s_path[i - 1] * 0.9
        new_st = np.where(ST < min_st, min_st, ST)
        new_st = np.where(ST > max_st, max_st, new_st)
        s_path[i] = new_st
    return s_path


if __name__ == '__main__':
    r = 0.025
    vol = 0.22
    residual_day = 252
    St = 1
    times = 5000
    price_path = get_s_path(St, r, vol, residual_day, times)

   对应得到的结果是:

 将得到的结果进行可视化:

    plt.rcParams['font.sans-serif'] = ['Microsoft YaHei']
    plt.rcParams['axes.unicode_minus'] = False
    x = [x+1 for x in range(residual_day)]
    for i in range(times):
        plt.plot(x,price_path[:,i])
    plt.title('标的价格模拟路径'+'('+'模拟次数:'+str(times)+')')
    plt.show()

2.2 雪球定价

2.2.1 加入要素

   在获取标的价格的模拟路径后,需要在路径上添加雪球对应的要素结构。此处代码续接上一小节的price_path。

    # 雪球要素
    knock_out = 1.03  # 敲出价格
    knock_in = 0.75  # 敲入价格
    coupon = 0.2  # 年化票息
    lock_time = 3  # 锁定期,单位为月
    T = 1  # 期限,折算为年
    all_time = T * 252  # 期限,折算为天
    survival_day = all_time - residual_day  # 已存续期限

    # 利用要素对路径进行判断
    one_path = price_path[:, 0]
    # 价格低于敲入价格的交易日对应的序号
    knock_in_day = np.where(one_path <= knock_in)
    # 价格高于敲出价格的交易日对应的序号
    knock_out_day = np.where(one_path >= knock_out)
    # 上述每个交易日的序号需要加上雪球已存续期限
    new_out_day = knock_out_day[0] + survival_day
    # 判断上述日期是否是观察日,1年252个交易日,因此一个月设定为12
    obs_may_day = new_out_day[new_out_day % 21 == 0]
    # 由于有锁定期,需要剔除前三个月的敲出观察日
    obs_day = obs_may_day[obs_may_day / 21 > 3]

  此处,需要注意两点:

1. 未使用具体日期来作为观察日的检测,由于数据本身是模拟的,同时模拟的次数较大。因此采取估算的思路来处理观察日与使用具体日期的差异不大,同时这样处理能有效提升运算的效率。

2. 由于敲出观察日事先是固定的,因此对于已存续一段时间的雪球,其敲出观察日的序号需要加上已存续的交易日,这样才能使得观察日的序号对称。举个例子:某雪球已经存续60个交易日,在对第61个交易日进行估值时,模拟路径对应的第一个交易日,实际是作为雪球的第61个交易日。

   上述思路确实有点绕,难以用文字完全表述清楚,只能靠读者自身去理解感悟,有种‘名可名,非常名’的意思。

2.2.2 估值定价

   定价计算需要区分雪球在已存续的时间里是否发生过敲入。首先,讨论常规的情况,即在当前估值节点前,雪球未发生过敲入。

    status = 'out'
    # 过去未发生敲入
    if status == 'out':
        # 情况1:未来发生过敲出
        if len(obs_day) > 0:
            t = obs_day[0]
            re = coupon * (t / 252) * np.exp(-r * t / 252)
            nav = 1 * (1 + re)  # 1表示期初的净值
        else:
            # 情况2:未来未发生敲入和敲出
            if len(knock_in_day[0]) == 0:
                re = coupon * np.exp(-r * T)
                nav = 1 + re
            # 情况3:未来未发敲出,但发生敲入
            else:
                # 期末净值大于等于1
                if one_path[-1] >= 1:
                    re = 0
                    nav = 1 * (1 + re)
                # 期末净值小于1
                elif one_path[-1] < 1:
                    re = (one_path[-1] - 1) * np.exp(-r * T)
                    nav = 1 * (1 + re)

   接下来,讨论另外一种情况,在当前估值节点前,雪球发生过敲入。相比于未发生过敲入的雪球,发生过敲入的雪球不存在未敲入和敲出的情况,即不具备获取全额票息的可能。

    elif status == 'in':
        # 情况1:未来发生过敲出
        if len(obs_day) > 0:
            t = obs_day[0]
            re = coupon * (t / 252) * np.exp(-r * t / 252)
            nav = 1 * (1 + re)  # 1表示期初的净值
        else:
            # 情况2:未来未发生过敲出
            # 期末净值大于等于1
            if one_path[-1] >= 1:
                re = 0
                nav = 1 * (1 + re)
            # 期末净值小于1
            elif one_path[-1] < 1:
                re = (one_path[-1] - 1) * np.exp(-r * T)
                nav = 1 * (1 + re)

   在上述代码的基础上,循环5000次,计算所有nav的均值,即对应当前节点雪球的预估净值。 

3. 完整代码

    前文讲的相对较细,也显得有些凌乱,希望整体上是说清楚了。此处,对以上代码进行封装处理,以便查阅代码的完整性。

import numpy as np


def get_s_path(St, r, vol, residual_day, times):
    '''
    模拟标的未来可能出现的价格路径
    :param St: int,标的初始价格,例如1
    :param r: int, 无风险利率,例如0.02
    :param vol: int, 波动率,例如0.02
    :param residual_day: int, 剩余期限(单位为天),例如252
    :param times: int, 模拟次数,例如5000
    :return:  价格路径对应的矩阵
    '''
    path_num = residual_day
    s_path = np.zeros((path_num, times))
    s_path[0] = St
    Tt = 1 / 252
    for i in range(1, path_num):
        N = np.random.standard_normal(times)  # 标准正态分布随机数
        ST = s_path[i - 1] * np.exp((r - 0.5 * vol ** 2) * Tt + vol * np.sqrt(Tt) * N)
        # 进行涨跌停的限制
        max_st = s_path[i - 1] * 1.1
        min_st = s_path[i - 1] * 0.9
        new_st = np.where(ST < min_st, min_st, ST)
        new_st = np.where(ST > max_st, max_st, new_st)
        s_path[i] = new_st
    return s_path


def get_one_nav(status, coupon, T, r, residual_day, one_path, knock_in, knock_out, lock_time):
    '''
    获取雪球单次的模拟路径下,对应的净值
    :param status: str,雪球过去的状态,'out'表示过去未发生过敲入,'in'表示过去发生过敲入
    :param coupon: int,雪球对应的年化票息,例如0.2
    :param T: int,雪球对应的期限(折算成年),例如1
    :param r: int,无风险利率,例如0.025
    :param residual_day: int,剩余天数,例如252
    :param one_path: 矩阵,单次对应的模拟路径
    :param knock_in: int,雪球对应的敲入价格
    :param knock_out: int,雪球对应的敲出价格
    :return: int,预估的雪球净值
    '''
    all_time = T * 252  # 期限,折算为天
    survival_day = all_time - residual_day  # 已存续期限
    # 价格低于敲入价格的交易日对应的序号
    knock_in_day = np.where(one_path <= knock_in)
    # 价格高于敲出价格的交易日对应的序号
    knock_out_day = np.where(one_path >= knock_out)
    # 上述每个交易日的序号需要加上雪球已存续期限
    new_out_day = knock_out_day[0] + survival_day
    # 判断上述日期是否是观察日,1年252个交易日,因此一个月设定为12
    obs_may_day = new_out_day[new_out_day % 21 == 0]
    # 由于有锁定期,需要剔除前三个月的观察日
    obs_day = obs_may_day[obs_may_day / 21 > lock_time]

    # 过去未发生敲入
    if status == 'out':
        # 情况1:未来发生过敲出
        if len(obs_day) > 0:
            t = obs_day[0]
            re = coupon * (t / 252) * np.exp(-r * t / 252)
            nav = 1 * (1 + re)  # 1表示期初的净值
        else:
            # 情况2:未来未发生敲入和敲出
            if len(knock_in_day[0]) == 0:
                re = coupon * np.exp(-r * T)
                nav = 1 + re
            # 情况3:未来未发敲出,但发生敲入
            else:
                # 期末净值大于等于1
                if one_path[-1] >= 1:
                    re = 0
                    nav = 1 * (1 + re)
                # 期末净值小于1
                elif one_path[-1] < 1:
                    re = (one_path[-1] - 1) * np.exp(-r * T)
                    nav = 1 * (1 + re)

    elif status == 'in':
        # 情况1:未来发生过敲出
        if len(obs_day) > 0:
            t = obs_day[0]
            re = coupon * (t / 252) * np.exp(-r * t / 252)
            nav = 1 * (1 + re)  # 1表示期初的净值
        else:
            # 情况2:未来未发生过敲出
            # 期末净值大于等于1
            if one_path[-1] >= 1:
                re = 0
                nav = 1 * (1 + re)
            # 期末净值小于1
            elif one_path[-1] < 1:
                re = (one_path[-1] - 1) * np.exp(-r * T)
                nav = 1 * (1 + re)
    else:
        nav = 0
    new_value = nav
    return new_value


def get_snowball_nav(status, coupon, T, r, residual_day, knock_in, knock_out, lock_time, price_path):
    # 获取多次模拟路径下雪球的估值
    # price_path:模拟的标的路径
    # 其余参数和get_one_nav相同
    nav_list = []
    times = np.array([price_path]).shape[-1]  # 价格矩阵的列数

    for num in range(times):
        one_path = price_path[:, num]
        one_nav = get_one_nav(status, coupon, T, r, residual_day, one_path, knock_in, knock_out, lock_time)
        nav_list.append(one_nav)

    return np.mean(nav_list)


if __name__ == '__main__':
    r = 0.025
    vol = 0.22
    residual_day = 252
    St = 1
    times = 5000
    price_path = get_s_path(St, r, vol, residual_day, times)
    # 雪球要素
    knock_out = 1.03  # 敲出价格
    knock_in = 0.75  # 敲入价格
    coupon = 0.2  # 年化票息
    lock_time = 3  # 锁定期,单位为月
    T = 1  # 期限,折算为年
    status_in = 'in'
    in_nav = get_snowball_nav(status_in, coupon, T, r, residual_day, knock_in, knock_out, lock_time, price_path)
    status_out = 'out'
    out_nav = get_snowball_nav(status_out, coupon, T, r, residual_day, knock_in, knock_out, lock_time, price_path)

  对应得到的结果为:

   关于定价部分的代码已经实现,文章篇幅已经较长,因此决定在后续的文章中继续分享代码的实际应用,以及各个自变量对雪球估值的影响等。

本期分享到此结束,有何问题欢迎交流。

评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值