#!/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.utilities.enums import CallPut, PaymentType, OutMechanism
from pricelib.common.pricing_engine_base import McEngine
from pricelib.common.date_handler import global_evaluation_date


class MCAccumulatorEngine(McEngine):
    """累购累沽 Monte Carlo 模拟定价引擎
    仅支持起初估值，不支持持仓估值，因为没有记录历史累积次数"""

    # 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=终止日，则返回盈亏测算向量，即每个价格对应的现值，而不是平均值
        """
        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 = np.array(prod.obs_dates.count_business_days(calculate_date))
        _pay_dates = np.array(prod.obs_dates.count_calendar_days(calculate_date))
        # 排除估值日=0（已实现的价格，已更新到status中）
        _obs_dates = _obs_dates[_obs_dates > 0]
        _pay_dates = _pay_dates[_pay_dates > 0]

        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()
        _n_path = paths.shape[1]

        # 敲出与行权价格
        strike = np.ones(_obs_dates.size) * prod.strike
        strike = np.tile(strike, (_n_path, 1)).T
        if prod.out_mechanism == OutMechanism.NoTerminate:  # 敲出不终止（标准累购累沽）
            barrier_out = np.ones(_obs_dates.size) * prod.barrier_out
            barrier_out = np.tile(barrier_out, (_n_path, 1)).T
            if prod.callput == CallPut.Call:  # 累购
                # 所有不利方向突破执行价的日期的布尔索引
                knock_in_time_idx = paths[_obs_dates, :] < strike
                # 1倍折价建仓的布尔索引
                no_touch_time_idx = np.where(paths[_obs_dates, :] > barrier_out, False, paths[_obs_dates, :] >= strike)
            else:  # 累沽
                # 所有不利方向突破执行价的日期的布尔索引
                knock_in_time_idx = paths[_obs_dates, :] > strike
                # 1倍折价建仓的布尔索引
                no_touch_time_idx = np.where(paths[_obs_dates, :] < barrier_out, False, paths[_obs_dates, :] <= strike)
            # 贴现因子(全部为到期日结算)
            discount_factor = self.process.interest.disc_factor(_pay_dates[-1] / prod.annual_days.value)
            if prod.payment_type == PaymentType.Expire:  # 若每个到期日建仓，发生敲出或到期时结算
                n_underlying = (np.sum(knock_in_time_idx, axis=0) * prod.leverage_ratio
                                + np.sum(no_touch_time_idx, axis=0)) * prod.amt + prod.status[0]
                # 到期盈亏 = 建仓量 * (到期S-K)
                payoff = ((prod.callput.value * (paths[-1] - prod.strike) * n_underlying) * discount_factor)
            elif prod.payment_type == PaymentType.Hit:  # 若每个观察日，立即以现金结算盈亏
                # 到期盈亏 = Σ{每个观察日的建仓量 * (该观察日S-K)}
                payoff = (((np.sum(knock_in_time_idx * (paths[_obs_dates, :] - prod.strike),
                                   axis=0) * prod.leverage_ratio
                            + np.sum(no_touch_time_idx * (paths[_obs_dates, :] - prod.strike),
                                     axis=0)) * prod.callput.value
                           * prod.amt) * discount_factor) + prod.status[0]
            else:
                raise ValueError(f"不支持的结算方式{prod.payment_type}")
        elif prod.out_mechanism == OutMechanism.Terminate:  # 敲出终止（熔断累购累沽）
            barrier_out = np.ones(_obs_dates.size) * prod.barrier_out
            barrier_out = np.tile(barrier_out, (_n_path, 1)).T
            if prod.callput == CallPut.Call:  # 累购
                # 最早发生敲出的日期的序号索引
                knock_out_time_idx = np.argmax(paths[_obs_dates, :] > barrier_out, axis=0)
                # 所有不利方向突破执行价的日期的布尔索引
                knock_in_time_idx = paths[_obs_dates, :] < strike
                # 1倍折价建仓的布尔索引
                no_touch_time_idx = np.where(paths[_obs_dates, :] > barrier_out, False, paths[_obs_dates, :] >= strike)
            else:  # 累沽
                # 最早发生敲出的日期的序号索引
                knock_out_time_idx = np.argmax(paths[_obs_dates, :] < barrier_out, axis=0)
                # 所有不利方向突破执行价的日期的布尔索引
                knock_in_time_idx = paths[_obs_dates, :] > strike
                # 1倍折价建仓的布尔索引
                no_touch_time_idx = np.where(paths[_obs_dates, :] < barrier_out, False, paths[_obs_dates, :] <= strike)
            # 每条路径，是否发生过敲出的布尔索引（0为未敲出）
            knock_out_scenario = np.where(knock_out_time_idx > 0, True, False)
            discount_time_idx = knock_out_time_idx.copy()
            discount_time_idx[~knock_out_scenario] = _obs_dates.size - 1  # 所有未敲出的路径，敲出时间设置为观察日期数量 - 1
            knock_out_time_idx[~knock_out_scenario] = _obs_dates.size  # 所有未敲出的路径，敲出时间设置为观察日期数量
            # 敲出终止（熔断），获取发生敲出之前的，不利方向突破执行价的日期的布尔索引
            knock_in_bool = np.where(np.arange(knock_in_time_idx.shape[0])[:, np.newaxis] > knock_out_time_idx,
                                     False, knock_in_time_idx)
            no_touch_bool = np.where(np.arange(no_touch_time_idx.shape[0])[:, np.newaxis] > knock_out_time_idx,
                                     False, no_touch_time_idx)
            # 贴现因子
            discount_factor = np.empty(_pay_dates.size)
            for i, j in enumerate(_pay_dates):
                discount_factor[i] = self.process.interest.disc_factor(j / prod.annual_days.value)
            # 敲出补偿：敲出每日固定收益 * 单日执行量 * 剩余敲出日数量（含敲出当日）
            knockout_rebate = prod.rebate * prod.amt * (_pay_dates.size - knock_out_time_idx)
            if prod.payment_type == PaymentType.Expire:  # 若每个到期日建仓，发生敲出或到期时结算
                n_underlying = (np.sum(knock_in_bool, axis=0) * prod.leverage_ratio
                                + np.sum(no_touch_bool, axis=0)) * prod.amt + prod.status[0]
                exercise_price = paths[_obs_dates[discount_time_idx], np.arange(_n_path)]
                # 到期盈亏 = 建仓量 * (到期S-K) + 敲出每日固定收益 * 单日执行量 * 剩余敲出日数量（含敲出当日）
                payoff = (prod.callput.value * (exercise_price - prod.strike) * n_underlying
                          + knockout_rebate) * discount_factor[discount_time_idx]
            elif prod.payment_type == PaymentType.Hit:  # 若每个观察日，立即以现金结算盈亏
                # 到期盈亏 = Σ{每个观察日的建仓量 * (该观察日S-K)} + 敲出每日固定收益 * 单日执行量 * 剩余敲出日数量（含敲出当日）
                payoff = (((np.sum(knock_in_bool * (paths[_obs_dates, :] - prod.strike), axis=0) * prod.leverage_ratio
                            + np.sum(no_touch_bool * (paths[_obs_dates, :] - prod.strike),
                                     axis=0)) * prod.callput.value * prod.amt
                           + knockout_rebate) * discount_factor[discount_time_idx]) + prod.status[0]
            else:
                raise ValueError(f"不支持的结算方式{prod.payment_type}")
        elif prod.out_mechanism == OutMechanism.NoKnockOut:  # 不敲出累购累沽

            if prod.callput == CallPut.Call:  # 累购
                # 所有不利方向突破执行价的日期的布尔索引
                knock_in_time_idx = paths[_obs_dates, :] < strike
                # 1倍折价建仓的布尔索引
                no_touch_time_idx = paths[_obs_dates, :] >= strike
            else:  # 累沽
                # 所有不利方向突破执行价的日期的布尔索引
                knock_in_time_idx = paths[_obs_dates, :] > strike
                # 1倍折价建仓的布尔索引
                no_touch_time_idx = paths[_obs_dates, :] <= strike
            # 贴现因子(全部为到期日结算)
            discount_factor = self.process.interest.disc_factor(_pay_dates[-1] / prod.annual_days.value)
            if prod.payment_type == PaymentType.Expire:  # 若每个到期日建仓，发生敲出或到期时结算
                n_underlying = (np.sum(knock_in_time_idx, axis=0) * prod.leverage_ratio
                                + np.sum(no_touch_time_idx, axis=0)) * prod.amt + prod.status[0]
                # 到期盈亏 = 建仓量 * (到期S-K)
                payoff = ((prod.callput.value * (paths[-1] - prod.strike) * n_underlying) * discount_factor)
            elif prod.payment_type == PaymentType.Hit:  # 若每个观察日，立即以现金结算盈亏
                # 到期盈亏 = Σ{每个观察日的建仓量 * (该观察日S-K)}
                payoff = (((np.sum(knock_in_time_idx * (paths[_obs_dates, :] - prod.strike),
                                   axis=0) * prod.leverage_ratio
                            + np.sum(no_touch_time_idx * (paths[_obs_dates, :] - prod.strike),
                                     axis=0)) * prod.callput.value
                           * prod.amt) * discount_factor) + prod.status[0]
            else:
                raise ValueError(f"不支持的结算方式{prod.payment_type}")
        else:
            raise ValueError("不支持的敲出机制: {}".format(prod.out_mechanism))

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