#!/user/bin/env python
# -*- coding: utf-8 -*-
"""
Copyright (C) 2024 Galaxy Technologies
Licensed under the Apache License, Version 2.0
"""
import bisect
import numpy as np
import datetime
from pricelib.common.utilities.enums import RandsMethod, LdMethod, StatusType, ExerciseType
from pricelib.common.pricing_engine_base import McEngine
from pricelib.common.date_handler import global_evaluation_date


class MCPhoenixEngine_OLD(McEngine):
    """凤凰类 AutoCallable Monte Carlo模拟定价引擎，不支持变敲入、变敲出、变票息
            较雪球式AutoCallable，增加派息价格，少了敲出和红利票息
            每个派息（敲出）观察日，如果价格高于派息价格，派固定利息，如果发生敲出，合约提前结束；
            发生敲入后，派息方式不变，到期如果未敲出，结构为看跌空头
    """

    def __init__(self, stoch_process=None, n_path=100000, rands_method=RandsMethod.LowDiscrepancy,
                 antithetic_variate=True, ld_method=LdMethod.Sobol, seed=0, *,
                 s=None, r=None, q=None, vol=None, rands=None, s_paths=None, var_paths=None):
        """构造函数
        Args:
            stoch_process: 随机过程StochProcessBase对象
            n_path: int，MC模拟路径数
            rands_method: 生成随机数方法，RandsMethod枚举类，Pseudorandom伪随机数/LowDiscrepancy低差异序列
            antithetic_variate: bool，是否使用对立变量法
            ld_method: 若使用了低差异序列，指定低差异序列方法，LdMethod枚举类，Sobol序列/Halton序列
            seed: int，随机数种子
        在未设置stoch_process时，(stoch_process=None)，会默认创建BSMprocess，需要输入以下变量进行初始化
            s: float，标的价格
            r: float，无风险利率
            q: float，分红/融券率
            vol: float，波动率
        """
        super().__init__(stoch_process, n_path, rands_method=rands_method, antithetic_variate=antithetic_variate,
                         ld_method=ld_method, seed=seed, s=s, r=r, q=q, vol=vol, rands=rands, s_paths=s_paths,
                         var_paths=var_paths)
        # 以下为计算过程的中间变量
        self.prod = None  # Product产品对象
        self._obs_dates = None  # 根据估值日，将敲出观察日转化为List[int]，交易日期限
        self._pay_dates = None  # 根据起始日，将支付日转化为List[float]，年化自然日期限，用于计算票息
        self._n_path = None  # 随机数矩阵的路径数、期末盈亏测算=价格向量数、期末现值测算=1
        self._past_coupon = np.zeros(0, dtype=int)  # 标记过去已发生派息的数组
        # 经过估值日截断的列表，例如prod.barrier_out有22个，存续一年时估值，_barrier_out只有12个
        self._barrier_out = None
        self._barrier_in = None
        self._barrier_yield = None
        self._coupon = None

    # pylint: disable=too-many-locals
    def calc_present_value(self, prod, t=None, spot=None):
        """计算现值
        Args:
            prod: Product产品对象
            t: datetime.date，估值日; 如果是None，则使用全局估值日globalEvaluationDate
            spot: float，估值日标的价格，如果是None，则使用随机过程的当前价格
        Returns: float，现值
                        不支持盈亏测算：输入spot = np.ndarray一维价格向量，t=终止日，输出payoff向量
        """
        self.prod = prod
        calculate_date = global_evaluation_date() if t is None else t
        _maturity_business_days = prod.trade_calendar.business_days_between(calculate_date, prod.end_date)
        obs_dates = prod.obs_dates.count_business_days(calculate_date)  # 估值日距离观察日的交易日天数
        obs_dates_list = prod.obs_dates.date_schedule
        pay_dates = prod.pay_dates.count_calendar_days(calculate_date)  # 估值日距离支付日的自然日天数

        try:  # 确定当前日期在观察日中的位置
            past_num = obs_dates_list.index(calculate_date)
        except ValueError:
            # 如果当前日期不在观察日中，找到最近的位置
            past_num = bisect.bisect_right(obs_dates_list, calculate_date)

        if spot is None:
            spot = self.process.spot()
        elif spot != self.process.spot():
            self.reset_paths_flag()  # 重置路径标志位，重新生成路径

        # 当估值日就是终止日时，直接用spot计算payoff，无需随机数矩阵
        if calculate_date == prod.stop_date:
            self.s_paths = np.array([[spot, ]])  # spot是一个数值，计算期末payoff
        else:  # 随机价格路径矩阵
            self.s_paths = self.path_generator(n_step=_maturity_business_days, spot=spot,
                                               t_step_per_year=prod.t_step_per_year).copy()
        self._n_path = self.s_paths.shape[1]

        if (calculate_date == prod.stop_date) and (calculate_date < prod.end_date):  # 提前敲出的参数需要特殊处理
            # 只有 obs_dates 中 = 0的观察日才会保留，之后的观察日因为敲出而消失了
            self._obs_dates = np.array([0])
            # 只取前past_num + 1个支付日, 之后的支付日因为敲出而消失了
            self._pay_dates = np.array([num / prod.annual_days.value for num in pay_dates][:past_num + 1])
            _obs_idx = obs_dates_list.index(calculate_date)
            self._barrier_out = np.array([prod.barrier_out[_obs_idx], ])
            self._barrier_in = np.array([prod.barrier_in[_obs_idx], ])
            self._barrier_yield = np.array([prod.barrier_yield[_obs_idx], ])
            self._coupon = np.array([prod.coupon[_obs_idx], ])
        else:  # 有机会持有到到期日
            self._obs_dates = np.array([num for num in obs_dates if num >= 0])  # 只取 num >= 0 (未来)的观察日
            # 包括 num < 0的支付日，从div_history取出已经发生过派息的观察日, 时间的负值可以直接输入折现因子函数
            self._pay_dates = np.array([num / prod.annual_days.value for num in pay_dates])
            # 经过估值日截断的列表，例如prod.barrier_out有22个，存续一年时估值，_barrier_out只有12个
            self._barrier_out = prod.barrier_out[-len(self._obs_dates):].copy()
            self._barrier_in = prod.barrier_in[-len(self._obs_dates):].copy()
            self._barrier_yield = prod.barrier_yield[-len(self._obs_dates):].copy()
            self._coupon = prod.coupon[-len(self._obs_dates):].copy()

        # 已经发生过派息的观察日
        div_history_list = prod.div_history.date_schedule
        if div_history_list:  # 如果该列表非空
            div_history_list = list(set(div_history_list))  # 日期去重
            div_history_list.sort()  # 从小到大排序
            prod.div_history.date_schedule = div_history_list  # 更新div_history

            # 获取过去的观察日
            past_obs_dates = obs_dates_list[:past_num]
            past_coupon = np.zeros(past_num, dtype=int)  # 过去past_num个观察日的标记数组
            # 标记过去发生派息的日期
            for i, day in enumerate(past_obs_dates):
                if day in div_history_list:
                    past_coupon[i] = self._n_path
            self._past_coupon = past_coupon
        else:
            self._past_coupon = np.zeros(past_num, dtype=int)

        # 统计各个情景的payoff
        self._cal_knock_out_date()
        self._cal_yield_date()
        self._cal_knock_in_scenario()
        result = self._cal_payoff()

        if (calculate_date == prod.end_date) and isinstance(spot, np.ndarray):
            return result  # 终止日, spot是价格向量, 则返回盈亏测算向量，即每个价格对应的现值，而不是平均值
        else:
            return np.mean(result)

    def _cal_knock_out_date(self):
        """统计每条路径敲出时间"""
        # 计算每条价格路径最小敲出时间
        barrier_out = np.tile(self._barrier_out, (self._n_path, 1)).T
        barrier_out_matrix = np.tile(np.arange(self._obs_dates.shape[0]), (self._n_path, 1)).T
        barrier_out_matrix = np.where(self.s_paths[self._obs_dates, :] >= barrier_out,
                                      barrier_out_matrix.astype(int), np.inf)
        # 返回每一列的最小值，即为每条路径的敲出时间
        self.knock_out_time_idx = np.min(barrier_out_matrix, axis=0)
        # 统计哪些路径属于发生了敲出的情景(布尔索引)
        self.knock_out_scenario = (self.knock_out_time_idx != np.inf)
        # 每条路径持有时长
        hold_time_idx = self.knock_out_time_idx.copy()
        # 对于未敲出情形，持有到期，因此将索引设置为最后一个观察日的索引，如24个观察日，则指定为23
        hold_time_idx[~self.knock_out_scenario] = self._obs_dates.size - 1
        # 转成整数
        self.hold_time_idx = hold_time_idx.astype(int)

    def _cal_yield_date(self):
        """统计每条路径发生派息的时间"""
        # 统计哪些派息日，标的价格在派息线上方(即该派息日发生派息)
        barrier_yield = np.tile(self._barrier_yield, (self._n_path, 1)).T
        coupon_time_idx = (self.s_paths[self._obs_dates, :] > barrier_yield)
        # 将发生敲出之后的派息bool(不包括敲出当天)由True改为False
        self.coupon_bool = np.where(np.arange(coupon_time_idx.shape[0])[:, np.newaxis] > self.knock_out_time_idx,
                                    False, coupon_time_idx)

    def _cal_knock_in_scenario(self):
        """统计哪些路径属于敲入未敲出的情景"""
        prod = self.prod
        if prod.status == StatusType.DownTouch:  # 之前已敲入，之后未敲出的路径全部算作敲入
            self.knock_in_scenario = np.where(self.knock_out_scenario, False, True)
        else:
            if prod.in_obs_type == ExerciseType.American:  # 每日观察敲入(凤凰)
                # 排除发生了敲出的路径，统计哪些路径属于敲入未敲出
                knock_in_level = np.array(self._barrier_in).repeat(
                    np.diff(np.append(np.zeros((1,)), self._obs_dates)).astype(int))
                knock_in_level = np.append(self._barrier_in[0], knock_in_level)
                knock_in_level = np.tile(knock_in_level, (self._n_path, 1)).T
                knock_in_time_idx = (self.s_paths <= knock_in_level)
                # 统计哪些路径属于敲入未敲出
                knock_in_bool = np.any(knock_in_time_idx, axis=0)
            elif prod.in_obs_type == ExerciseType.European:  # 到期观察敲入(DCN/FCN)
                knock_in_bool = (self.s_paths[-1] <= self._barrier_in[-1])
            else:
                raise ValueError(f"不支持的敲入观察类型{prod.in_obs_type}，仅支持美式(凤凰)/欧式(DCN/FCN)")
            # 将发生敲出之后的敲入由True改为False
            self.knock_in_scenario = np.where(self.knock_out_scenario, False, knock_in_bool)

    def _cal_payoff(self):
        """统计每条路径的收益加总"""
        # 不同派息日的票息分别计算贴现因子，得到派息的现值
        discount_factor = np.empty(self._pay_dates.size)
        for i, pay_d in enumerate(self._pay_dates):
            discount_factor[i] = self.process.interest.disc_factor(pay_d)  # 每月的派息会复利
            # discount_factor[i] = self.process.interest.disc_factor(self._pay_dates[-1])  # 每月的派息不复利
        discounted_coupon = self._coupon * self.prod.s0 * discount_factor

        # payoff汇总
        payoff = 0
        # 1.未敲入的部分（敲出/未敲入未敲出），到期还本
        payoff += np.sum(discount_factor[self.hold_time_idx[~self.knock_in_scenario]]
                         ) * self.prod.margin_lvl * self.prod.s0

        # 2.派息payoff
        paths_sum_coupon_times = np.sum(self.coupon_bool, axis=1)
        # 拼接过去已发生的派息次数数组
        full_coupon = np.concatenate([self._past_coupon, paths_sum_coupon_times])
        # assert len(full_coupon) != len(discounted_coupon), "票息数组与折现因子数组长度应该相等"
        payoff += np.sum(full_coupon * discounted_coupon)

        # 3.敲入，承担跌幅损失
        s_vec = self.s_paths[-1, self.knock_in_scenario].copy()
        payoff += (np.sum(self.prod.margin_lvl * self.prod.s0 + self.prod.parti_in *
                          np.maximum(
                              np.minimum(s_vec - self.prod.strike_upper, 0),
                              self.prod.strike_lower - self.prod.strike_upper))
                   ) * discount_factor[-1]
        payoff /= self._n_path
        return payoff


class MCPhoenixEngine(McEngine):
    """凤凰类 AutoCallable Monte Carlo模拟定价引擎，不支持变敲入、变敲出、变票息
            较雪球式AutoCallable，增加派息价格，少了敲出和红利票息
            每个派息（敲出）观察日，如果价格高于派息价格，派固定利息，如果发生敲出，合约提前结束；
            发生敲入后，派息方式不变，到期如果未敲出，结构为看跌空头
    """

    def __init__(self, stoch_process=None, n_path=100000, rands_method=RandsMethod.LowDiscrepancy,
                 antithetic_variate=True, ld_method=LdMethod.Sobol, seed=0, *,
                 s=None, r=None, q=None, vol=None, rands=None, s_paths=None, var_paths=None):
        """构造函数
        Args:
            stoch_process: 随机过程StochProcessBase对象
            n_path: int，MC模拟路径数
            rands_method: 生成随机数方法，RandsMethod枚举类，Pseudorandom伪随机数/LowDiscrepancy低差异序列
            antithetic_variate: bool，是否使用对立变量法
            ld_method: 若使用了低差异序列，指定低差异序列方法，LdMethod枚举类，Sobol序列/Halton序列
            seed: int，随机数种子
        在未设置stoch_process时，(stoch_process=None)，会默认创建BSMprocess，需要输入以下变量进行初始化
            s: float，标的价格
            r: float，无风险利率
            q: float，分红/融券率
            vol: float，波动率
        """
        super().__init__(stoch_process, n_path, rands_method=rands_method, antithetic_variate=antithetic_variate,
                         ld_method=ld_method, seed=seed, s=s, r=r, q=q, vol=vol, rands=rands, s_paths=s_paths,
                         var_paths=var_paths)
        # 以下为计算过程的中间变量
        self.prod = None  # Product产品对象
        self._obs_dates = None  # 根据估值日，将敲出观察日转化为List[int]，交易日期限
        self._div_obs_dates = None  # 根据估值日，将派息观察日转化为List[int]，交易日期限
        self._pay_dates = None  # 根据起始日，将支付日转化为List[float]，年化自然日期限，用于计算票息
        self._n_path = None  # 随机数矩阵的路径数、期末盈亏测算=价格向量数、期末现值测算=1
        self._past_coupon = np.zeros(0, dtype=int)  # 标记过去已发生派息的数组
        # 经过估值日截断的列表，例如prod.barrier_out有22个，存续一年时估值，_barrier_out只有12个
        self._barrier_out = None
        self._barrier_in = None
        self._barrier_yield = None
        self._coupon = None

    # pylint: disable=too-many-locals
    def calc_present_value(self, prod, t=None, spot=None):
        """计算现值
        Args:
            prod: Product产品对象
            t: datetime.date，估值日; 如果是None，则使用全局估值日globalEvaluationDate
            spot: float，估值日标的价格，如果是None，则使用随机过程的当前价格
        Returns: float，现值
                        不支持盈亏测算：输入spot = np.ndarray一维价格向量，t=终止日，输出payoff向量
        """
        self.prod = prod
        calculate_date = global_evaluation_date() if t is None else t
        _maturity_business_days = prod.trade_calendar.business_days_between(calculate_date, prod.end_date)

        obs_dates = prod.obs_dates.count_business_days(calculate_date)  # 估值日距离敲出观察日的交易日天数
        obs_dates_list = prod.obs_dates.date_schedule
        div_obs_dates = prod.div_obs_dates.count_business_days(calculate_date)  # 估值日距离派息观察日的交易日天数
        div_obs_dates_list = prod.div_obs_dates.date_schedule
        pay_dates = prod.pay_dates.count_calendar_days(calculate_date)  # 估值日距离支付日的自然日天数
        digital_num = prod.lock_term - 1

        try:  # 确定当前日期在观察日中的位置
            # past_num = obs_dates_list.index(calculate_date)
            past_div_num = div_obs_dates_list.index(calculate_date)
        except ValueError:
            # 如果当前日期不在观察日中，找到最近的位置
            # past_num = bisect.bisect_right(obs_dates_list, calculate_date)
            past_div_num = bisect.bisect_right(div_obs_dates_list, calculate_date)

        if spot is None:
            spot = self.process.spot()

        # 当估值日就是终止日时，直接用spot计算payoff，无需随机数矩阵
        if calculate_date == prod.stop_date:
            if isinstance(spot, (list, np.ndarray)):  # spot是一个np.ndarray一维价格向量，返回盈亏测算向量，即每个价格对应的现值，而不是平均值
                self.s_paths = np.array([spot, ])
            else:  # spot是一个数值，计算期末payoff
                self.s_paths = np.array([[spot, ]])
        else:  # 随机价格路径矩阵
            if spot != self.process.spot():
                self.reset_paths_flag()  # 重置路径标志位，重新生成路径
            self.s_paths = self.path_generator(n_step=_maturity_business_days, spot=spot,
                                               t_step_per_year=prod.t_step_per_year).copy()
        self._n_path = self.s_paths.shape[1]

        if (calculate_date == prod.stop_date) and (calculate_date < prod.end_date):  # 提前敲出的参数需要特殊处理
            # 只有 obs_dates 中 = 0的观察日才会保留，之后的观察日因为敲出而消失了
            self._obs_dates = np.array([0])
            self._div_obs_dates = np.array([0]) if 0 in div_obs_dates else np.array([])
            # 只取前past_div_num个支付日, 之后的支付日因为敲出而消失了
            self._pay_dates = np.array([num / prod.annual_days.value for num in pay_dates][:past_div_num + 1])
            _obs_idx = obs_dates_list.index(calculate_date)
            self._barrier_out = np.array([prod.barrier_out[_obs_idx], ])
            self._barrier_in = np.array([prod.barrier_in[_obs_idx + digital_num], ])
            self._barrier_yield = np.array([prod.barrier_yield[_obs_idx + digital_num], ])
            self._coupon = prod.coupon[:past_div_num + 1]
        else:  # 有机会持有到到期日
            self._obs_dates = np.array([num for num in obs_dates if num >= 0])  # 只取 num >= 0 (未来)的敲出观察日
            # 包括 num < 0的支付日，从div_history取出已经发生过派息的观察日, 时间的负值可以直接输入折现因子函数
            self._div_obs_dates = np.array([num for num in div_obs_dates if num >= 0])  # 只取 num >= 0 (未来)的派息观察日
            self._pay_dates = np.array([num / prod.annual_days.value for num in pay_dates])
            # 经过估值日截断的列表，例如prod.barrier_out有22个，存续一年时估值，_barrier_out只有12个
            self._barrier_out = prod.barrier_out[-len(self._obs_dates):].copy()
            self._barrier_in = prod.barrier_in[-len(self._div_obs_dates):].copy()
            self._barrier_yield = prod.barrier_yield[-len(self._div_obs_dates):].copy()
            self._coupon = prod.coupon[-len(self._pay_dates):].copy()

        # 已经发生过派息的观察日
        div_history_list = prod.div_history.date_schedule
        if div_history_list:  # 如果该列表非空
            div_history_list = list(set(div_history_list))  # 日期去重
            div_history_list.sort()  # 从小到大排序
            prod.div_history.date_schedule = div_history_list  # 更新div_history

            # 获取过去的派息观察日
            past_div_obs_dates = div_obs_dates_list[:past_div_num]
            past_coupon = np.zeros(past_div_num, dtype=int)  # 过去past_num个观察日的标记数组
            # 标记过去发生派息的日期
            for i, day in enumerate(past_div_obs_dates):
                if day in div_history_list:
                    past_coupon[i] = True
            self._past_coupon = past_coupon
        else:
            self._past_coupon = np.zeros(past_div_num, dtype=int)

        # 统计各个情景的payoff
        self._cal_knock_out_date()
        self._cal_yield_date()
        self._cal_knock_in_scenario()
        result = self._cal_payoff()

        if (calculate_date == prod.end_date) and isinstance(spot, np.ndarray):
            return result  # 终止日, spot是价格向量, 则返回盈亏测算向量，即每个价格对应的现值，而不是平均值
        else:
            return np.mean(result)

    def _cal_knock_out_date(self):
        """统计每条路径敲出时间"""
        # 计算每条价格路径最小敲出时间
        barrier_out = np.tile(self._barrier_out, (self._n_path, 1)).T
        barrier_out_matrix = np.tile(np.arange(self._obs_dates.shape[0]), (self._n_path, 1)).T
        barrier_out_matrix = np.where(self.s_paths[self._obs_dates, :] >= barrier_out,
                                      barrier_out_matrix.astype(int), np.inf)
        # 返回每一列的最小值，即为每条路径的敲出时间
        self.knock_out_time_idx = np.min(barrier_out_matrix, axis=0)
        # 统计哪些路径属于发生了敲出的情景(布尔索引)
        self.knock_out_scenario = (self.knock_out_time_idx != np.inf)
        # 每条路径持有时长, 用于计算返还保证金的折现因子
        hold_time_idx = self.knock_out_time_idx.copy()
        # 对于未敲出情形，持有到期，因此将索引设置为最后一个观察日的索引，如24个观察日，则指定为23
        hold_time_idx[~self.knock_out_scenario] = self._obs_dates.size - 1
        # 由于敲出观察日会根据估值日的位置被截断，因此，应该补齐敲出观察日比派息观察日少的数量，得到的持有时长索引才能与派息支付日的索引对齐
        self.hold_time_idx = hold_time_idx.astype(int) + len(self._pay_dates) - len(self._obs_dates)

    def _cal_yield_date(self):
        """统计每条路径发生派息的时间"""
        # 统计哪些派息日，标的价格在派息线上方(即该派息日发生派息)
        barrier_yield = np.tile(self._barrier_yield, (self._n_path, 1)).T
        coupon_time_idx = (self.s_paths[self._div_obs_dates, :] > barrier_yield)
        # 利用每条路径持有时长的索引数组, 将发生敲出之后的派息bool(不包括敲出当天)由True改为False
        self.coupon_bool = np.where(np.arange(coupon_time_idx.shape[0])[:, np.newaxis] > self.hold_time_idx,
                                    False, coupon_time_idx)

    def _cal_knock_in_scenario(self):
        """统计哪些路径属于敲入未敲出的情景"""
        prod = self.prod
        if prod.status == StatusType.UpTouch:  # 已敲出
            self.knock_in_scenario = np.zeros(self._n_path)
        elif prod.status == StatusType.DownTouch:  # 之前已敲入，之后未敲出的路径全部算作敲入
            self.knock_in_scenario = np.where(self.knock_out_scenario, False, True)
        else:
            if prod.in_obs_type == ExerciseType.American:  # 每日观察敲入(凤凰)
                # 排除发生了敲出的路径，统计哪些路径属于敲入未敲出
                knock_in_level = np.array(self._barrier_in).repeat(
                    np.diff(np.append(np.zeros((1,)), self._div_obs_dates)).astype(int))
                knock_in_level = np.append(self._barrier_in[0], knock_in_level)
                knock_in_level = np.tile(knock_in_level, (self._n_path, 1)).T
                knock_in_time_idx = (self.s_paths <= knock_in_level)
                # 统计哪些路径属于敲入未敲出
                knock_in_bool = np.any(knock_in_time_idx, axis=0)
            elif prod.in_obs_type == ExerciseType.European:  # 到期观察敲入(DCN/FCN)
                knock_in_bool = (self.s_paths[-1] <= self._barrier_in[-1])
            else:
                raise ValueError(f"不支持的敲入观察类型{prod.in_obs_type}，仅支持美式(凤凰)/欧式(DCN/FCN)")
            # 将发生敲出之后的敲入由True改为False
            self.knock_in_scenario = np.where(self.knock_out_scenario, False, knock_in_bool)

    def _cal_payoff(self):
        """统计每条路径的收益加总"""
        # 不同派息日的票息分别计算贴现因子，得到派息的现值
        discount_factor = np.empty(self._pay_dates.size)
        for i, pay_d in enumerate(self._pay_dates):
            discount_factor[i] = self.process.interest.disc_factor(pay_d)  # 每月的派息会复利
        discounted_coupon = self._coupon * self.prod.s0 * discount_factor

        # payoff汇总
        # 1. 归还保证金(敲入亏损在3.另行计算)
        payoff1 = self.prod.margin_lvl * self.prod.s0 * discount_factor[self.hold_time_idx]

        # 2. 派息payoff
        # 过去已发生的派息
        past_value = np.dot(self._past_coupon, discounted_coupon[:len(self._past_coupon)])  # 向量内积
        # 模拟路径发生的派息
        future_value = discounted_coupon[len(self._past_coupon):] @ self.coupon_bool  # 矩阵乘法
        payoff2 = past_value + future_value

        # 3. 敲入的路径，承担跌幅损失
        payoff3 = np.zeros_like(self.s_paths[-1])
        if np.any(self.knock_in_scenario):
            s_knock_in = self.s_paths[-1][self.knock_in_scenario]
            payoff_knock_in = self.prod.parti_in * discount_factor[-1] * np.minimum(
                np.maximum(s_knock_in, self.prod.strike_lower) - self.prod.strike_upper, 0)
            payoff3[self.knock_in_scenario] = payoff_knock_in

        payoff = payoff1 + payoff2 + payoff3
        return payoff
