# !/user/bin/env python
# -*- coding: utf-8 -*-
"""
Copyright (C) 2024 Galaxy Technologies
Licensed under the Apache License, Version 2.0
"""
import datetime
from contextlib import suppress
import numpy as np
from pricelib.common.utilities.enums import InOut, ExerciseType, EngineType, PaymentType, StatusType
from pricelib.common.date_handler import CN_CALENDAR, AnnualDays, global_evaluation_date
from pricelib.common.utilities.patterns import Observer, global_auto_update_status
from pricelib.common.utilities.utility import time_this, logging
from pricelib.common.product_base.option_base import OptionBase
from pricelib.pricing_engines.fdm_engines import FdmDoubleSharkEngine
from pricelib.pricing_engines.mc_engines import MCDoubleSharkEngine
from pricelib.pricing_engines.integral_engines import QuadDoubleSharkEngine


class DoubleShark(OptionBase, Observer):
    """双向鲨鱼鳍结构
    双向鲨鱼鳍结构属于敲出期权，由两个高低障碍价相同的双边看涨、双边看跌敲出障碍期权组合而成，且put的执行价小于等于call的执行价"""
    inout = InOut.Out  # 双鲨结构属于敲出期权

    def __init__(self, obs_type=ExerciseType.American, payment_type=PaymentType.Expire,
                 strike_lower=95., strike_upper=105., barrier_lower=90., barrier_upper=110.,
                 parti_lower=1., parti_upper=1., rebate_lower=0., rebate_upper=0., rebate_annual=False,
                 floor_yield=0., status=StatusType.NoTouch, window=(None, None),
                 maturity=None, start_date=None, end_date=None, discrete_obs_interval=None,
                 trade_calendar=CN_CALENDAR, annual_days=AnnualDays.N365, t_step_per_year=243,
                 engine=None, stoch_process=None, s0=None, s=None, r=None, q=None, vol=None, engine_method=None):
        """构造函数
        产品参数:
            obs_type: 障碍观察类型，ExerciseType枚举类，美式观察American/欧式到期观察European
            payment_type: 支付类型，PaymentType枚举类，默认为到期支付Expire
            strike: (strike_lower, strike_upper), 低行权价/高行权价，一般低行权价是put的执行价，高行权价是call的执行价
            bound: (barrier_lower, barrier_upper), 低障碍价/高障碍价，障碍价格绝对数值。如果是默认值None，即为无穷大或无穷小inf。
            parti: (parti_lower, parti_upper), 香草期权的参与率，默认为1
            rebate: (rebate_lower, rebate_upper), 未敲出补偿的绝对数值，非年化
                                                  元组第一位是低障碍价下方的补偿，第二位是高障碍价上方的补偿，
            rebate_annual: bool, 现金返还是否为年化，默认为False，即绝对数值，非年化，非百分比；若为True，则为百分比年化收益率
            floor_yield: float, 封底收益(保底收益)，默认为0，未敲出情况下，约定的最低收益率，即 max(|S-K|,0) 额外的固定收益
            status: 敲出状态，StatusType枚举类，默认为NoTouch未敲出，UpTouch为向上敲出，DownTouch为向下敲出
        时间参数: 要么输入年化期限，要么输入起始日和到期日
            maturity: float，年化期限
            start_date: datetime.date，起始日
            end_date: datetime.date，到期日
            window: (start_date, end_date)，障碍观察时间窗口，距离估值日的年化期限。 仅PDE定价引擎支持该参数。
                    如果是默认值None，说明观察区间是整个存续期: 估值日-终止日
            discrete_obs_interval: 观察时间间隔. 若为连续观察，None；若为均匀离散观察，为年化的观察时间间隔
            trade_calendar: 交易日历，Calendar类，默认为中国内地交易日历
            annual_days: int，每年的自然日数量
            t_step_per_year: int，每年的交易日数量
        可选参数:
            s0: float，标的期初价格，只有当 rebate_annual = True，即现金返还为年化收益率时；
                      或者当 floor_yield != 0，即存在非零的保底收益率时，需要输入标的期初价格，将百分比的收益率转化成绝对数值
            engine: 定价引擎，PricingEngine类
                    解析解: AnalyticDoubleSharkEngine 分为两种，只支持美式观察(整个有效期观察)；支持连续观察/离散观察(默认为每日观察)；
                                                    Ikeda and Kunitomo(1992) 仅当行权价在障碍范围内时，公式才是成立的
                                                    Haug(1998) 仅适用于标的持有成本为0的期权(期货期权)
                                    两者的现金返还部分都使用美式双接触闭式解Hui(1996)，因此高低敲出现金返还也必须相等，现金返还只支持到期支付
                    PDE: FdmDoubleSharkEngine  只支持美式观察(整个有效期观察)；支持连续观察/离散观察(默认为每日观察)；
                                               支持现金返还；敲出现金返还支持 立即支付/到期支付
                    蒙特卡洛: MCDoubleSharkEngine  支持欧式观察(仅到期观察)/美式观察(整个有效期观察)；只支持离散观察(默认为每日观察)；
                                                 支持现金返还；敲出现金返还支持 立即支付/到期支付
                    积分法: QuadDoubleSharkEngine  只支持美式观察(整个有效期观察)；只支持离散观察(默认为每日观察)；支持现金返还到期支付；
            engine_method: str, 定价引擎种类。若未输入engine，可以用engine_method快捷生成指定定价引擎。可选Analytic、MC/MonteCarlo、PDE/FDM、QUAD。
            若未提供引擎engine、也未指定引擎种类engine_method的情况下，如果提供了 随机过程 或 标的价格、无风险利率、分红/融券率、波动率，
            则默认使用PDE定价引擎 FdmDoubleSharkEngine
            stoch_process: 随机过程StochProcessBase对象
            s: float，标的价格
            r: float，无风险利率
            q: float，分红/融券率
            vol: float，波动率
        """
        super().__init__()
        self.trade_calendar = trade_calendar
        self.annual_days = annual_days
        self.t_step_per_year = t_step_per_year
        assert maturity is not None or (start_date is not None and end_date is not None), "Error: 到期时间或起止时间必须输入一个"
        self.start_date = start_date if start_date is not None else global_evaluation_date()  # 起始时间
        self.end_date = end_date if end_date is not None else (
                self.start_date + datetime.timedelta(days=round(maturity * annual_days.value)))  # 结束时间
        if self.trade_calendar.isholiday(self.end_date):  # 如果到期日不是交易日，向后顺延一个交易日
            self.end_date = self.trade_calendar.advance(self.end_date, datetime.timedelta(days=1))
        self.maturity = maturity if maturity is not None else (self.end_date - self.start_date).days / annual_days.value
        assert strike_lower <= strike_upper, "ValueError: 双鲨结构的低行权价必须小于等于高行权价"
        self.strike = (strike_lower, strike_upper)  # (低行权价, 高行权价)
        assert barrier_lower < barrier_upper, "ValueError: 双鲨结构的低障碍价必须小于高障碍价"
        self.bound = (barrier_lower, barrier_upper)  # (低障碍价, 高障碍价)
        self.parti = (parti_lower, parti_upper)  # (低参与率，高参与率)
        self.rebate = (rebate_lower, rebate_upper)  # 敲出现金返还: (下边界payoff, 上边界payoff)
        self.rebate_annual = rebate_annual  # 现金返还是否为年化
        self.window = window  # (观察窗口起始时间, 观察窗口结束时间)
        self.floor_yield = floor_yield  # 封底收益
        self.obs_type = obs_type
        self.s0 = s0
        self.payment_type = payment_type
        self.discrete_obs_interval = discrete_obs_interval  # 连续观察=None；均匀离散观察=观察时间间隔
        if self.obs_type == ExerciseType.European:
            err_msg = ""
            if self.payment_type != PaymentType.Expire:
                err_msg += "ValueError: 到期观察双鲨的现金返还方式PaymentType一定是到期支付Expire.\n"
            if self.discrete_obs_interval is not None:
                err_msg += "ValueError: 到期观察的双向鲨鱼鳍，观察时间间隔只能为None.\n"
            if err_msg != "":
                raise ValueError(err_msg)
        self.status = status
        self.stop_date = self.end_date  # 停止日期，默认等于到期日。如果发生敲出并立即支付现金返还，停止日期更新为敲出日期
        if engine is not None:
            self.set_pricing_engine(engine)
        else:
            if engine_method is None:
                default_engine = FdmDoubleSharkEngine  # 默认PDE-FDM定价引擎
            elif engine_method == "Analytic":
                from pricelib.pricing_engines.analytic_engines import AnalyticDoubleSharkEngine
                default_engine = AnalyticDoubleSharkEngine
            elif engine_method in ["PDE", "FDM"]:
                default_engine = FdmDoubleSharkEngine
            elif engine_method in ["MC", "MonteCarlo"]:
                default_engine = MCDoubleSharkEngine
            elif engine_method == "QUAD":
                default_engine = QuadDoubleSharkEngine
            else:
                raise NotImplementedError(
                    f"不支持的engine_method{engine_method}, {self.__repr__()}仅支持Analytic/MC/PDE/QUAD")
            if stoch_process is not None:
                self.set_pricing_engine(default_engine(stoch_process))
            elif s is not None and r is not None and q is not None and vol is not None:
                self.set_pricing_engine(default_engine(s=s, r=r, q=q, vol=vol))

    def set_pricing_engine(self, engine):
        """设置定价引擎，同时将自己注册为观察者。若已有定价引擎，先将自己从原定价引擎的观察者列表中移除"""
        with suppress(AttributeError, ValueError):
            self.engine.process.spot.remove_observer(self)
        self.engine = engine
        self.engine.process.spot.add_observer(self)
        logging.info(f"{self}当前定价方法为{engine.engine_type.value}")

    def remove_self(self):
        """删除对象自己，del自己之前，先将自己从被观察者的列表中移除"""
        self.engine.process.spot.remove_observer(self)
        del self

    def update(self, observable, *args, **kwargs):
        """收到标的价格变化的通知时，自动更新是否已经触碰障碍"""  # TODO: 目前update仅支持连续观察，或者每日观察
        if global_evaluation_date() > self.stop_date:  # 如果估值日已经过了终止日，维持原来的状态
            return
        if global_auto_update_status() and observable == self.engine.process.spot:
            if self.obs_type == ExerciseType.American:
                # TODO: 目前美式观察双边障碍期权的update仅支持连续观察，或者每日观察（因为每周、每月观察日不确定，需要额外输入参数）
                if observable.data >= self.bound[1]:
                    self.status = StatusType.UpTouch
                    if self.inout == InOut.Out and self.payment_type == PaymentType.Hit:
                        self.stop_date = global_evaluation_date()
                        logging.info(f"{self}【已向上触碰】，终止日为 {self.stop_date}")
                elif observable.data <= self.bound[0]:
                    self.status = StatusType.DownTouch
                    if self.inout == InOut.Out and self.payment_type == PaymentType.Hit:
                        self.stop_date = global_evaluation_date()
                        logging.info(f"{self}【已向下触碰】，终止日为 {self.stop_date}")
            elif self.obs_type == ExerciseType.European:  # 欧式到期观察
                if global_evaluation_date() == self.end_date:  # 如果估值日是到期日
                    if observable.data >= self.bound[1]:
                        self.status = StatusType.UpTouch
                    elif observable.data <= self.bound[0]:
                        self.status = StatusType.DownTouch

    def __repr__(self):
        """返回期权的描述"""
        if self.obs_type == ExerciseType.European:
            obs_type = "到期观察"
        elif self.obs_type == ExerciseType.American:
            if self.discrete_obs_interval is None:
                obs_type = "连续观察"
            elif self.discrete_obs_interval == 1 / 243:
                obs_type = "每日观察"
            else:
                obs_type = "离散观察"
        else:
            raise ValueError("obs_type should be European or American")
        return f"{obs_type}{self.payment_type.value}双向鲨鱼鳍期权"

    @time_this
    def price(self, t: datetime.date = None, spot=None):
        """计算期权价格
        Args:
            t: datetime.date，计算期权价格的日期
            spot: float，标的价格
        Returns: 期权现值
        """
        self.validate_parameters(t=t)
        assert (self.strike[0] <= self.strike[1]) and (
                self.bound[0] < self.bound[1]), "Error: 双鲨结构输入的行权价和障碍价必须是递增的，不能是None"
        if self.status != StatusType.NoTouch:  # 如果已经触碰障碍，则敲出价值=现金返还的现值
            calculate_date = global_evaluation_date() if t is None else t
            tenor = (self.end_date - self.start_date).days / self.annual_days.value  # 从起始日到终止日的自然日时间（年）
            _maturity = (self.end_date - calculate_date).days / self.annual_days.value
            # 已经发生敲出，支付现金返还rebate
            if self.payment_type == PaymentType.Hit:  # 立即支付
                if self.rebate_annual:  # 年化百分比
                    elapsed_time = (calculate_date - self.start_date).days / self.annual_days.value
                    _rebate = [r * self.s0 * elapsed_time for r in self.rebate]
                else:  # 绝对数值
                    _rebate = self.rebate
                if self.status == StatusType.UpTouch:  # 向上触碰
                    return _rebate[1]
                else:  # 向下触碰
                    return _rebate[0]
            elif self.payment_type == PaymentType.Expire:  # 到期支付
                if self.rebate_annual:  # 年化百分比
                    _rebate = [r * self.s0 * tenor for r in self.rebate]
                else:  # 绝对数值
                    _rebate = self.rebate
                if self.status == StatusType.UpTouch:  # 向上触碰
                    return _rebate[1] * self.engine.process.interest.disc_factor(_maturity)
                else:  # 向下触碰
                    return _rebate[0] * self.engine.process.interest.disc_factor(_maturity)
            else:
                raise ValueError(f"未知的支付方式{self.payment_type}")
        else:  # 尚未触碰障碍，正常定价
            if self.engine.engine_type == EngineType.PdeEngine:  # 接口特殊是因为PDE引擎兼容了单双边障碍
                price = self.engine.calc_present_value(prod=self, t=t, spot=spot, bound=self.bound,
                                                       rebate=self.rebate, window=self.window)
            else:
                price = self.engine.calc_present_value(prod=self, t=t, spot=spot)
        return price
