#!/user/bin/env python
# -*- coding: utf-8 -*-
"""
Copyright (C) 2024 Galaxy Technologies
Licensed under the Apache License, Version 2.0
"""
import math
import numpy as np
from pricelib.common.date_handler import global_evaluation_date
from pricelib.common.utilities.enums import TouchType, ExerciseType, PaymentType
from pricelib.common.processes import StochProcessBase
from pricelib.common.pricing_engine_base.analytic_engine_base import (AnalyticEngine, euro_cash_or_nothing)
from pricelib.common.utilities.utility import logging


class AnalyticDoubleDigitalEngine(AnalyticEngine):
    """双边二元期权闭式级数近似解定价引擎
    欧式：二元凸式、二元凹式
        由欧式现金或无的解析解组合而成
    美式：美式双接触/双不接触
        Hui(1996) One-Touch Double Barrier Binary Option Values
        只支持美式双接触/双不接触, 到期支付；双接触期权的下边界payoff和上边界payoff必须相等；不支持年化的行权收益，只支持绝对数值的行权收益
        需要注意的是，Hui(1996)的形式是一个无穷级数，这个数列收敛非常迅速，大部分情况下前几项就足够确定期权价格，
                   但是在临近到期日时，这个解有误差，需要更多项级数来近似"""

    def __init__(self, stoch_process: StochProcessBase = None, series_num=10, *,
                 s=None, r=None, q=None, vol=None):
        """构造函数
        Args:
            stoch_process: 随机过程StochProcessBase对象
            series_num: int，计算Hui(1996)近似解时级数项的数量
        在未设置stoch_process时，(stoch_process=None)，会默认创建BSMprocess，需要输入以下变量进行初始化
            s: float，标的价格
            r: float，无风险利率
            q: float，分红/融券率
            vol: float，波动率
        """
        super().__init__(stoch_process=stoch_process, s=s, r=r, q=q, vol=vol)
        self.series_num = series_num
        self.i_vec = np.array(range(1, series_num + 1))  # 用于计算近似解的级数项

    # pylint: disable=invalid-name, 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，现值
        """
        calculate_date = global_evaluation_date() if t is None else t
        _maturity_days = prod.trade_calendar.business_days_between(calculate_date, prod.end_date)
        _maturity = _maturity_days / prod.t_step_per_year
        tenor = (prod.stop_date - prod.start_date).days / prod.annual_days.value  # 从起始日到终止日的自然日时间（年）
        if spot is None:
            spot = self.process.spot()
        r = self.process.interest(_maturity)
        q = self.process.div(_maturity)
        vol = self.process.vol(_maturity, spot)

        if calculate_date == prod.stop_date:  # 如果估值日就是到期日，则直接计算payoff
            value = self.calc_maturity_payoff(prod, np.array([spot]), tenor)[0]
            return value

        if prod.exercise_type == ExerciseType.European:  # 如果是欧式双边二元期权（到期观察）
            return self._calc_european_double_digital(prod, spot, _maturity_days, _maturity, tenor, r, q, vol)
        elif prod.exercise_type == ExerciseType.American:  # 如果是美式双边二元期权
            assert prod.payment_type == PaymentType.Expire, "Error: 美式双边二元期权Hui(1996)只支持到期支付，请更换定价方法"
            return self._calc_american_double_digital(prod, spot, _maturity_days, _maturity, tenor, r, q, vol)
        else:
            raise ValueError("obs_type must be European or American")

    def calc_maturity_payoff(self, prod, s_vec, tenor):
        """到期时的期权价值，在障碍价格内侧，默认设定未敲入，未敲出
        Args:
            prod: Product产品对象
            s_vec: np.ndarray, 网格的价格格点
            tenor: float, 从起始日到终止日的自然日时间（年）
        Returns:
            payoff: np.ndarray, 期末价值向量
        """
        if prod.touch_type == TouchType.Touch:  # 二元凹式，此时相当于低行权价的现金或无put加上高行权价的现金或无call
            _rebate = self._preprocess_extra_returns(prod, tenor, prod.rebate)  # 高低触碰行权收益绝对数值
            v_vec = np.where(s_vec <= prod.bound[0], _rebate[0],
                             np.where(s_vec >= prod.bound[1], _rebate[1], 0))
        elif prod.touch_type == TouchType.NoTouch:  # 二元凸式，此时相当于低行权价的现金或无call减去高行权价的现金或无put
            _rebate = self._preprocess_extra_returns(prod, tenor, [prod.rebate[0], ])  # 中间的敲入现金返还绝对数值
            v_vec = np.where(s_vec > prod.bound[0], np.where(s_vec < prod.bound[1], _rebate[0], 0), 0)
        else:
            raise ValueError("touch_type must be Touch or NoTouch")
        return v_vec

    @staticmethod
    def _preprocess_extra_returns(prod, period, rebate_list):
        """预处理可能存在的年化现金返还rebate
        Args:
            prod: Product产品对象
            period: float 或 np.ndarray, 从起始到期权终止的时间（年）
                    对欧式障碍、美式敲入而言是整个存续期；对美式敲出而言，期权可能会因为敲出提前终止，此时period是起始日到敲出日的时间
            rebate_list: List[float], 现金返还率或绝对数值
        Returns:
            _rebate: float，处理后的现金返还绝对数值
        """
        if prod.rebate_annual:  # 年化百分比
            _rebate = []
            for r in rebate_list:
                _rebate.append(r * period * prod.s0)
        else:
            _rebate = rebate_list  # 绝对数值
        return _rebate

    def _calc_european_double_digital(self, prod, spot, _maturity_days, _maturity, tenor, r, q, vol):
        """二元凸式、二元凹式的解析解
            由欧式现金或无的解析解组合而成
        Args:
            prod: Product产品对象
            spot: float，估值日标的价格
            _maturity_days: float，交易日到期时间（天）
            _maturity: float，交易日到期时间（年）
            tenor: float, 从起始日到终止日的自然日时间（年）
            r: float，无风险利率（常数）
            q: float，连续分红率（常数）
            vol: float，波动率（常数）
        Returns:
            美式障碍期权的解析解
        """
        if prod.touch_type == TouchType.Touch:  # 二元凹式，此时相当于低行权价的现金或无put加上高行权价的现金或无call
            _rebate = self._preprocess_extra_returns(prod, tenor, prod.rebate)  # 高低触碰行权收益绝对数值
            value = (euro_cash_or_nothing(s=spot, k=prod.bound[0], t=_maturity, r=r, q=q, sigma=vol, sign=-1) *
                     _rebate[0] + euro_cash_or_nothing(s=spot, k=prod.bound[1], t=_maturity, r=r, q=q, sigma=vol,
                                                       sign=1) * _rebate[1])
        elif prod.touch_type == TouchType.NoTouch:  # 二元凸式，此时相当于低行权价的现金或无call减去高行权价的现金或无put
            _rebate = self._preprocess_extra_returns(prod, tenor, [prod.rebate[0], ])  # 中间的敲入现金返还绝对数值
            value = (euro_cash_or_nothing(s=spot, k=prod.bound[0], t=_maturity, r=r, q=q, sigma=vol, sign=1) -
                     euro_cash_or_nothing(s=spot, k=prod.bound[1], t=_maturity, r=r, q=q, sigma=vol, sign=1)
                     ) * _rebate[0]
        else:
            raise ValueError("touch_type must be Touch or NoTouch")
        return value

    def _calc_american_double_digital(self, prod, spot, _maturity_days, _maturity, tenor, r, q, vol):
        """美式双接触/双不接触的解析解
            Hui(1996) One-Touch Double Barrier Binary Option Values
            只支持到期支付的美式双接触/双不接触；并且，双接触期权的下边界payoff和上边界payoff必须相等
            需要注意的是，Hui(1996)的形式是一个无穷级数，这个数列收敛非常迅速，大部分情况下前几项就足够确定期权价格，
                       但是在临近到期日时，这个解有误差，需要更多项级数来近似
            支持Broadie, Glasserman和Kou(1995)提出的均匀离散观察障碍期权近似解.
        Args:
            prod: Product产品对象
            spot: float，估值日标的价格
            _maturity_days: float，交易日到期时间（天）
            _maturity: float，交易日到期时间（年）
            tenor: float, 从起始日到终止日的自然日时间（年）
            r: float，无风险利率（常数）
            q: float，连续分红率（常数）
            vol: float，波动率（常数）
        Returns:
            美式双接触/双不接触的解析解
        """
        _rebate = self._preprocess_extra_returns(prod, tenor, prod.rebate)  # 高低触碰行权收益/非触碰行权收益的绝对数值
        assert (prod.payment_type == PaymentType.Expire and
                prod.exercise_type == ExerciseType.American), "Error: Hui(1996)双边二元期权级数近似解, 只支持到期支付的美式双接触/双不接触"
        if prod.touch_type == TouchType.Touch:
            assert _rebate[0] == _rebate[1], "Error: Hui(1996)双边二元期权闭式解, 双接触期权的下边界payoff和上边界payoff必须相等"
        if _maturity_days <= 15 and self.series_num < 100000:
            # 在临近到期日时，较少的级数项会导致误差，需要更多项级数来近似
            logging.info(
                f"距离到期仅剩{_maturity_days}个交易日, 较少的级数项会导致误差，需要更多项级数来近似，已重设为100000项")
            self.series_num = 100000
            self.i_vec = np.array(range(1, self.series_num + 1))
        elif _maturity_days > 15:
            # 在非临近到期日时，数列收敛非常迅速，大部分情况下前几项就足够确定期权价格，设为10即可
            if self.series_num >= 100000:
                logging.info(
                    f"距离到期{_maturity_days}个交易日, 数列收敛非常迅速，大部分情况下前几项就足够确定期权价格，已重设为10项")
                self.series_num = 10
            self.i_vec = np.array(range(1, self.series_num + 1))

        # 如果估值时的标的价格已经触碰行权价，直接返回行权收益的现值
        if spot <= prod.bound[0] or spot >= prod.bound[1]:
            if prod.touch_type == TouchType.NoTouch:
                return 0
            elif prod.touch_type == TouchType.Touch:
                return _rebate[0] * math.exp(-r * _maturity)
            else:
                raise ValueError("Error: TouchType只能是NoTouch或Touch")

        if prod.discrete_obs_interval is not None:
            # 均匀离散观察，M. Broadie, P. Glasserman, S.G. Kou(1997) 在连续障碍期权解析解上加调整项，调整障碍价格水平
            # 指数上的beta = -zeta(1/2) / sqrt(2*pi) = 0.5826, 其中zeta是黎曼zeta函数
            bound = [None, None]
            bound[1] = prod.bound[1] * np.exp(0.5826 * vol * np.sqrt(prod.discrete_obs_interval))
            bound[0] = prod.bound[0] * np.exp(-0.5826 * vol * np.sqrt(prod.discrete_obs_interval))
        else:  # 连续观察
            bound = prod.bound

        Z = math.log(bound[1] / bound[0])
        alpha = - 0.5 * (2 * (r - q) / vol ** 2 - 1)
        beta = - 0.25 * (2 * (r - q) / vol ** 2 - 1) ** 2 - 2 * r / vol ** 2

        value0 = self.i_vec * np.pi / Z
        value1 = 2 * np.pi * self.i_vec * _rebate[0] / Z ** 2
        value2 = (spot / bound[0]) ** alpha - np.power(-1, self.i_vec) * (spot / bound[1]) ** alpha
        value3 = alpha ** 2 + value0 ** 2
        value4 = np.sin(value0 * np.log(spot / bound[0]))
        value5 = np.exp(-0.5 * vol ** 2 * _maturity * (value0 ** 2 - beta))
        value_series = value1 * value2 * value4 * value5 / value3  # 期权价值
        double_no_touch = np.sum(value_series)

        if prod.touch_type == TouchType.NoTouch:
            return double_no_touch
        if prod.touch_type == TouchType.Touch:
            return _rebate[0] * np.exp(-r * _maturity) - double_no_touch
        raise ValueError("Hui(1996)双边二元期权闭式解, 未知的触碰类型")
