大家好,好久没更新了,我准备后面就一些相关场外衍生品的的研报进行复现,不仅是让自己加深对这一领域的理解,也希望国内这个发展迅速但不是特别大的市场能有更多的人能感兴趣甚至加入。这次我先就信达证券于明明老师的《雪球结构定价与风险深度分析》中的第一部分蒙特卡洛模拟对雪球定价进行复现,研报链接如下:
具体雪球产品的多种收益情况和详细内容可以从研报中学习。
这就是MC定价雪球的初始条件设置表,从表中可以得知该雪球的期限为1年,敲出水平为103%,敲入水平为85%,年化票息为20%,无风险利率为3%,标的波动率为13%,每日观察敲入,每月观察敲出,总模拟路径条数为300000。
话不多少,直接开干,蒙特卡洛模拟就是模拟多条路径来计算大数据下的产品价格应该是多少,利用均值来表示。本文代码部分借鉴了一定知乎内容,有兴趣的朋友可以去看看: 雪球定价方法之蒙特卡洛模拟(附Python代码) - 知乎据说雪球最近要将认购门槛提高到1000万,可见其火爆程度。那么今天我们结合研报用蒙特卡洛模拟法来看看现在的合约的票息是否合理,同时对雪球的结构进行梳理。 这里我们用到的研报是信达证券《雪球结构定价与风险…https://zhuanlan.zhihu.com/p/527393555
下面是本文的MonteCarlo模拟标准雪球定价代码:
import numpy as np
def MonteCarlo_Simulation(S0, r, T, vol, times, steps):
dt = float(T) / steps
price_paths = np.zeros((steps + 1, times))
price_paths[0] = S0
for i in range(1, steps + 1):
random_seed = np.random.standard_normal(times)
middle = price_paths[i - 1, 0:times] * np.exp((r - 0.5 * vol ** 2) * dt + vol * np.sqrt(dt) * random_seed)
uplimit = price_paths[i - 1] * 1.1 # 涨停设置
downlimit = price_paths[i - 1] * 0.9 # 跌停设置
trail = np.where(uplimit < middle, uplimit, middle)
trail = np.where(downlimit > middle, downlimit, trail)
price_paths[i, 0:times] = trail
return price_paths
def standard_snowball_pricing(price_paths, coupon, times):
payoff = np.zeros(times)
knock_out_times = 0
knock_in_times = 0
existence_times = 0
loss_times = 0
knockout_m_list = []
payoff_list = []
for i in range(times):
# 收盘价超过敲出线的交易日
knockout_day = np.where(price_paths[:, i] >= knockout_barrier)
# 收盘价超出敲出线的观察日(每月观察)
knockout_mday = knockout_day[0][knockout_day[0] % 21 == 0]
# 收盘价超出敲出线的观察日(超过封闭期)
knockout_mday_overlock = knockout_mday[knockout_mday / 21 > lock_period]
# 收盘价低于敲入线的观察日(每日观察)
knockin_day = np.where(price_paths[:, i] < knockin_barrier)
# 情景1:发生过向上敲出
if len(knockout_mday_overlock) > 0:
t = knockout_mday_overlock[0]
payoff[i] = coupon * (t/252) * np.exp(-r * t/252)
knock_out_times += 1
knockout_m_list.append(t/21)
payoff_list.append(payoff[i])
# 情景2:未敲出且未敲入
elif len(knockout_mday_overlock) == 0 and len(knockin_day[0]) == 0:
payoff[i] = coupon * np.exp(-r * T)
existence_times += 1
payoff_list.append(payoff[i])
# 情景3:只发生向下敲入,不发生向上敲出
elif len(knockin_day[0]) > 0 and len(knockout_mday_overlock) == 0:
# 只有向下敲入,没有向上敲出
payoff[i] = 0 if price_paths[-1][i] >= S0 else (price_paths[-1][i] - S0) * np.exp(-r * T)
knock_in_times += 1
payoff_list.append(payoff[i])
if payoff[i] < 0:
loss_times += 1
else:
print(i)
return payoff, knock_out_times, knock_in_times, existence_times, loss_times, knockout_m_list, payoff_list
if __name__=='__main__':
# 设置参数
np.random.seed(1)
vol = 0.13
T = 1
S0 = 1
r = 0.03
knockout_barrier = 1.03
knockin_barrier = 0.85
times = 300000
lock_period = 0
coupon = 0.2
steps = 252 * T
price_paths = MonteCarlo_Simulation(S0, r, T, vol, times, steps)
payoff, knock_out_times, knock_in_times, existence_times, loss_times, knockout_m_list, payoff_list = standard_snowball_pricing(price_paths, coupon, times)
price = sum(payoff)/len(payoff)
print('雪球价值: %f' % price)
print('敲出: %f' % (knock_out_times/times))
print('未敲入也未敲出: %f' % (existence_times / times))
print('敲入未敲出: %f' % (knock_in_times/times))
print('发生亏损: %f' % (loss_times/times))
print('最大亏损: %f' % (min(payoff_list)))
print('最大盈利: %f' % (max(payoff_list)))
print('平均敲出月份: %f' % (sum(knockout_m_list) / len(knockout_m_list)))
print('平均存续月份: %f' % (((times - len(knockout_m_list)) * 12 + sum(knockout_m_list)) / times))
大家可以利用np.random.seed来进行复现,代码跑出来的结果为:
研报原文的回测结果如下:
可以看出结果大致和研报结果都比较接近,最大的误差在于平均敲出月份,我的结果为3.51,研报结果为2.59,相差了近一个月,希望大家如果能够看出我代码中有什么错误的话,希望不吝指教,后面我也会继续更新PDE方法对雪球定价的研报复现。