import math from dataclasses import dataclass from typing import List import numpy as np class PayoffEvaluator: def evaluate(self, path: List[List[float]], context: "PayoffContext") -> float: raise NotImplementedError @dataclass class PayoffContext: days_to_maturity: int r: float capital_value: float prot_min_val: float pdi_barrier: float pdi_style: str coupon_in_memory: float days_to_obs_y_fract: List[float] coupon_triggers: List[float] coupon_values: List[float] autocall_triggers: List[float] autocall_values: List[float] memory_flags: List[int] caso: str pdi_strike: float fattore_airbag: float trigger_one_star: float cap: float days_to_obs: List[int] volatility: List[float] airbag: int sigma: int relief: int twin_win: int one_star: int leva: float nominal_amount: float prices_ul: List[float] class PayoffFactory: @staticmethod def get_evaluator(caso: str) -> PayoffEvaluator: mapping = { "Standard": PayoffStandard(), "Airbag": PayoffAirbag(), "Sigma": PayoffSigma(), "Relief": PayoffRelief(), "TwinWin": PayoffTwinWin(), "OneStar": PayoffOneStar(), "Airbag + OneStar": PayoffAirbagOneStar(), "Sigma + OneStar": PayoffSigmaOneStar(), "Relief + OneStar": PayoffReliefOneStar(), "TwinWin + OneStar": PayoffTwinWinOneStar(), "Airbag + TwinWin": PayoffAirbagTwinWin(), } if caso not in mapping: raise ValueError(f"Payoff '{caso}' non gestito.") return mapping[caso] def _row_min(path, index: int) -> float: if isinstance(path, np.ndarray): return float(path[index].min()) return min(path[index]) def _row_max(path, index: int) -> float: if isinstance(path, np.ndarray): return float(path[index].max()) return max(path[index]) def _overall_min(path) -> float: if isinstance(path, np.ndarray): return float(path.min()) return _overall_min(path) class PayoffStandard(PayoffEvaluator): def __init__(self): self.last_label = "" def evaluate(self, path: List[List[float]], c: PayoffContext) -> float: memory = c.coupon_in_memory coupons = 0.0 payoff = 0.0 for k in range(len(c.days_to_obs)): min_val = _row_min(path, c.days_to_obs[k]) if min_val >= c.autocall_triggers[k]: payoff = (c.autocall_values[k] + memory) * math.exp(-c.r * c.days_to_obs_y_fract[k]) + coupons self.last_label = ( f"Autocall ? Trigger={c.autocall_triggers[k]:.2f}, Min={min_val:.2f} ? Payoff={payoff:.2f}" ) return payoff if min_val >= c.coupon_triggers[k]: coupons += (c.coupon_values[k] + memory) * math.exp(-c.r * c.days_to_obs_y_fract[k]) memory = 0.0 else: memory += c.memory_flags[k] * c.coupon_values[k] if k == len(c.days_to_obs) - 1: min_mat = _row_min(path, c.days_to_maturity) min_total = _overall_min(path) barrier_check = min_mat if c.pdi_style == "European" else min_total if barrier_check >= c.pdi_barrier: payoff = (c.capital_value + memory) * math.exp(-c.r * c.days_to_obs_y_fract[k]) + coupons self.last_label = ( f"RIMBORSO standard ? minMat={min_mat:.4f}, minTotal={min_total:.4f} ? Payoff={payoff:.2f}" ) else: payoff = max(c.prot_min_val, min_mat / 100.0) * math.exp(-c.r * c.days_to_obs_y_fract[k]) + coupons self.last_label = ( f"PERDITA standard ? minMat={min_mat:.4f}, minTotal={min_total:.4f} ? Payoff={payoff:.2f}" ) return payoff self.last_label = "UNKNOWN" return 0.0 class PayoffAirbag(PayoffEvaluator): def evaluate(self, path: List[List[float]], c: PayoffContext) -> float: memory = c.coupon_in_memory coupons = 0.0 for k in range(len(c.days_to_obs)): min_val = _row_min(path, c.days_to_obs[k]) if min_val >= c.autocall_triggers[k]: return (c.autocall_values[k] + memory) * math.exp(-c.r * c.days_to_obs_y_fract[k]) + coupons if min_val >= c.coupon_triggers[k]: coupons += (c.coupon_values[k] + memory) * math.exp(-c.r * c.days_to_obs_y_fract[k]) memory = 0.0 else: memory += c.memory_flags[k] * c.coupon_values[k] if k == len(c.days_to_obs) - 1: min_mat = _row_min(path, c.days_to_maturity) min_total = _overall_min(path) sopra_barriera = ( (c.pdi_style == "European" and min_mat >= c.pdi_barrier) or (c.pdi_style == "American" and min_total >= c.pdi_barrier) ) if sopra_barriera: payoff = c.capital_value + memory else: payoff = max(c.prot_min_val, (min_mat * c.fattore_airbag / 100.0)) return payoff * math.exp(-c.r * c.days_to_obs_y_fract[k]) + coupons return 0.0 class PayoffTwinWin(PayoffEvaluator): def evaluate(self, path: List[List[float]], c: PayoffContext) -> float: memory = c.coupon_in_memory coupons = 0.0 for k in range(len(c.days_to_obs)): min_val = _row_min(path, c.days_to_obs[k]) if min_val >= c.autocall_triggers[k]: return (c.autocall_values[k] + memory) * math.exp(-c.r * c.days_to_obs_y_fract[k]) + coupons if min_val >= c.coupon_triggers[k]: coupons += (c.coupon_values[k] + memory) * math.exp(-c.r * c.days_to_obs_y_fract[k]) memory = 0.0 else: memory += c.memory_flags[k] * c.coupon_values[k] if k == len(c.days_to_obs) - 1: min_mat = _row_min(path, c.days_to_maturity) min_total = _overall_min(path) sopra_strike = min_mat >= c.pdi_strike and c.autocall_triggers[k] == 999 sopra_barriera = ( (c.pdi_style == "European" and min_mat >= c.pdi_barrier) or (c.pdi_style == "American" and min_total >= c.pdi_barrier) ) if sopra_strike: payoff = memory + min(min_mat / 100.0, c.cap) elif sopra_barriera: payoff = (200.0 - min_mat) * c.capital_value / 100.0 + memory else: payoff = max(c.prot_min_val, min_mat / 100.0) + memory return payoff * math.exp(-c.r * c.days_to_obs_y_fract[k]) + coupons return 0.0 class PayoffOneStar(PayoffEvaluator): def evaluate(self, path: List[List[float]], c: PayoffContext) -> float: memory = c.coupon_in_memory coupons = 0.0 for k in range(len(c.days_to_obs)): min_val = _row_min(path, c.days_to_obs[k]) if min_val >= c.autocall_triggers[k]: return (c.autocall_values[k] + memory) * math.exp(-c.r * c.days_to_obs_y_fract[k]) + coupons if min_val >= c.coupon_triggers[k]: coupons += (c.coupon_values[k] + memory) * math.exp(-c.r * c.days_to_obs_y_fract[k]) memory = 0.0 else: memory += c.memory_flags[k] * c.coupon_values[k] if k == len(c.days_to_obs) - 1: max_mat = _row_max(path, c.days_to_obs[k]) min_mat = _row_min(path, c.days_to_maturity) min_total = _overall_min(path) sopra_trigger = max_mat >= c.trigger_one_star sopra_barriera = ( (c.pdi_style == "European" and min_mat >= c.pdi_barrier) or (c.pdi_style == "American" and min_total >= c.pdi_barrier) ) capitale = c.capital_value if (sopra_trigger or sopra_barriera) else max(c.prot_min_val, min_mat / 100.0) cedola_finale = 0.0 if min_mat >= c.coupon_triggers[k]: cedola_finale = memory + c.coupon_values[k] return ( capitale * math.exp(-c.r * c.days_to_obs_y_fract[k]) + cedola_finale * math.exp(-c.r * c.days_to_obs_y_fract[k]) + coupons ) return 0.0 class PayoffSigma(PayoffEvaluator): def evaluate(self, path: List[List[float]], c: PayoffContext) -> float: memory = c.coupon_in_memory coupons = 0.0 for k in range(len(c.days_to_obs)): min_val = _row_min(path, c.days_to_obs[k]) if min_val >= c.autocall_triggers[k]: return (c.autocall_values[k] + memory) * math.exp(-c.r * c.days_to_obs_y_fract[k]) + coupons if min_val >= c.coupon_triggers[k]: coupons += (c.coupon_values[k] + memory) * math.exp(-c.r * c.days_to_obs_y_fract[k]) memory = 0.0 else: memory += c.memory_flags[k] * c.coupon_values[k] if k == len(c.days_to_obs) - 1: min_mat = _row_min(path, c.days_to_maturity) min_total = _overall_min(path) sopra_barriera = ( (c.pdi_style == "European" and min_mat >= c.pdi_barrier) or (c.pdi_style == "American" and min_total >= c.pdi_barrier) ) if sopra_barriera: payoff = c.capital_value + memory else: payoff = max(c.prot_min_val, (min_mat + c.pdi_strike - c.pdi_barrier) / 100.0) return payoff * math.exp(-c.r * c.days_to_obs_y_fract[k]) + coupons return 0.0 class PayoffRelief(PayoffEvaluator): def evaluate(self, path: List[List[float]], c: PayoffContext) -> float: memory = c.coupon_in_memory coupons = 0.0 for k in range(len(c.days_to_obs)): min_val = _row_min(path, c.days_to_obs[k]) if min_val >= c.autocall_triggers[k]: return (c.autocall_values[k] + memory) * math.exp(-c.r * c.days_to_obs_y_fract[k]) + coupons if min_val >= c.coupon_triggers[k]: coupons += (c.coupon_values[k] + memory) * math.exp(-c.r * c.days_to_obs_y_fract[k]) memory = 0.0 else: memory += c.memory_flags[k] * c.coupon_values[k] if k == len(c.days_to_obs) - 1: prices_at_mat = path[c.days_to_maturity] sorted_prices = sorted(prices_at_mat) second_min = sorted_prices[1] if len(sorted_prices) >= 2 else sorted_prices[0] min_mat = min(prices_at_mat) min_total = _overall_min(path) sopra_barriera = ( (c.pdi_style == "European" and min_mat >= c.pdi_barrier) or (c.pdi_style == "American" and min_total >= c.pdi_barrier) ) if sopra_barriera: payoff = c.capital_value + memory else: payoff = max(c.prot_min_val, second_min / 100.0) return payoff * math.exp(-c.r * c.days_to_obs_y_fract[k]) + coupons return 0.0 class PayoffAirbagOneStar(PayoffEvaluator): def evaluate(self, path: List[List[float]], c: PayoffContext) -> float: memory = c.coupon_in_memory coupons = 0.0 for k in range(len(c.days_to_obs)): min_val = _row_min(path, c.days_to_obs[k]) if min_val >= c.autocall_triggers[k]: return (c.autocall_values[k] + memory) * math.exp(-c.r * c.days_to_obs_y_fract[k]) + coupons if min_val >= c.coupon_triggers[k]: coupons += (c.coupon_values[k] + memory) * math.exp(-c.r * c.days_to_obs_y_fract[k]) memory = 0.0 else: memory += c.memory_flags[k] * c.coupon_values[k] if k == len(c.days_to_obs) - 1: max_mat = _row_max(path, c.days_to_obs[k]) min_mat = _row_min(path, c.days_to_maturity) min_total = _overall_min(path) sopra_trigger = max_mat >= c.trigger_one_star sopra_barriera = ( (c.pdi_style == "European" and min_mat >= c.pdi_barrier) or (c.pdi_style == "American" and min_total >= c.pdi_barrier) ) if sopra_trigger or sopra_barriera: capitale = c.capital_value else: perc = max(c.prot_min_val, min_mat / 100.0) * c.fattore_airbag capitale = min(c.capital_value, perc) cedola_finale = 0.0 if min_mat >= c.coupon_triggers[k]: cedola_finale = memory + c.coupon_values[k] return ( capitale * math.exp(-c.r * c.days_to_obs_y_fract[k]) + cedola_finale * math.exp(-c.r * c.days_to_obs_y_fract[k]) + coupons ) return 0.0 class PayoffSigmaOneStar(PayoffEvaluator): def evaluate(self, path: List[List[float]], c: PayoffContext) -> float: memory = c.coupon_in_memory coupons = 0.0 for k in range(len(c.days_to_obs)): min_val = _row_min(path, c.days_to_obs[k]) if min_val >= c.autocall_triggers[k]: return (c.autocall_values[k] + memory) * math.exp(-c.r * c.days_to_obs_y_fract[k]) + coupons if min_val >= c.coupon_triggers[k]: coupons += (c.coupon_values[k] + memory) * math.exp(-c.r * c.days_to_obs_y_fract[k]) memory = 0.0 else: memory += c.memory_flags[k] * c.coupon_values[k] if k == len(c.days_to_obs) - 1: max_mat = _row_max(path, c.days_to_obs[k]) min_mat = _row_min(path, c.days_to_maturity) min_total = _overall_min(path) sopra_trigger = max_mat >= c.trigger_one_star sopra_barriera = ( (c.pdi_style == "European" and min_mat >= c.pdi_barrier) or (c.pdi_style == "American" and min_total >= c.pdi_barrier) ) if sopra_trigger or sopra_barriera: capitale = c.capital_value else: capitale = max(c.prot_min_val, (min_mat + c.pdi_strike - c.pdi_barrier) / 100.0) cedola_finale = 0.0 if min_mat >= c.coupon_triggers[k]: cedola_finale = memory + c.coupon_values[k] return ( capitale * math.exp(-c.r * c.days_to_obs_y_fract[k]) + cedola_finale * math.exp(-c.r * c.days_to_obs_y_fract[k]) + coupons ) return 0.0 class PayoffReliefOneStar(PayoffEvaluator): def evaluate(self, path: List[List[float]], c: PayoffContext) -> float: memory = c.coupon_in_memory coupons = 0.0 for k in range(len(c.days_to_obs)): min_val = _row_min(path, c.days_to_obs[k]) if min_val >= c.autocall_triggers[k]: return (c.autocall_values[k] + memory) * math.exp(-c.r * c.days_to_obs_y_fract[k]) + coupons if min_val >= c.coupon_triggers[k]: coupons += (c.coupon_values[k] + memory) * math.exp(-c.r * c.days_to_obs_y_fract[k]) memory = 0.0 else: memory += c.memory_flags[k] * c.coupon_values[k] if k == len(c.days_to_obs) - 1: prices_at_mat = path[c.days_to_maturity] sorted_prices = sorted(prices_at_mat) second_min = sorted_prices[1] if len(sorted_prices) >= 2 else sorted_prices[0] min_mat = min(prices_at_mat) max_mat = _row_max(path, c.days_to_obs[k]) min_total = _overall_min(path) sopra_trigger = max_mat >= c.trigger_one_star sopra_barriera = ( (c.pdi_style == "European" and min_mat >= c.pdi_barrier) or (c.pdi_style == "American" and min_total >= c.pdi_barrier) ) if sopra_trigger or sopra_barriera: capitale = c.capital_value else: capitale = max(c.prot_min_val, second_min / 100.0) cedola_finale = 0.0 if min_mat >= c.coupon_triggers[k]: cedola_finale = memory + c.coupon_values[k] return ( capitale * math.exp(-c.r * c.days_to_obs_y_fract[k]) + cedola_finale * math.exp(-c.r * c.days_to_obs_y_fract[k]) + coupons ) return 0.0 class PayoffTwinWinOneStar(PayoffEvaluator): def evaluate(self, path: List[List[float]], c: PayoffContext) -> float: memory = c.coupon_in_memory coupons = 0.0 for k in range(len(c.days_to_obs)): min_val = _row_min(path, c.days_to_obs[k]) if min_val >= c.autocall_triggers[k]: return (c.autocall_values[k] + memory) * math.exp(-c.r * c.days_to_obs_y_fract[k]) + coupons if min_val >= c.coupon_triggers[k]: coupons += (c.coupon_values[k] + memory) * math.exp(-c.r * c.days_to_obs_y_fract[k]) memory = 0.0 else: memory += c.memory_flags[k] * c.coupon_values[k] if k == len(c.days_to_obs) - 1: max_mat = _row_max(path, c.days_to_obs[k]) min_mat = _row_min(path, c.days_to_maturity) min_total = _overall_min(path) sopra_strike = min_mat >= c.pdi_strike sopra_barriera = ( (c.pdi_style == "European" and min_mat >= c.pdi_barrier) or (c.pdi_style == "American" and min_total >= c.pdi_barrier) ) if sopra_strike: capitale = min(min_mat / 100.0, c.cap) elif sopra_barriera: capitale = (2 * c.capital_value) - (min_mat / 100.0) else: if max_mat >= c.trigger_one_star: capitale = c.capital_value else: capitale = max(c.prot_min_val, min_mat / 100.0) cedola_finale = 0.0 if min_mat >= c.coupon_triggers[k]: cedola_finale = memory + c.coupon_values[k] return ( capitale * math.exp(-c.r * c.days_to_obs_y_fract[k]) + cedola_finale * math.exp(-c.r * c.days_to_obs_y_fract[k]) + coupons ) return 0.0 class PayoffAirbagTwinWin(PayoffEvaluator): def evaluate(self, path: List[List[float]], c: PayoffContext) -> float: memory = c.coupon_in_memory coupons = 0.0 for k in range(len(c.days_to_obs)): min_val = _row_min(path, c.days_to_obs[k]) if min_val >= c.autocall_triggers[k]: return (c.autocall_values[k] + memory) * math.exp(-c.r * c.days_to_obs_y_fract[k]) + coupons if min_val >= c.coupon_triggers[k]: coupons += (c.coupon_values[k] + memory) * math.exp(-c.r * c.days_to_obs_y_fract[k]) memory = 0.0 else: memory += c.memory_flags[k] * c.coupon_values[k] if k == len(c.days_to_obs) - 1: min_mat = _row_min(path, c.days_to_maturity) if min_mat >= c.pdi_barrier: if c.autocall_triggers[k] == 999 and min_mat >= c.pdi_strike: return ( math.exp(-c.r * c.days_to_obs_y_fract[k]) * (memory + min(min_mat / 100.0, c.cap)) + coupons ) return ( math.exp(-c.r * c.days_to_obs_y_fract[k]) * ((2 * c.capital_value) + memory - (min_mat / 100.0)) + coupons ) return ( max(c.prot_min_val, min_mat / 100.0) * math.exp(-c.r * c.days_to_obs_y_fract[k]) * c.fattore_airbag + coupons ) return 0.0