#!/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 (CallPut, InOut, UpDown, BarrierType, PaymentType, EngineType, StatusType,
                                             ExerciseType)
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 AnalyticVanillaEuEngine, AnalyticBarrierEngine
from pricelib.pricing_engines.fdm_engines import FdmVanillaEngine, FdmBarrierEngine
from pricelib.pricing_engines.mc_engines import MCBarrierEngine
from pricelib.pricing_engines.integral_engines import QuadBarrierEngine
from pricelib.pricing_engines.tree_engines import BiTreeBarrierEngine
from pricelib.products.vanilla.vanilla_option import VanillaOption



class BarrierOption(OptionBase, Observer):
    """单边障碍期权
    敲入期权在障碍被触及时才会生效，敲出期权在障碍被触及时立即失效"""

    def __init__(self, strike, barrier, rebate, callput=CallPut.Call, inout=InOut.Out, updown=UpDown.Up, *, parti=1,
                 obs_type=ExerciseType.American, payment_type=None, rebate_annual=False, floor_yield=0.0, engine=None,
                 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, stoch_process=None,
                 s0=None, s=None, r=None, q=None, vol=None, engine_method=None):
        """构造函数
        产品参数:
            strike: float, 执行价
            barrier: float, 障碍价
            rebate: float, 敲出现金返还金额，或未敲入现金返还金额
            rebate_annual: bool, 现金返还是否为年化，默认为False，即绝对数值，非年化，非百分比；若为True，则为百分比年化收益率
            callput: 看涨看跌类型，CallPut枚举类，Call看涨/Put看跌
            inout: 敲入敲出类型，InOut枚举类，In敲入/Out敲出
            updown: 向上向下类型，UpDown枚举类，Up向上/Down向下
            obs_type: 观察类型，ExerciseType枚举类，默认为American美式观察, 可选European欧式观察 (即到期观察)
            payment_type: 现金返还支付类型，PaymentType枚举类，敲入默认为到期支付Expire；敲出默认为立即支付Hit
            parti: float, 香草期权的参与率，默认为1
            floor_yield: float, 敲入或未敲出时，在香草的payoff之外，额外的保底收益率，年化收益，百分比小数
            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，即现金返还为年化收益率时；
                                  或者当 floor_yield != 0，即存在非零的保底收益率时，
                       需要输入标的期初价格s0，用于将百分比的收益率转化成绝对数值
            engine: 定价引擎，PricingEngine类，以下几种引擎均支持向上/向下、敲入/敲出、看涨/看跌8种单边障碍期权，支持敲出/未敲入现金返还
                            对于美式障碍，解析解和PDE引擎支持连续观察，其余引擎只支持离散观察(默认为每日观察)
                    解析解: AnalyticBarrierEngine 支持欧式障碍（到期观察）和美式障碍（支持连续观察/离散观察）
                                        欧式障碍：到期观察，支持保底收益率，支持支持现金返还为绝对数值/年化百分比
                                        美式障碍：敲入现金返还为到期支付；敲出现金返还为立即支付。现金返还为固定绝对数值，不支持年化的现金返还。
                                                不支持同时输入非零的现金返还和不等于1的参与率，二者只有其一时支持。不支持保底收益率
                    蒙特卡洛: MCBarrierEngine 支持欧式障碍（到期观察）和美式障碍（仅支持离散观察）
                                        欧式障碍：到期观察，支持保底收益率，支持支持现金返还为绝对数值/年化百分比
                                        美式障碍 ：敲入现金返还为到期支付；敲出现金返还 支持 立即支付/到期支付
                                                支持现金返还为绝对数值/年化百分比，支持保底收益率
                    PDE: FdmBarrierEngine  支持欧式障碍（到期观察）和美式障碍（支持连续观察/离散观察）
                                        欧式障碍：到期观察，支持保底收益率，支持支持现金返还为绝对数值/年化百分比
                                        美式障碍 ：敲入现金返还为到期支付；敲出现金返还 支持 立即支付/到期支付
                                                支持现金返回为绝对数值/年化百分比，支持保底收益率
                    积分法: QuadBarrierEngine  仅支持离散观察的美式障碍，todo: 支持欧式障碍、年化的现金返还、保底收益率
                                            敲入现金返还为到期支付；敲出现金返还 支持 立即支付/到期支付
                    二叉树: BiTreeBarrierEngine  仅支持离散观察的美式障碍，todo: 支持欧式障碍、年化的现金返还、保底收益率
                                            敲入现金返还为到期支付；敲出现金返还 支持 立即支付/到期支付
            engine_method: str, 定价引擎种类。若未输入engine，可以用engine_method快捷生成指定定价引擎。可选Analytic、MC/MonteCarlo、PDE/FDM、QUAD、TREE。
            若未提供引擎engine、也未指定引擎种类engine_method的情况下，如果提供了 随机过程 或 标的价格、无风险利率、分红/融券率、波动率，
            则默认使用解析解定价引擎 AnalyticBarrierEngine - Merton(1973), Reiner & Rubinstein(1991a)
                                支持 Broadie, Glasserman和Kou(1995)均匀离散观察调整
            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
        self.strike = strike
        self.barrier = barrier
        self.rebate = rebate  # 敲出现金返还，或未敲入现金返还
        self.rebate_annual = rebate_annual  # 现金返还是否为年化
        self.parti = parti
        self._callput = callput
        self._inout = inout
        self._updown = updown
        self.barrier_type = BarrierType.get_category(self._updown, self._inout, self._callput)
        if payment_type is None:  # 敲入默认为到期支付Expire；敲出默认为立即支付Hit
            if self._inout == InOut.In:
                self.payment_type = PaymentType.Expire
            else:
                self.payment_type = PaymentType.Hit
        else:
            self.payment_type = payment_type
            if self._inout == InOut.In:
                assert payment_type == PaymentType.Expire, "ValueError: 敲入期权的现金返还方式PaymentType一定是到期支付Expire"
        self.obs_type = obs_type
        self.floor_yield = floor_yield
        self.s0 = s0
        if self.rebate_annual or self.floor_yield != 0:
            assert s0 is not None, "Error: 现金返还为年化收益率时；或者存在非零的保底收益率时，需要输入标的期初价格s0，用于将百分比的收益率转化成绝对数值"
        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: 到期观察的障碍期权，现金返还的支付方式只能是到期支付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:
                if self.analytic_check():
                    default_engine = AnalyticBarrierEngine
                else:
                    default_engine = FdmBarrierEngine
            elif engine_method == "Analytic":
                default_engine = AnalyticBarrierEngine
            elif engine_method in ["PDE", "FDM"]:
                default_engine = FdmBarrierEngine
            elif engine_method in ["MC", "MonteCarlo"]:
                default_engine = MCBarrierEngine
            elif engine_method == "QUAD":
                default_engine = QuadBarrierEngine
            elif engine_method == "TREE":
                default_engine = BiTreeBarrierEngine
            else:
                raise NotImplementedError(
                    f"不支持的engine_method{engine_method}, {self.__repr__()}仅支持Analytic/MC/PDE/QUAD/TREE")
            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 analytic_check(self):
        """检查输入的产品参数是否可以使用解析解定价引擎"""
        if self.obs_type == ExerciseType.American:  # 美式障碍期权解析解有限制条件
            if self.rebate_annual or self.floor_yield != 0:
                return False  # 美式障碍解析解只支持绝对数值的现金返还，非百分比，非年化; 不支持保底收益率
            if self.parti != 1 and self.rebate != 0:
                # 障碍期权解析解，由于公式限制，参与率parti不等于1、现金返还rebate不等于0不能共存，否则结果错误
                return False
            if self.inout == InOut.In and self.payment_type != PaymentType.Expire:
                # 敲入期权解析解，一直未敲入，到期时支付现金返还。payment_type只支持Expire
                return False
            if self.inout == InOut.Out and self.payment_type != PaymentType.Hit:
                # 敲出期权解析解，发生敲出，立刻支付现金返还。payment_type应该是Hit
                return False
        return True

    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:
            if self.obs_type == ExerciseType.American:
                # TODO: 目前美式观察障碍期权的update仅支持连续观察，或者每日观察（因为每周、每月观察日不确定，需要额外输入参数）
                if observable.data >= self.barrier and self.updown == UpDown.Up:
                    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.barrier and self.updown == UpDown.Down:
                    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.barrier and self.updown == UpDown.Up:
                        self.status = StatusType.UpTouch
                    elif observable.data <= self.barrier and self.updown == UpDown.Down:
                        self.status = StatusType.DownTouch

    def __repr__(self):
        """返回期权的描述"""
        if self.obs_type == ExerciseType.American:
            if self.discrete_obs_interval is None:
                obs_type = "连续观察"
            else:
                obs_type = "离散观察"
        elif self.obs_type == ExerciseType.European:
            obs_type = "到期观察"
        else:
            raise ValueError(f"ExerciseType {self.obs_type} is not supported")
        return f"{obs_type}{self.barrier_type.value.text}期权"

    @property
    def callput(self):
        return self._callput

    @callput.setter
    def callput(self, value):
        self._callput = value
        self.status = StatusType.NoTouch  # 产品参数改变，重置是否已经敲出或敲入
        self.barrier_type = BarrierType.get_category(self._updown, self._inout, self._callput)

    @property
    def updown(self):
        return self._updown

    @updown.setter
    def updown(self, value):
        self._updown = value
        self.status = StatusType.NoTouch  # 产品参数改变，重置是否已经敲出或敲入
        self.barrier_type = BarrierType.get_category(self._updown, self._inout, self._callput)

    @property
    def inout(self):
        return self._inout

    @inout.setter
    def inout(self, value):
        self._inout = value
        self.status = StatusType.NoTouch  # 产品参数改变，重置是否已经敲出或敲入
        self.barrier_type = BarrierType.get_category(self._updown, self._inout, self._callput)

    @time_this
    def price(self, t: datetime.date = None, spot=None):
        """计算期权价格
        Args:
            t: datetime.date，计算期权价格的日期
            spot: float，标的价格
        Returns: 期权现值
        """
        self.validate_parameters(t=t)
        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
            # 敲出现金返还
            if self.inout == InOut.Out:  # 已经发生敲出，支付现金返还rebate
                if self.rebate_annual:
                    _rebate = self.rebate * tenor * self.s0  # 年化百分比（仅适用于到期支付rebate的情形，敲出立即支付需自行按敲出时间计算）
                else:
                    _rebate = self.rebate  # 绝对数值
                if self.payment_type == PaymentType.Hit:  # 立即支付
                    if self.rebate_annual:
                        elapsed_time = (calculate_date - self.start_date).days / self.annual_days.value
                        _rebate = self.rebate * self.s0 * elapsed_time
                    return _rebate
                elif self.payment_type == PaymentType.Expire:  # 到期支付
                    return _rebate * self.engine.process.interest.disc_factor(_maturity)
                else:
                    raise ValueError(f"未知的支付方式{self.payment_type}")
            elif self.inout == InOut.In:  # 已经发生敲入，返回香草期权的价值
                vanilla_engine = AnalyticVanillaEuEngine(stoch_process=self.engine.process)
                in_option = VanillaOption(exercise_type=ExerciseType.European, callput=self.callput, strike=self.strike,
                                          start_date=self.start_date, end_date=self.end_date, engine=vanilla_engine)
                # 保底收益率
                _floor_yield = 0
                if self.floor_yield != 0:  # 保底收益率需要输入期初价格s0作为名义本金
                    _floor_yield = self.floor_yield * tenor * self.s0
                return in_option.price(t, spot) + _floor_yield * self.engine.process.interest.disc_factor(_maturity)
            else:
                raise ValueError("inout只能是InOut.In或InOut.Out")
        else:  # 尚未触碰障碍，正常定价
            if self.engine.engine_type == EngineType.PdeEngine:  # 接口特殊是因为PDE引擎兼容了单双边障碍，所以需要把barrier填入bound
                if self.updown == UpDown.Up:
                    bound = (None, self.barrier)
                else:
                    bound = (self.barrier, None)
                price = self.engine.calc_present_value(prod=self, t=t, spot=spot, bound=bound,
                                                       rebate=(self.rebate, self.rebate))
            else:
                price = self.engine.calc_present_value(prod=self, t=t, spot=spot)
        return price
