#!/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 StatusType, CallPut, PaymentType, OutMechanism
from pricelib.common.date_handler import (global_evaluation_date, Schedule, CN_CALENDAR, AnnualDays)
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.mc_engines import MCAccumulatorEngine


class Accumulator(OptionBase, Observer):
    """累购累沽期权，支持标准累购累沽（敲出不终止），熔断累购累沽（敲出终止），不敲出累购累沽（无敲出）
    todo: 固定赔付累购累沽，增强型累购累沽，末日加倍累购累沽
    以累购为例：行权价通常低于二级市场标的现价，敲出障碍价通常高于二级市场标的现价。
             在每个交易日，期权买方按以下规则买入标的建仓，或者直接获得以下规则的现金收益/亏损：
                        如果标的价格在执行价之下，以行权价购买N份标的，N是杠杆比例，例如N=2，此时发生多倍建仓的亏损；
                        如果标的价格在执行价之上，以行权价购买1份标的，此时享有折价建仓的收益；
             敲出机制有三种：敲出不终止、敲出终止、不敲出。
                在敲出观察日，如果标的价格在敲出障碍价之上，发生敲出，
                        (1)敲出不终止，下一观察日继续观察。(标准累购累沽)
                        (2)敲出终止，合约提前终止 (熔断累购累沽)，一次性获得剩余观察日（含敲出当日）的每日固定现金补偿。
                (3)不敲出，只要标的价格在行权价之上，就可以用行权价购买1份标的。但是不敲出的累购的行权价一般高于标准累沽，折价建仓收益较小。
             到期时，最终的收益即是累计的所有标的份数 x (结算时的标的价格 - 行权价)。
    累沽与累沽相反，行权价通常高于二级市场标的现价，敲出障碍价通常低于二级市场标的现价。
    """

    def __init__(self, s0, amt, barrier_out=None, strike=None, leverage_ratio=None, callput=CallPut.Call,
                 out_mechanism=OutMechanism.NoTerminate, rebate=0.0, payment_type=PaymentType.Expire, obs_type="d",
                 engine=None, status=(0.0, StatusType.NoTouch), maturity=None, start_date=None, obs_dates=None,
                 end_date=None, trade_calendar=CN_CALENDAR, annual_days=AnnualDays.N365, t_step_per_year=243,
                 stoch_process=None, s=None, r=None, q=None, vol=None, engine_method=None):
        """构造函数
        产品参数:
            s0: float，标的初始价格
            amt: float，单日执行量，例如10吨、100手等
            callput: CallPut枚举类，期权类型，call累购/put累沽
            strike: float，行权价
            leverage_ratio: float, 杠杆比例，累购期权下，当标的价格在执行价之下时，购买leverage_ratio份标的；
                                                    当标的价格在执行价之上、敲出价之下时，购买1份标的。累沽同理。
            out_mechanism: OutMechanism枚举类，敲出机制，有三种：敲出不终止NoTerminate/敲出终止（熔断）Terminate/不敲出NoKnockOut
            barrier_out: float，敲出障碍价
            rebate: float，敲出补偿(元/(天*amt))。发生敲出时，合约提前终止（敲出熔断），一次性获得剩余观察日（含敲出当日）的每日固定现金补偿。
            payment_type: PaymentType枚举类，结算方式，有两种：每个观察日建仓，到期结算Expire/每个观察日立即结算现金收益Hit
            obs_type: str，观察日类型，仅支持每日观察 'd'
            status: 存续期的持仓状态。(amount, isknockout), isknockout代表是否发生敲出，默认为StatusType.NoTouch未敲出
                    当PaymentType.Expire时，amount代表已累计仓位，正数表示买入，负数表示卖出，期初默认是0.
                    当PaymentType.Hit时，amount代表已发生的收益/亏损的现值。期初默认是0.
        时间参数: 要么输入年化期限，要么输入起始日和到期日；敲出观察日可缺省
            maturity: float，年化期限
            start_date: datetime.date，起始日
            end_date: datetime.date，到期日
            obs_dates: List[datetime.date]，敲出观察日，可缺省，缺省时会自动生成每月观察的敲出日期序列(已根据节假日调整)
            trade_calendar: 交易日历，Calendar类，默认为中国内地交易日历
            annual_days: int，每年的自然日数量
            t_step_per_year: int，每年的交易日数量
        可选参数:
            engine: 定价引擎，PricingEngine类，仅支持Monte Carlo模拟 MCAccumulatorEngine
            engine_method: str, 定价引擎种类。若未输入engine，可以用engine_method快捷生成指定定价引擎。可选MC/MonteCarlo
            若未提供引擎engine、也未指定引擎种类engine_method的情况下，如果提供了 随机过程 或 标的价格、无风险利率、分红/融券率、波动率，
            则默认使用蒙特卡洛模拟定价引擎 (BSM模型)
            stoch_process: 随机过程StochProcessBase对象
            s: float，标的价格
            r: float，无风险利率
            q: float，分红/融券率
            vol: float，波动率
        """
        super().__init__()
        self.s0 = s0  # 标的初始价格
        self.amt = amt  # 单日执行量
        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))
        if maturity is None:
            self.maturity = (end_date - start_date).days / annual_days.value
        else:
            self.maturity = maturity
        self.callput = callput  # 期权类型: 累购/累沽
        self.out_mechanism = out_mechanism  # 敲出机制
        self.barrier_out = barrier_out  # 敲出线
        self.strike = strike  # 行权价
        self.payment_type = payment_type  # 结算方式
        self.leverage_ratio = leverage_ratio  # 杠杆比率
        if self.out_mechanism != OutMechanism.NoKnockOut:
            if self.callput == CallPut.Call:  # 累购
                assert barrier_out > strike, "Error:有敲出的累购期权的敲出障碍价必须大于行权价。"
            else:  # 累沽
                assert barrier_out < strike, "Error:有敲出的累沽期权的敲出障碍价必须小于行权价。"
        if obs_dates is None:  # 观察日列表
            self.obs_dates = Schedule(trade_calendar=trade_calendar, start=self.start_date, end=self.end_date,
                                      freq=obs_type, lock_term=0)  # 累购累沽无锁定期
            self.end_date = self.obs_dates.end  # 修正结束时间
        else:
            self.obs_dates = Schedule(trade_calendar=trade_calendar, date_schedule=obs_dates)
        self.status = list(status)
        self.rebate = rebate
        if engine is not None:
            self.set_pricing_engine(engine)
        else:
            if engine_method is None:
                default_engine = MCAccumulatorEngine  # 默认MC定价引擎
            elif engine_method in ["MC", "MonteCarlo"]:
                default_engine = MCAccumulatorEngine
            else:
                raise NotImplementedError(f"不支持的engine_method{engine_method}, {self.__repr__()}仅支持MC")
            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):
        """收到标的价格变化的通知时，自动更新状态status"""
        if global_auto_update_status():
            calculate_date = global_evaluation_date()
            if calculate_date < self.start_date or calculate_date > self.end_date:
                return  # 如果在起始日前或到期日之后，状态维持不变
            if observable == self.engine.process.spot:
                if calculate_date in self.obs_dates.date_schedule:  # 如果估值日是敲出观察日
                    self._accumulating(observable.data, calculate_date)

    def _accumulating(self, new_price, calculate_date):
        """根据最新价格，判断收益支付"""
        if (self.callput == CallPut.Call and new_price < self.strike) or (  # 累购
                self.callput == CallPut.Put and new_price > self.strike):  # 累沽
            self.discounted_positioning(new_price, self.leverage_ratio)  # 折价建仓
        else:  # 标的价格没有以不利方向突破执行价时
            if self.out_mechanism == OutMechanism.NoKnockOut:  # 不敲出
                self.discounted_positioning(new_price, 1)  # 折价建仓
            else:  # 有敲出价格
                obs_idx = self.obs_dates.date_schedule.index(calculate_date)
                if isinstance(self.barrier_out, (float, int)):
                    barrier_out = self.barrier_out
                elif isinstance(self.barrier_out, (list, np.ndarray)):
                    barrier_out = self.barrier_out[obs_idx]
                else:
                    raise TypeError(f"Error: 不支持的敲出障碍价类型{type(self.barrier_out)}")
                if self.out_mechanism == OutMechanism.Terminate:  # 敲出终止（熔断）
                    if (self.callput == CallPut.Call and new_price > barrier_out):  # 累购
                        self.status[1] = StatusType.UpTouch
                        logging.info(f"{self}已向上敲出，提前终止")
                    elif (self.callput == CallPut.Put and new_price < barrier_out):  # 累沽
                        self.status[1] = StatusType.DownTouch
                        logging.info(f"{self}已向下敲出，提前终止")
                    else:
                        self.discounted_positioning(new_price, 1)  # 折价建仓
                elif self.out_mechanism == OutMechanism.NoTerminate:  # 敲出不终止
                    if (self.callput == CallPut.Call and new_price > barrier_out) or (  # 累购
                            self.callput == CallPut.Put and new_price < barrier_out):  # 累沽
                        pass
                    else:
                        self.discounted_positioning(new_price, 1)  # 折价建仓
                else:
                    raise NotImplementedError(f"不支持的敲出机制{self.out_mechanism}")

    def discounted_positioning(self, new_price, ratio=1):
        """观察日，折价建仓/立即结算现金收益"""
        if self.payment_type == PaymentType.Expire:  # 折价建仓
            new_amt = self.callput.value * self.amt * ratio
            self.status[0] += new_amt
            logging.info(f"{self}新增建仓量{new_amt}，累计建仓量{self.status[0]}")
        else:  # 立即结算现金收益
            new_pnl = (new_price - self.strike) * self.callput.value * self.amt * ratio
            self.status[0] = round(self.status[0] + new_pnl, 4)
            logging.info(f"{self} 新增现金盈亏 {new_pnl}，累计现金盈亏 {self.status[0]}")

    def __repr__(self):
        """返回期权的描述"""
        if self.callput == CallPut.Call:
            callput_str = "累购"  # (Accumulator)
        else:
            callput_str = "累沽"  # (Decumulator)
        if self.out_mechanism == OutMechanism.NoTerminate:
            out_mechanism_str = "标准"
        elif self.out_mechanism == OutMechanism.Terminate:
            out_mechanism_str = "熔断"
        else:
            out_mechanism_str = "不敲出"
        if self.payment_type == PaymentType.Expire:
            payment_type_str = "建仓到期结算"
        else:
            payment_type_str = "现金立即结算"
        return f"{out_mechanism_str}{callput_str}-{payment_type_str}"

    @time_this
    def price(self, t: datetime.date = None, spot=None):
        """计算期权价格
        Args:
            t: datetime.date，计算期权价格的日期
            spot: float，标的价格
        Returns: 期权现值
        """
        self.validate_parameters(t=t)
        price = self.engine.calc_present_value(prod=self, t=t, spot=spot)
        return price
