基于最小二乘法蒙特卡洛模拟(LSM)可转换债券博弈条款的代码实现(Python)

部署运行你感兴趣的模型镜像

一、背景

在使用最小二乘法蒙特卡洛模拟对可转债进行定价过程中,可转债特殊条款的设置,是在路径遍历过程中较为复杂的地方,不同转债有不同的条款设计,有的更有利于发行人,有的设置更能吸引投资者。从日常工作角度,需要快速修改参数,实现对可转债的定价。

从上一篇文章《基于最小二乘法蒙特卡洛模拟(LSM)可转债定价原理及算法演示》中关于可转债特殊条款的介绍,可以认为(有条件赎回,强制转股)和(有条件回售,下修行权价)作为条款博弈的组合。

本篇即是便于参数修改和应用的角度,以上两对博弈条款进行实现。

二、条款实现

1、有条件赎回和强制转股

有条件赎回:在转股期限内,股票在任何连续【30】个交易日中至少【15】个交易日的收盘价格不低于当期转股价格的 【130%】。公司可按照面额+当期应计利息赎回转债。公司通过赎回,迫使投资人完成转股。

在触发有条件赎回条款时,此时往往转股价值大于赎回价格,有条件赎回条款仅仅是迫使投资者转股的条款,投资者也往往选择转股,因此可以认为赎回不会发生。

class CallClause(CBClause):
    
    def __init__(self, enabled=True, trigger_days=30, up_percent=0.3, required_days=30,  
                 conversion_period_start=126, conversion_period_end=1512):
        super().__init__(enabled)
        
        self.observation_window = trigger_days  # 观察窗口长度
        self.up_percent = up_percent      # 股价超过转股价的幅度(百分比,如0.3表示30%)
        self.required_days = required_days# 需满足触发条件的天数
        self.conversion_period_start = conversion_period_start  # 转股期开始时间
        self.conversion_period_end = conversion_period_end      # 转股期结束时间       
        
        self.trigger_counter = 0                # 触发计数器
        self.trigger_threshold = required_days  # 触发阈值
        self.call_count = 0                     # 记录触发次数
        self.call_path = []                     # 记录触发赎回路径
    
        
    def is_in_conversion_period(self, current_step):
        """判断是否在转股期内"""
        # 整个观察窗口都需要在转股期内
        return self.conversion_period_start + self.observation_window - 1 <= current_step <= self.conversion_period_end
    
    def check_trigger(self, path_data, current_step, path_conversion_price):        
        self.trigger_counter = 0
        if not self.enabled:
            return False
        
        # 判断整个观察窗口是否在转股期内
        if not self.is_in_conversion_period(current_step):
            return False
                
        # 检查观察窗口内是否满足触发条件        
        recent_prices = path_data[current_step - (self.observation_window - 1):current_step + 1]       
        
        conversion_price = path_conversion_price[current_step - (self.observation_window - 1):current_step + 1]
        # 计算触发天数
        for p, cp in zip(recent_prices, conversion_price):
            if p >= cp * (1 + self.up_percent):
                self.trigger_counter += 1
        
        if self.trigger_counter >= self.required_days:                        
            return True            
       
        return False
    
    def process_redemption(self, s_data, cashflows, conversion_price_table):
        
        n_paths, n_steps = s_data.shape        
        
        for path_idx in range(n_paths):
            path_data = s_data[path_idx, :] # 取当前路径的股价数据
            path_conversion_price = conversion_price_table[path_idx, :] # 取当前路径的转股价数据
            # 时点0为初始节点,从恰好进入观察窗口开始遍历
            for step in range(self.conversion_period_start + self.observation_window - 1, n_steps+1):
                # 检查当前路径是否满足赎回条款的触发条件
                trigger_result = self.check_trigger(path_data, step, path_conversion_price)
                if trigger_result:
                    # 记录触发信息                    
                    self.call_path.append(path_idx)                    
                    cashflows[path_idx, step] = 100 * path_data[step] / path_conversion_price[step]                    
                    self.call_count += 1 
                    break
               
        print('已处理赎回条款')
        return cashflows

2、有条件回售和下修转股价

上市公司发行可转债的目的是为了进行股权融资,因此可以认为在触发有条件回售条款时,上市公司往往下修行权价,有条件回售不会发生,而仅仅触发转股价下修条款时,发行人并不会下修行权价。这里需要注意的是,需要先遍历检测有条件回售和下修转股价,然后再遍历是否触发有条件赎回。同时,在遍历是否触发有条件回售时,在触发后需要继续遍历,即当前路径有可能多次触发有条件回售而下修转股价。而且需要注意,可转债条款中会有如下约定“如果出现转股价格向下修正的情况,则上述连续三十个交易日须从转股价格调整之后的第一个交易日起重新计算。”

class PutClause(CBClause):
    def __init__(self, enabled=True, trigger_days=30, down_percent=0.3, required_days=15, 
                 put_period_start=126, put_period_end=1512):
        
        super().__init__(enabled)
        self.observation_window = trigger_days # 观察窗口长度
        self.down_percent = down_percent # 股价低于转股价幅度(0-1)
        self.required_days = required_days # 需满足触发条件的天数
        self.put_period_start = put_period_start  # 回售期开始时间
        self.put_period_end = put_period_end      # 回售期结束时间  
        
        self.put_counter = 0  # 触发计数器
        self.put_path = []    # 记录触发回售路径

    def is_in_put_period(self, current_step):
        
        return self.put_period_start + self.observation_window - 1 <= current_step <= self.put_period_end

    def check_trigger(self, path_data, current_step, conversion_price):
        
        if not self.is_in_put_period(current_step):
            return False
            
        # 检查观察窗口内是否满足触发条件        
        recent_prices = path_data[current_step - (self.observation_window - 1):current_step + 1]
        recent_conversion_prices = conversion_price[current_step - (self.observation_window - 1):current_step + 1]
        
        # 检查股价是否低于转股价的(1-down_percent)倍
        trigger_days = sum(p < cp * (1 - self.down_percent) 
                          for p, cp in zip(recent_prices, recent_conversion_prices))
        
        if trigger_days >= self.required_days:
            
            return True
        
        return False

    def process_put(self, s_data, conversion_price_table):
        
        n_paths, n_steps = s_data.shape        
        
        for path_idx in range(n_paths):
            path_data = s_data[path_idx, :] 
            path_conversion_price = conversion_price_table[path_idx, :]             
            step = self.put_period_start + self.observation_window - 1            

            while step <= n_steps:
                # 检查当前路径是否满足赎回条款的触发条件
                # 同一路径可能多次触发
                trigger_result = self.check_trigger(path_data, step, path_conversion_price)
                
                if trigger_result:
                    avg_price = np.mean(path_data[step - 19:step + 1])
                    current_price = path_data[step]
                    new_conversion_price = max(avg_price, current_price)
                    conversion_price_table[path_idx, step + 1:] = new_conversion_price
                    self.put_path.append(path_idx)                                    

                    # 触发后跳过30个时间步长,“如果出现转股价格向下修正的情况,则上述连续三十个交易日须从转股价格调整之后的第一个交易日起重新计算。”
                    step += 30
                    continue  
                    
                step += 1  # 正常情况每次递增1               
        print('已处理回售条款')
        return conversion_price_table    

您可能感兴趣的与本文相关的镜像

Python3.8

Python3.8

Conda
Python

Python 是一种高级、解释型、通用的编程语言,以其简洁易读的语法而闻名,适用于广泛的应用,包括Web开发、数据分析、人工智能和自动化脚本

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值