561 lines
20 KiB
Python
561 lines
20 KiB
Python
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
|
|
|