#!/user/bin/env python
# -*- coding: utf-8 -*-
"""
Copyright (C) 2024 Galaxy Technologies
Licensed under the Apache License, Version 2.0
"""
from abc import ABCMeta, abstractmethod
from typing import Callable
from contextlib import suppress
from functools import partial
import datetime
import numpy as np
from ..date_handler import global_evaluation_date
from ..utilities.enums import EngineType
from ..utilities.utility import time_this, logging
from ..pricing_engine_base.engine_base import PricingEngineBase


class OptionBase(metaclass=ABCMeta):
    """衍生品基类"""

    def __init__(self):
        """初始化
        process: StochProcessBase，标的资产随机过程
        engine: PricingEngine，定价引擎
        engine_method: str, 定价引擎种类。
                若未输入engine，可以用engine_method快捷生成指定定价引擎。可选Analytic、MC/MonteCarlo、PDE/FDM、QUAD、TREE。
        trade_calendar: TradeCalendar，交易日历
        start_date : datetime.date，起始日
        end_date: datetime.date，到期日
        """
        self.process = None
        self.engine = None
        self.trade_calendar = None
        self.start_date = None
        self.end_date = None
        # 反算波动率
        self.calc_implied_bs_vol = partial(self.calc_param, reset_param_method=self.reset_vol)

    @abstractmethod
    def set_pricing_engine(self, engine):
        """设置定价引擎，指定当前的定价函数_pricing_func.
        根据产品类型，可能存在闭式解、蒙特卡洛模拟、PDE有限差分、积分法、树方法等定价方法。
        """

    @abstractmethod
    def __repr__(self):
        """返回期权的描述"""

    @abstractmethod
    def price(self, *args, **kwargs):
        """执行定价
        对于标的期初价格为s0的期权，以香草期权为例，返回的是BSM公式的定价结果，即max(0, St - K)期望的现值
        如果想要得到单位估值，需要再除以s0"""
        self.validate_parameters()

    def validate_parameters(self, t=None):
        """price方法中，先检查产品是否已经配置了定价引擎，估值日是否小于终止日
        Args:
            t: datetime.date，计算期权价格的日期，默认None，此时使用全局估值日"""
        if self.engine is None or not isinstance(self.engine, PricingEngineBase):
            raise AttributeError("尚未配置定价引擎，您可以在以下4种方法中选择:\n"
                                 "    1. 在产品类同时输入s,r,q,vol以创建定价引擎，可以用engine_method='MC'/'PDE'等指定引擎种类;\n"
                                 "    2. 在产品类同时输入s,r,q,vol和engine_method='MC'/'PDE'...以创建指令种类的定价引擎;\n"
                                 "    3. 在产品类同时输入标的资产随机过程StochProcess实例和engine_method='MC'/'PDE'...以创建指令种类的定价引擎;\n"
                                 "    4. 自行创建定价引擎，再使用set_pricing_engine方法配置到产品中")
        calculate_date = global_evaluation_date() if t is None else t
        date_message = None
        if calculate_date < self.start_date:
            date_message = f"估值日必须大于等于起始日, 当前估值日为{calculate_date}, 起始日为{self.start_date}.\n"
        if hasattr(self, "stop_date") and calculate_date > self.stop_date:  # 某些带有敲出的衍生品，会有终止日stop_date属性
            date_message = f"估值日必须小于等于终止日, 当前估值日为{calculate_date}, 终止日为{self.stop_date}.\n"
        elif calculate_date > self.end_date:
            date_message = f"估值日必须小于等于到期日, 当前估值日为{calculate_date}, 到期日为{self.end_date}.\n"
        if date_message:
            raise ValueError(date_message + f"默认估值日是距离今天最近的交易日。请在price(t=evaluation_date)指定估值日，或者使用set_evaluation_date(datetime.date(YYYY, mm, dd))配置全局估值日。")

    def delta(self, spot=None, step=None, *args, **kwargs):
        """求价格spot处的delta值
        会先尝试调用定价引擎的delta方法，如果没有则使用通用的差分法计算
        Args:
            spot: float，价格的绝对值
            step: float，微扰步长，默认为价格spot的0.1%
        Returns: delta = ∂V/∂S
        """
        try:
            return self.engine.delta(prod=self, spot=spot, step=step, *args, **kwargs)
        except:
            self.validate_parameters()
            spot = self.engine.process.spot() if spot is None else spot
            step = spot * 0.001 if step is None else step
            up_price = self.price(spot=spot + step)
            down_price = self.price(spot=spot - step)
            delta = (up_price - down_price) / (2 * step)
            return delta

    def gamma(self, spot=None, step=None, *args, **kwargs):
        """求价格spot处的gamma值
        会先尝试调用定价引擎的gamma方法，如果没有则使用通用的差分法计算
        Args:
            spot: float，价格的绝对值
            step: float，微扰步长，默认为价格spot的0.1%
        Returns: gamma = ∂2V/∂S2
        """
        try:
            return self.engine.gamma(prod=self, spot=spot, step=step, *args, **kwargs)
        except:
            self.validate_parameters()
            spot = self.engine.process.spot() if spot is None else spot
            step = spot * 0.001 if step is None else step
            up_price = self.price(spot=spot + step)
            down_price = self.price(spot=spot - step)
            mid_price = self.price(spot=spot)
            gamma = (up_price - 2 * mid_price + down_price) / (step ** 2)
            return gamma

    def vega(self, spot=None, step=0.01, *args, **kwargs):
        """计算波动率上升1%的vega todo:目前只支持常数波动率
        会先尝试调用定价引擎的vega方法，如果没有则使用通用的差分法计算
        Args:
            spot: float，价格的绝对值
            step: float，微扰步长，默认为1%
        Returns: vega = ∂V/∂σ
        """
        try:
            return self.engine.vega(prod=self, spot=spot, step=step, *args, **kwargs)
        except:
            self.validate_parameters()
            spot = self.engine.process.spot() if spot is None else spot
            last_vol = self.engine.process.vol.data
            last_price = self.price(spot=spot)
            self.engine.process.vol.data = last_vol + step
            new_price = self.price(spot=spot)
            self.engine.process.vol.data = last_vol
            vega = (new_price - last_price)
            return vega

    def theta(self, spot=None, step=1, *args, **kwargs):
        """计算一天的theta值
        会先尝试调用定价引擎的theta方法，如果没有则使用通用的差分法计算
        Args:
            spot: float，价格的绝对值
            step: int，时间步长，默认为1天
        Returns: theta = ∂V/∂t
        """
        try:
            return self.engine.theta(prod=self, spot=spot, step=step, *args, **kwargs)
        except:
            try:
                spot = self.engine.process.spot() if spot is None else spot
                step = datetime.timedelta(days=step)
                origin_price = self.price(spot=spot)
                next_date = self.trade_calendar.advance(global_evaluation_date(), step)
                new_price = self.price(t=next_date, spot=spot)
                theta = (new_price - origin_price)
                return theta
            except:  # 可能遇到到期日估值，无法计算theta
                calculate_date = global_evaluation_date()
                if calculate_date > self.trade_calendar.advance(self.end_date, datetime.timedelta(days=1)):
                    raise ValueError(
                        f"估值日必须小于等于到期日, 当前估值日为{calculate_date}, 到期日为{self.end_date}.")
                return np.nan  # 到期日估值，无法计算theta,返回nan

    def rho(self, spot=None, step=0.01, *args, **kwargs):
        """计算无风险利率上升1%的rho todo:目前只支持常数无风险利率
        会先尝试调用定价引擎的rho方法，如果没有则使用通用的差分法计算
        Args:
            spot: float，价格的绝对值
            step: float，微扰步长，默认为1%
        Returns: rho = ∂V/∂r
        """
        try:
            return self.engine.rho(prod=self, spot=spot, step=step, *args, **kwargs)
        except:
            self.validate_parameters()
            spot = self.engine.process.spot() if spot is None else spot
            last_r = self.engine.process.interest.data
            last_price = self.price(spot=spot)
            self.engine.process.interest.data = last_r + step
            new_price = self.price(spot=spot)
            self.engine.process.interest.data = last_r
            rho = (new_price - last_price)
            return rho

    def vanna(self, spot=None, step=0.001, *args, **kwargs):
        """计算vanna todo:目前只支持常数波动率
        会先尝试调用定价引擎的vanna方法，如果没有则使用通用的差分法计算
        Args:
            spot: float，价格的绝对值
            step: float，微扰步长，默认为1%
        Returns: vanna = ∂2V/∂S∂σ
        """
        try:
            return self.engine.vanna(prod=self, spot=spot, step=step, *args, **kwargs)
        except:
            self.validate_parameters()
            spot = self.engine.process.spot() if spot is None else spot
            last_vol = self.engine.process.vol.data
            last_delta = self.delta(spot=spot)
            self.engine.process.vol.data = last_vol + step
            new_delta = self.delta(spot=spot)
            self.engine.process.vol.data = last_vol
            vanna = (new_delta - last_delta) / step
            return vanna

    def volga(self, spot=None, step=0.001, *args, **kwargs):
        """计算volga todo:目前只支持常数波动率
        会先尝试调用定价引擎的volga方法，如果没有则使用通用的差分法计算
        Args:
            spot: float，价格的绝对值
            step: float，微扰步长，默认为1%
        Returns: volga = ∂2V/∂σ2
        """
        try:
            return self.engine.volga(prod=self, spot=spot, step=step, *args, **kwargs)
        except:
            self.validate_parameters()
            spot = self.engine.process.spot() if spot is None else spot
            last_vol = self.engine.process.vol.data
            price = self.price(spot=spot)
            self.engine.process.vol.data = last_vol - step
            down_price = self.price(spot=spot)
            self.engine.process.vol.data = last_vol + step
            up_price = self.price(spot=spot)
            self.engine.process.vol.data = last_vol
            volga = (up_price - 2 * price + down_price) / step ** 2
            return volga

    def pv_and_greeks(self, spot=None, *args, **kwargs):
        """当一次性计算pv和5个greeks时，可以调用此函数，减少计算量
        会先尝试调用定价引擎的pv_and_greeks方法，如果没有则使用通用的差分法计算
        Args:
            t: int，期权价值矩阵的列(时间)的索引
            spot: float，价格的绝对值
        Returns:
            Dict[str:float]: {'pv': pv, 'delta': delta, 'gamma': gamma, 'theta': theta, 'vega': vega, 'rho': rho}
        """
        # if hasattr(self.engine, "pv_and_greeks"):
        #     with suppress(AttributeError, ValueError):
        try:
            if self.engine.engine_type == EngineType.PdeEngine:  # PDE定价引擎需要先经过一次定价，生成S-t估值矩阵
                self.price()
            return self.engine.pv_and_greeks(prod=self, spot=spot, *args, **kwargs)
        except:
            self.validate_parameters()
            spot = self.engine.process.spot() if spot is None else spot
            pv = np.float64(self.price(spot=spot))
            s_step = spot * 0.01
            up_price = self.price(spot=spot + s_step)
            down_price = self.price(spot=spot - s_step)
            delta = (up_price - down_price) / (2 * s_step)
            gamma = (up_price - 2 * pv + down_price) / (s_step ** 2)

            last_vol = self.engine.process.vol.data
            self.engine.process.vol.data = last_vol + 0.01
            new_price = self.price(spot=spot)
            self.engine.process.vol.data = last_vol
            vega = (new_price - pv)

            last_r = self.engine.process.interest.data
            self.engine.process.interest.data = last_r + 0.01
            new_price = self.price(spot=spot)
            self.engine.process.interest.data = last_r
            rho = (new_price - pv)

            try:  # 可能遇到到期日估值，无法计算theta
                t_step = datetime.timedelta(days=1)
                next_date = self.trade_calendar.advance(global_evaluation_date(), t_step)
                new_price = self.price(t=next_date, spot=spot)
                theta = (new_price - pv)
            except Exception as e:
                logging.error(e)
                theta = np.nan
            return {'pv': pv, 'delta': delta, 'gamma': gamma, 'vega': vega, 'theta': theta, 'rho': rho}

    @time_this
    def calc_param(self, reset_param_method: Callable, target_value: float = None, t: datetime.date = None, spot=None,
                   high=2., low=0., thres=0.001, max_iters=20, *args, **kwargs):
        """根据目标估值Y，用二分法反算衍生品参数X
        Args:
            reset_param_method: class method，重置衍生品参数X的类方法
            target_value: float，目标估值，对期权而言是期权的市场价格(期权费)，对票据是估值。
                                票据的公允估值=名义本金(pricelib估值不含名本，因此默认target_value=期初价格s0)
            t: datetime.date，计算衍生品价格的日期
            spot: float，计算衍生品价格的标的资产价格
            high: float，二分法的衍生品参数X搜索区间高点
            low: float，二分法的衍生品参数X搜索区间低点
            thres: float，判断二分法收敛的阈值
            max_iters: int, 二分法最大迭代次数，避免无限循环
        Returns:
        """
        mid, estimate = None, None
        for i in range(max_iters):
            mid = (high + low) / 2
            if mid < thres:
                mid = thres
            temp_param = mid
            reset_param_method(temp_param, *args, **kwargs)
            estimate = self.price(t=t, spot=spot)
            logging.info(f"{reset_param_method.__name__}参数 = {temp_param}: estimate = {estimate}")
            if abs(estimate - target_value) <= abs(thres * target_value):
                break
            elif estimate > target_value * (1 + thres):
                high = mid
            elif estimate < target_value * (1 - thres):
                low = mid
        return mid, estimate

    def reset_vol(self, vol):
        """重置标的资产的波动率的值"""
        self.process.volatility.data = vol
