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


class MCAirbagEngine(McEngine):
    """安全气囊 Monte Carlo 模拟定价引擎
    安全气囊是美式观察向下敲入期权，到期支付
    只支持离散观察(默认为每日观察)
    """

    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=终止日，则返回盈亏测算向量，即每个价格对应的现值，而不是平均值
        """
        calculate_date = global_evaluation_date() if t is None else t
        _maturity = (prod.end_date - calculate_date).days / prod.annual_days.value
        _maturity_business_days = prod.trade_calendar.business_days_between(calculate_date, prod.end_date)
        _tenor = (prod.end_date - prod.start_date).days / prod.annual_days.value  # 从起始日到到期日的自然日时间（年）
        if spot is None:
            spot = self.process.spot()
        # 当估值日就是到期日时，直接用spot计算payoff，无需随机数矩阵
        if calculate_date == prod.end_date:
            if isinstance(spot, (list, np.ndarray)):  # spot是一个np.ndarray一维价格向量，返回盈亏测算向量，即每个价格对应的现值，而不是平均值
                paths = np.array([spot, ])
            else:  # spot是一个数值，计算期末payoff
                paths = np.array([[spot, ]])
        else:
            if spot != self.process.spot():
                self.reset_paths_flag()  # 重置路径标志位，重新生成路径
            paths = self.path_generator(n_step=_maturity_business_days, spot=spot,
                                    t_step_per_year=prod.t_step_per_year).copy()

        if prod.discrete_obs_interval is None:  # 每日观察
            obs_points = np.arange(0, _maturity_business_days + 1, 1)
        else:  # 均匀离散观察
            dt_step = prod.discrete_obs_interval * prod.t_step_per_year
            obs_points = np.flip(np.round(np.arange(_maturity_business_days, 0, -dt_step)).astype(int))
            obs_points = np.concatenate((np.array([0]), obs_points))
            obs_points[obs_points > _maturity_business_days] = _maturity_business_days  # 防止闰年导致的下标越界

        # 未敲入，期末价值是call或牛市价差
        if prod.init_strike_cap is None:
            init_payoff = paths[-1] - prod.strike
        else:  # 若存在敲入前封顶价格
            init_payoff = np.where(paths[-1] - prod.init_strike_cap > 0,
                                   prod.init_strike_cap - prod.strike, paths[-1] - prod.strike)
        # 已敲入，且标的上涨
        if prod.in_strike_cap is None:
            in_payoff_upper = paths[-1] - prod.s0
        else:  # 若存在敲入后封顶价格
            in_payoff_upper = np.where(paths[-1] - prod.in_strike_cap > 0,
                                       prod.in_strike_cap - prod.s0, paths[-1] - prod.s0)
        # 已敲入，且标的下跌
        if prod.in_strike_floor is None:
            in_payoff_lower = paths[-1] - prod.s0
        else:  # 若存在敲入后封底价格
            in_payoff_lower = np.where(paths[-1] - prod.in_strike_floor < 0,
                                       prod.in_strike_floor - prod.s0, paths[-1] - prod.s0)
        # 安全气囊，payoff汇总
        knock_in_bool = np.any(paths[obs_points] <= prod.barrier, axis=0)
        value = (np.where(knock_in_bool,
                                 # 已敲入，标的涨跌幅，可能存在保底或封顶执行价
                                 np.where(paths[-1] - prod.s0 < 0,
                                          # 敲入且标的下跌
                                          in_payoff_lower * prod.knockin_parti,  # 敲入下跌参与率
                                          # 敲入且标的上涨
                                          in_payoff_upper * prod.reset_call_parti),  # 敲入重置后看涨参与率
                                 # 未敲入，香草看涨或者牛市价差
                                 np.where(init_payoff < 0, 0, init_payoff) * prod.call_parti)  # 未敲入看涨参与率
                        ) + prod.floor_yield * prod.s0 * _tenor  # 保底收益率
        value *= self.process.interest.disc_factor(_maturity)

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