#!/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 TouchType, EngineType, ExerciseType, 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.analytic_engines import AnalyticDoubleDigitalEngine
from pricelib.pricing_engines.fdm_engines import FdmDigitalEngine
from pricelib.pricing_engines.mc_engines import MCDoubleDigitalEngine


class DoubleDigitalOption(OptionBase, Observer):
    """双边二元期权
        欧式: 二元凹式/二元凸式
        美式: 美式双接触/美式双不接触"""

    def __init__(self, touch_type=TouchType.Touch, exercise_type=ExerciseType.American, payment_type=PaymentType.Expire,
                 *, strike_lower=95., strike_upper=105., rebate_lower=0., rebate_upper=0., rebate_annual=False,
                 status=StatusType.NoTouch,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):
        """构造函数
        产品参数:
            touch_type: 接触方式 - 接触Touch/不接触NotTouch
            exercise_type: 行权方式，ExerciseType枚举类，European/American
            payment_type: 支付方式，PaymentType枚举类，立即支付Hit/到期支付Expire
            bound: (strike_lower, strike_upper), (低行权价/高行权价)
            rebate: (rebate_lower, rebate_upper), 行权收益的绝对数值，非年化
                    双接触: (下边界payoff, 上边界payoff); 双不接触: (到期不接触的payoff, 无作用)
            rebate_annual: bool, 现金返还是否为年化，默认为False，即绝对数值，非年化，非百分比；若为True，则为百分比年化收益率
            status: 定价时，触碰障碍的状态，StatusType枚举类，默认为NoTouch未触碰，UpTouch为向上触碰，DownTouch为向下触碰
        时间参数: 要么输入年化期限，要么输入起始日和到期日
            maturity: float，年化期限
            start_date: datetime.date，起始日
            end_date: datetime.date，到期日
            trade_calendar: 交易日历，Calendar类，默认为中国内地交易日历
            annual_days: int，每年的自然日数量
            t_step_per_year: int，每年的交易日数量
            discrete_obs_interval: 观察时间间隔. 若为连续观察，None；若为均匀离散观察，为年化的观察时间间隔
        可选参数:
            s0: float，标的期初价格，只有当 rebate_annual = True，即现金返还为年化收益率时；
                       需要输入标的期初价格，将百分比的收益率转化成绝对数值
            engine: 定价引擎，PricingEngine类
                    解析解: AnalyticDoubleDigitalEngine - Hui(1996) 级数近似解，双接触期权的下边界payoff和上边界payoff必须相等
                                                        支持 连续/离散观察、到期支付的(美式)双接触/双不接触
                    PDE: FdmDigitalEngine 支持 (欧式)二元凹式/二元凸式；支持 连续/离散观察、立即/到期支付的(美式)双接触/美式双不接触
                    蒙特卡洛: MCDoubleDigitalEngine 支持 (欧式)二元凹式/二元凸式；只支持离散观察、立即/到期支付的(美式)双接触/美式双不接触
            engine_method: str, 定价引擎种类。若未输入engine，可以用engine_method快捷生成指定定价引擎。可选Analytic、MC/MonteCarlo、PDE/FDM。
            若未提供引擎engine、也未指定引擎种类engine_method的情况下，如果提供了 随机过程 或 标的价格、无风险利率、分红/融券率、波动率，
            则使用默认定价引擎：
                到期支付的美式双接触/双不接触默认使用 Hui(1996) 级数近似解 AnalyticDoubleDigitalEngine
                其余情况默认使用 PDE 定价引擎 FdmDigitalEngine
            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.bound = (strike_lower, strike_upper)  # (低行权价, 高行权价)
        self.rebate = (rebate_lower, rebate_upper)  # 双接触: (下边界payoff, 上边界payoff); 双不接触: (不接触payoff, 无作用)
        self.rebate_annual = rebate_annual  # 现金返还是否为年化
        self.touch_type = touch_type
        self.exercise_type = exercise_type
        self.payment_type = payment_type
        if self.exercise_type == ExerciseType.European or self.touch_type == TouchType.NoTouch:
            assert payment_type == PaymentType.Expire, "ValueError: 欧式双边二元期权或美式双不触碰期权的现金返还方式PaymentType一定是到期支付Expire"
        self.discrete_obs_interval = discrete_obs_interval  # 连续观察=None；均匀离散观察=观察时间间隔
        if self.exercise_type == ExerciseType.European:
            assert self.discrete_obs_interval is None, "ValueError: 欧式双边二元期权，观察时间间隔只能为None."
        self.s0 = s0
        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:
                if (exercise_type == ExerciseType.European or (  # 支持二元凸式/二元凹式解析解
                        exercise_type == ExerciseType.American and payment_type == PaymentType.Expire and (
                        touch_type == TouchType.NoTouch or (  # 美式双接触/双不接触只支持到期支付，
                        touch_type == TouchType.Touch and rebate_lower == rebate_upper)))):  # 若为美式双接触，则上下边界payoff必须相等
                    default_engine = AnalyticDoubleDigitalEngine
                else:
                    default_engine = FdmDigitalEngine
            elif engine_method == "Analytic":
                default_engine = AnalyticDoubleDigitalEngine
            elif engine_method in ["PDE", "FDM"]:
                default_engine = FdmDigitalEngine  # todo 支持 rebate_annual 年化的现金返还
            elif engine_method in ["MC", "MonteCarlo"]:
                default_engine = MCDoubleDigitalEngine  # todo 支持 rebate_annual 年化的现金返还
            else:
                raise NotImplementedError(
                    f"不支持的engine_method{engine_method}, {self.__repr__()}仅支持Analytic/MC/PDE")
            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):
        """收到标的价格变化的通知时，自动更新是否已经触碰障碍"""
        if global_evaluation_date() > self.stop_date:  # 如果估值日已经过了终止日，维持原来的状态
            return
        if global_auto_update_status() and observable == self.engine.process.spot:
            # 如果是欧式双边二元，且估值日是到期日时，更新状态  todo: 暂时只支持日频行情更新，即估值日的第一次标的价格变化即视为收盘价更新
            if self.exercise_type == ExerciseType.European and global_evaluation_date() == self.end_date:
                self.change_status(observable.data)
            # 如果是美式双边二元，连续观察或每日离散观察，每次标的价格变化，均更新状态
            elif self.exercise_type == ExerciseType.American:
                # todo: 均匀离散观察，只支持每日观察，暂不支持其他观察频率。即每次标的价格变化，均更新状态
                self.change_status(observable.data)

    def change_status(self, spot):
        """获取状态变化"""
        if self.status == StatusType.NoTouch:
            if spot >= self.bound[1]:
                self.status = StatusType.UpTouch
                if self.payment_type == PaymentType.Hit:  # 若美式双接触提前终止
                    self.stop_date = global_evaluation_date()
            elif spot <= self.bound[0]:
                self.status = StatusType.DownTouch
                if self.payment_type == PaymentType.Hit:  # 若美式双接触提前终止
                    self.stop_date = global_evaluation_date()
        else:
            return

    def __repr__(self):
        """返回期权的描述"""
        if self.exercise_type == ExerciseType.European:
            if self.touch_type == TouchType.Touch:
                return f"二元凹式(欧式双接触期权)"
            else:  # NoTouch
                return f"二元凸式(欧式双不接触期权)"
        elif self.exercise_type == ExerciseType.American:
            if self.discrete_obs_interval is None:
                obs_type = "连续观察"
            else:
                obs_type = "离散观察"
            return f"{self.payment_type.value}{obs_type}美式双{self.touch_type.value}二元期权"
        else:
            raise ValueError("无法识别的期权类型")

    @time_this
    def price(self, t=None, spot=None):
        self.validate_parameters(t=t)
        if self.status != StatusType.NoTouch:  # 如果已经触碰障碍，则直接返回行权收益的现值
            if self.touch_type == TouchType.NoTouch:
                return 0
            elif self.touch_type == TouchType.Touch:
                calculate_date = global_evaluation_date() if t is None else t
                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]
                    elif self.status == StatusType.UpTouch:
                        return _rebate[0]
                elif self.payment_type == PaymentType.Expire:  # 到期支付
                    if self.rebate_annual:  # 年化百分比
                        tenor = (self.end_date - self.start_date).days / self.annual_days.value  # 从起始日到终止日的自然日时间（年）
                        _rebate = [r * self.s0 * tenor for r in self.rebate]
                    else:  # 绝对数值
                        _rebate = self.rebate
                    _maturity = (self.end_date - calculate_date).days / self.annual_days.value
                    if self.status == StatusType.UpTouch:
                        return _rebate[1] * self.engine.process.interest.disc_factor(_maturity)
                    elif self.status == StatusType.UpTouch:
                        return _rebate[0] * self.engine.process.interest.disc_factor(_maturity)
                else:
                    raise ValueError(f"未知的支付方式{self.payment_type}")
            else:
                raise ValueError("Error: TouchType只能是NoTouch或Touch")
        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)
            else:
                price = self.engine.calc_present_value(prod=self, t=t, spot=spot)
        return price
