commit f707b9c95b2b1abe7d31fa91a8ca79b2ab90bf17 Author: fredmaloggia Date: Mon Jan 5 19:15:03 2026 +0100 primo commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..ac6c1ad --- /dev/null +++ b/README.md @@ -0,0 +1,10 @@ +# Pricer Python port (WIP) + +This folder hosts the Python conversion of the C# pricer. + +Notes: +- Uses the same `appsettings.json` from the parent `Pricer` project. +- Target: feature parity with the C# console app and LibraryPricer. + +Run (once implemented): + python app.py diff --git a/VALIDATION.md b/VALIDATION.md new file mode 100644 index 0000000..f72a398 --- /dev/null +++ b/VALIDATION.md @@ -0,0 +1,23 @@ +Validation checklist (C# vs Python) + +Goal: verify output parity for the same ISIN and inputs. + +1) Build inputs + - Use the same ISIN and the same prompts in both apps. + - Set the same number of simulations. + - Use the same dividend flag (s/n). + - Optional: set the same RNG seed in Python. + +2) Compare core outputs + - Correlation matrix and volatility values. + - Fair value output line (value and date). + - Percent "Sopra/Sotto" and average gain/loss. + - Valore atteso. + +3) Edge cases + - Ask = 0 and Bid > 0 (reference price changes to Bid). + - Ex-date warning when DaysToExDate > DaysToObs and last ExDate > last ObsDate. + +Notes +- Monte Carlo output is stochastic; use a seed to reduce variance. +- If parity differs, compare intermediate inputs printed in the tables. diff --git a/app.py b/app.py new file mode 100644 index 0000000..1db7016 --- /dev/null +++ b/app.py @@ -0,0 +1,11 @@ +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parent +if str(ROOT) not in sys.path: + sys.path.insert(0, str(ROOT)) + +from pricer.cli import main + +if __name__ == "__main__": + main() diff --git a/appsettings.json b/appsettings.json new file mode 100644 index 0000000..b12e2ec --- /dev/null +++ b/appsettings.json @@ -0,0 +1,14 @@ +{ + "ConnectionStrings": { + "FirstSolutionDB": "Driver={ODBC Driver 17 for SQL Server};Server=26.69.45.60;Database=FirstSolutionDB;UID=sa;PWD=Skyline72;" + }, + "exclude": [ + "**/bin", + "**/bower_components", + "**/jspm_packages", + "**/node_modules", + "**/obj", + "**/platforms" + ] +} + diff --git a/pricer/__init__.py b/pricer/__init__.py new file mode 100644 index 0000000..5438d59 --- /dev/null +++ b/pricer/__init__.py @@ -0,0 +1 @@ +"""Pricer package.""" diff --git a/pricer/__pycache__/__init__.cpython-312.pyc b/pricer/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..090763f Binary files /dev/null and b/pricer/__pycache__/__init__.cpython-312.pyc differ diff --git a/pricer/__pycache__/analytics.cpython-312.pyc b/pricer/__pycache__/analytics.cpython-312.pyc new file mode 100644 index 0000000..fdeccb9 Binary files /dev/null and b/pricer/__pycache__/analytics.cpython-312.pyc differ diff --git a/pricer/__pycache__/calc._nj_caso_airbag-893.py312.1.nbc b/pricer/__pycache__/calc._nj_caso_airbag-893.py312.1.nbc new file mode 100644 index 0000000..d9fb341 Binary files /dev/null and b/pricer/__pycache__/calc._nj_caso_airbag-893.py312.1.nbc differ diff --git a/pricer/__pycache__/calc._nj_caso_airbag-893.py312.nbi b/pricer/__pycache__/calc._nj_caso_airbag-893.py312.nbi new file mode 100644 index 0000000..063be7c Binary files /dev/null and b/pricer/__pycache__/calc._nj_caso_airbag-893.py312.nbi differ diff --git a/pricer/__pycache__/calc._nj_caso_airbag_one_star-980.py312.1.nbc b/pricer/__pycache__/calc._nj_caso_airbag_one_star-980.py312.1.nbc new file mode 100644 index 0000000..ebca38c Binary files /dev/null and b/pricer/__pycache__/calc._nj_caso_airbag_one_star-980.py312.1.nbc differ diff --git a/pricer/__pycache__/calc._nj_caso_airbag_one_star-980.py312.nbi b/pricer/__pycache__/calc._nj_caso_airbag_one_star-980.py312.nbi new file mode 100644 index 0000000..9d0b253 Binary files /dev/null and b/pricer/__pycache__/calc._nj_caso_airbag_one_star-980.py312.nbi differ diff --git a/pricer/__pycache__/calc._nj_caso_airbag_twinwin-1020.py312.1.nbc b/pricer/__pycache__/calc._nj_caso_airbag_twinwin-1020.py312.1.nbc new file mode 100644 index 0000000..cd4003d Binary files /dev/null and b/pricer/__pycache__/calc._nj_caso_airbag_twinwin-1020.py312.1.nbc differ diff --git a/pricer/__pycache__/calc._nj_caso_airbag_twinwin-1020.py312.nbi b/pricer/__pycache__/calc._nj_caso_airbag_twinwin-1020.py312.nbi new file mode 100644 index 0000000..3f1ad59 Binary files /dev/null and b/pricer/__pycache__/calc._nj_caso_airbag_twinwin-1020.py312.nbi differ diff --git a/pricer/__pycache__/calc._nj_caso_one_star-961.py312.1.nbc b/pricer/__pycache__/calc._nj_caso_one_star-961.py312.1.nbc new file mode 100644 index 0000000..0e5a153 Binary files /dev/null and b/pricer/__pycache__/calc._nj_caso_one_star-961.py312.1.nbc differ diff --git a/pricer/__pycache__/calc._nj_caso_one_star-961.py312.nbi b/pricer/__pycache__/calc._nj_caso_one_star-961.py312.nbi new file mode 100644 index 0000000..b750c7a Binary files /dev/null and b/pricer/__pycache__/calc._nj_caso_one_star-961.py312.nbi differ diff --git a/pricer/__pycache__/calc._nj_caso_relief-925.py312.1.nbc b/pricer/__pycache__/calc._nj_caso_relief-925.py312.1.nbc new file mode 100644 index 0000000..4dc0d0e Binary files /dev/null and b/pricer/__pycache__/calc._nj_caso_relief-925.py312.1.nbc differ diff --git a/pricer/__pycache__/calc._nj_caso_relief-925.py312.nbi b/pricer/__pycache__/calc._nj_caso_relief-925.py312.nbi new file mode 100644 index 0000000..8fb1359 Binary files /dev/null and b/pricer/__pycache__/calc._nj_caso_relief-925.py312.nbi differ diff --git a/pricer/__pycache__/calc._nj_caso_relief_one_star-1000.py312.1.nbc b/pricer/__pycache__/calc._nj_caso_relief_one_star-1000.py312.1.nbc new file mode 100644 index 0000000..f03338c Binary files /dev/null and b/pricer/__pycache__/calc._nj_caso_relief_one_star-1000.py312.1.nbc differ diff --git a/pricer/__pycache__/calc._nj_caso_relief_one_star-1000.py312.nbi b/pricer/__pycache__/calc._nj_caso_relief_one_star-1000.py312.nbi new file mode 100644 index 0000000..ed0414f Binary files /dev/null and b/pricer/__pycache__/calc._nj_caso_relief_one_star-1000.py312.nbi differ diff --git a/pricer/__pycache__/calc._nj_caso_sigma-909.py312.1.nbc b/pricer/__pycache__/calc._nj_caso_sigma-909.py312.1.nbc new file mode 100644 index 0000000..3e0a46b Binary files /dev/null and b/pricer/__pycache__/calc._nj_caso_sigma-909.py312.1.nbc differ diff --git a/pricer/__pycache__/calc._nj_caso_sigma-909.py312.nbi b/pricer/__pycache__/calc._nj_caso_sigma-909.py312.nbi new file mode 100644 index 0000000..46400d7 Binary files /dev/null and b/pricer/__pycache__/calc._nj_caso_sigma-909.py312.nbi differ diff --git a/pricer/__pycache__/calc._nj_caso_sigma_one_star-990.py312.1.nbc b/pricer/__pycache__/calc._nj_caso_sigma_one_star-990.py312.1.nbc new file mode 100644 index 0000000..062849e Binary files /dev/null and b/pricer/__pycache__/calc._nj_caso_sigma_one_star-990.py312.1.nbc differ diff --git a/pricer/__pycache__/calc._nj_caso_sigma_one_star-990.py312.nbi b/pricer/__pycache__/calc._nj_caso_sigma_one_star-990.py312.nbi new file mode 100644 index 0000000..bfc7ee4 Binary files /dev/null and b/pricer/__pycache__/calc._nj_caso_sigma_one_star-990.py312.nbi differ diff --git a/pricer/__pycache__/calc._nj_caso_standard-877.py312.1.nbc b/pricer/__pycache__/calc._nj_caso_standard-877.py312.1.nbc new file mode 100644 index 0000000..d70f803 Binary files /dev/null and b/pricer/__pycache__/calc._nj_caso_standard-877.py312.1.nbc differ diff --git a/pricer/__pycache__/calc._nj_caso_standard-877.py312.nbi b/pricer/__pycache__/calc._nj_caso_standard-877.py312.nbi new file mode 100644 index 0000000..22a11e7 Binary files /dev/null and b/pricer/__pycache__/calc._nj_caso_standard-877.py312.nbi differ diff --git a/pricer/__pycache__/calc._nj_caso_twinwin-941.py312.1.nbc b/pricer/__pycache__/calc._nj_caso_twinwin-941.py312.1.nbc new file mode 100644 index 0000000..fc347ca Binary files /dev/null and b/pricer/__pycache__/calc._nj_caso_twinwin-941.py312.1.nbc differ diff --git a/pricer/__pycache__/calc._nj_caso_twinwin-941.py312.nbi b/pricer/__pycache__/calc._nj_caso_twinwin-941.py312.nbi new file mode 100644 index 0000000..f31c500 Binary files /dev/null and b/pricer/__pycache__/calc._nj_caso_twinwin-941.py312.nbi differ diff --git a/pricer/__pycache__/calc._nj_caso_twinwin_one_star-1010.py312.1.nbc b/pricer/__pycache__/calc._nj_caso_twinwin_one_star-1010.py312.1.nbc new file mode 100644 index 0000000..f7d309e Binary files /dev/null and b/pricer/__pycache__/calc._nj_caso_twinwin_one_star-1010.py312.1.nbc differ diff --git a/pricer/__pycache__/calc._nj_caso_twinwin_one_star-1010.py312.nbi b/pricer/__pycache__/calc._nj_caso_twinwin_one_star-1010.py312.nbi new file mode 100644 index 0000000..5fad583 Binary files /dev/null and b/pricer/__pycache__/calc._nj_caso_twinwin_one_star-1010.py312.nbi differ diff --git a/pricer/__pycache__/calc._nj_max_row-859.py312.1.nbc b/pricer/__pycache__/calc._nj_max_row-859.py312.1.nbc new file mode 100644 index 0000000..f47ade1 Binary files /dev/null and b/pricer/__pycache__/calc._nj_max_row-859.py312.1.nbc differ diff --git a/pricer/__pycache__/calc._nj_max_row-859.py312.nbi b/pricer/__pycache__/calc._nj_max_row-859.py312.nbi new file mode 100644 index 0000000..92247e6 Binary files /dev/null and b/pricer/__pycache__/calc._nj_max_row-859.py312.nbi differ diff --git a/pricer/__pycache__/calc._nj_min_overall-872.py312.1.nbc b/pricer/__pycache__/calc._nj_min_overall-872.py312.1.nbc new file mode 100644 index 0000000..0c23143 Binary files /dev/null and b/pricer/__pycache__/calc._nj_min_overall-872.py312.1.nbc differ diff --git a/pricer/__pycache__/calc._nj_min_overall-872.py312.nbi b/pricer/__pycache__/calc._nj_min_overall-872.py312.nbi new file mode 100644 index 0000000..cf0eef6 Binary files /dev/null and b/pricer/__pycache__/calc._nj_min_overall-872.py312.nbi differ diff --git a/pricer/__pycache__/calc._nj_min_row-854.py312.1.nbc b/pricer/__pycache__/calc._nj_min_row-854.py312.1.nbc new file mode 100644 index 0000000..085401c Binary files /dev/null and b/pricer/__pycache__/calc._nj_min_row-854.py312.1.nbc differ diff --git a/pricer/__pycache__/calc._nj_min_row-854.py312.nbi b/pricer/__pycache__/calc._nj_min_row-854.py312.nbi new file mode 100644 index 0000000..e3cedc7 Binary files /dev/null and b/pricer/__pycache__/calc._nj_min_row-854.py312.nbi differ diff --git a/pricer/__pycache__/calc._nj_payoff_for_path-1031.py312.1.nbc b/pricer/__pycache__/calc._nj_payoff_for_path-1031.py312.1.nbc new file mode 100644 index 0000000..2b38660 Binary files /dev/null and b/pricer/__pycache__/calc._nj_payoff_for_path-1031.py312.1.nbc differ diff --git a/pricer/__pycache__/calc._nj_payoff_for_path-1031.py312.nbi b/pricer/__pycache__/calc._nj_payoff_for_path-1031.py312.nbi new file mode 100644 index 0000000..532f099 Binary files /dev/null and b/pricer/__pycache__/calc._nj_payoff_for_path-1031.py312.nbi differ diff --git a/pricer/__pycache__/calc._nj_payoffs_for_paths-1114.py312.1.nbc b/pricer/__pycache__/calc._nj_payoffs_for_paths-1114.py312.1.nbc new file mode 100644 index 0000000..64645b5 Binary files /dev/null and b/pricer/__pycache__/calc._nj_payoffs_for_paths-1114.py312.1.nbc differ diff --git a/pricer/__pycache__/calc._nj_payoffs_for_paths-1114.py312.nbi b/pricer/__pycache__/calc._nj_payoffs_for_paths-1114.py312.nbi new file mode 100644 index 0000000..0d1da69 Binary files /dev/null and b/pricer/__pycache__/calc._nj_payoffs_for_paths-1114.py312.nbi differ diff --git a/pricer/__pycache__/calc._nj_second_min_row-864.py312.1.nbc b/pricer/__pycache__/calc._nj_second_min_row-864.py312.1.nbc new file mode 100644 index 0000000..f3105e7 Binary files /dev/null and b/pricer/__pycache__/calc._nj_second_min_row-864.py312.1.nbc differ diff --git a/pricer/__pycache__/calc._nj_second_min_row-864.py312.nbi b/pricer/__pycache__/calc._nj_second_min_row-864.py312.nbi new file mode 100644 index 0000000..112eded Binary files /dev/null and b/pricer/__pycache__/calc._nj_second_min_row-864.py312.nbi differ diff --git a/pricer/__pycache__/calc.cpython-312.pyc b/pricer/__pycache__/calc.cpython-312.pyc new file mode 100644 index 0000000..1a85b66 Binary files /dev/null and b/pricer/__pycache__/calc.cpython-312.pyc differ diff --git a/pricer/__pycache__/cli.cpython-312.pyc b/pricer/__pycache__/cli.cpython-312.pyc new file mode 100644 index 0000000..de0a580 Binary files /dev/null and b/pricer/__pycache__/cli.cpython-312.pyc differ diff --git a/pricer/__pycache__/comparison.cpython-312.pyc b/pricer/__pycache__/comparison.cpython-312.pyc new file mode 100644 index 0000000..17bd998 Binary files /dev/null and b/pricer/__pycache__/comparison.cpython-312.pyc differ diff --git a/pricer/__pycache__/config.cpython-312.pyc b/pricer/__pycache__/config.cpython-312.pyc new file mode 100644 index 0000000..29e50e7 Binary files /dev/null and b/pricer/__pycache__/config.cpython-312.pyc differ diff --git a/pricer/__pycache__/db.cpython-312.pyc b/pricer/__pycache__/db.cpython-312.pyc new file mode 100644 index 0000000..a5011ac Binary files /dev/null and b/pricer/__pycache__/db.cpython-312.pyc differ diff --git a/pricer/__pycache__/engines.cpython-312.pyc b/pricer/__pycache__/engines.cpython-312.pyc new file mode 100644 index 0000000..d0b9ac4 Binary files /dev/null and b/pricer/__pycache__/engines.cpython-312.pyc differ diff --git a/pricer/__pycache__/models.cpython-312.pyc b/pricer/__pycache__/models.cpython-312.pyc new file mode 100644 index 0000000..af2a488 Binary files /dev/null and b/pricer/__pycache__/models.cpython-312.pyc differ diff --git a/pricer/__pycache__/payoffs.cpython-312.pyc b/pricer/__pycache__/payoffs.cpython-312.pyc new file mode 100644 index 0000000..c0e0cdf Binary files /dev/null and b/pricer/__pycache__/payoffs.cpython-312.pyc differ diff --git a/pricer/__pycache__/report.cpython-312.pyc b/pricer/__pycache__/report.cpython-312.pyc new file mode 100644 index 0000000..b238add Binary files /dev/null and b/pricer/__pycache__/report.cpython-312.pyc differ diff --git a/pricer/__pycache__/utils.cpython-312.pyc b/pricer/__pycache__/utils.cpython-312.pyc new file mode 100644 index 0000000..922b4be Binary files /dev/null and b/pricer/__pycache__/utils.cpython-312.pyc differ diff --git a/pricer/calc.py b/pricer/calc.py new file mode 100644 index 0000000..7205a82 --- /dev/null +++ b/pricer/calc.py @@ -0,0 +1,1445 @@ +import math +from dataclasses import dataclass +from typing import List, Optional, Tuple + +import os +from concurrent.futures import ProcessPoolExecutor + +import numpy as np + +try: + from .db import default_sql_query + from .models import PrezziSottostanti, UnderlyingStats + from .utils import cumulative_sums, standard_deviation +except ImportError: # Allow running as a script without a package context. + import sys + from pathlib import Path + + pkg_root = Path(__file__).resolve().parents[1] + if str(pkg_root) not in sys.path: + sys.path.insert(0, str(pkg_root)) + + from pricer.db import default_sql_query + from pricer.models import PrezziSottostanti, UnderlyingStats + from pricer.utils import cumulative_sums, standard_deviation + +try: + from numba import njit + _USE_NUMBA = True +except Exception: + _USE_NUMBA = False + def njit(*args, **kwargs): + def wrap(fn): + return fn + return wrap + + +_cached_eod_underlyings: List[PrezziSottostanti] = [] +_cached_fair_value_array = None + + +@dataclass +class FairValueResult: + fair_value: float + fair_value_array: Optional[List[float]] + stdev: float + simulations: int + caso_fair_value: str + + +@dataclass +class FairValueResultArray: + fair_value_array: List[float] + caso_fair_value: Optional[str] + + +def get_cached_eod_underlyings() -> List[PrezziSottostanti]: + return _cached_eod_underlyings + + +def compute_underlying_stats(prezzi_ul: List[PrezziSottostanti]) -> List[UnderlyingStats]: + results = [] + for ul in prezzi_ul: + prezzi = ul.prezzi_close + n = len(prezzi) + log_returns = [] + for i in range(n - 1): + if prezzi[i] > 0 and prezzi[i + 1] > 0: + log_returns.append(math.log(prezzi[i] / prezzi[i + 1])) + else: + log_returns.append(0.0) + + if len(log_returns) > 1: + avg = sum(log_returns) / len(log_returns) + sum_sq = sum((r - avg) ** 2 for r in log_returns) + variance = sum_sq / (len(log_returns) - 1) + std_dev = math.sqrt(variance) + else: + std_dev = 0.0 + + volatility_annualized = std_dev * math.sqrt(252) + + results.append( + UnderlyingStats( + nome=ul.sottostante, + prezzi=prezzi, + log_returns=log_returns, + volatility=volatility_annualized, + ) + ) + return results + +def fair_value( + prices_ul: List[float], + cor_mat: List[List[float]], + num_sottostanti: int, + num_sims: int, + tasso_interesse: float, + days_to_maturity: int, + dividends: List[float], + volatility: List[float], + days_to_observation: List[int], + days_to_observation_y_fract: List[float], + coupon_values: List[float], + coupon_triggers: List[float], + autocall_values: List[float], + autocall_triggers: List[float], + memory_flags: List[int], + coupon_in_memory: float, + pdi_style: Optional[str], + pdi_strike: float, + pdi_barrier: float, + capital_value: float, + prot_min_val: float, + airbag: int, + sigma: int, + twinwin: int, + relief: int, + fattore_airbag: float, + one_star: int, + trigger_one_star: float, + cap: float, + leva: float, +) -> FairValueResult: + pv_digits = [0.0] * num_sims + caso_fair_value = "" + + if _cached_fair_value_array is not None: + pv_digits = _cached_fair_value_array.fair_value_array + caso_fair_value = _cached_fair_value_array.caso_fair_value or "" + else: + pv_digits = simulate_payoffs( + num_sims, + prices_ul, + cor_mat, + days_to_maturity, + tasso_interesse, + dividends, + volatility, + days_to_observation, + days_to_observation_y_fract, + coupon_values, + coupon_triggers, + autocall_values, + autocall_triggers, + memory_flags, + coupon_in_memory, + pdi_style, + pdi_strike, + pdi_barrier, + capital_value, + prot_min_val, + airbag, + sigma, + twinwin, + relief, + fattore_airbag, + one_star, + trigger_one_star, + cap, + ) + + if airbag == 0 and sigma == 0 and relief == 0 and twinwin == 0 and one_star == 0: + caso_fair_value = "Standard" + if airbag == 1 and sigma == 0 and relief == 0 and twinwin == 0 and one_star == 0: + caso_fair_value = "Airbag" + if airbag == 0 and sigma == 1 and relief == 0 and twinwin == 0 and one_star == 0: + caso_fair_value = "Sigma" + if airbag == 0 and sigma == 0 and relief == 1 and twinwin == 0 and one_star == 0: + caso_fair_value = "Relief" + if airbag == 0 and sigma == 0 and relief == 0 and twinwin == 1 and one_star == 0: + caso_fair_value = "TwinWin" + if airbag == 0 and sigma == 0 and relief == 0 and twinwin == 0 and one_star == 1: + caso_fair_value = "OneStar" + if airbag == 1 and sigma == 0 and relief == 0 and twinwin == 0 and one_star == 1: + caso_fair_value = "Airbag + OneStar" + if airbag == 0 and sigma == 1 and relief == 0 and twinwin == 0 and one_star == 1: + caso_fair_value = "Sigma + OneStar" + if airbag == 0 and sigma == 0 and relief == 1 and twinwin == 0 and one_star == 1: + caso_fair_value = "Relief + OneStar" + if airbag == 0 and sigma == 0 and relief == 0 and twinwin == 1 and one_star == 1: + caso_fair_value = "TwinWin + OneStar" + if airbag == 1 and sigma == 0 and relief == 0 and twinwin == 1 and one_star == 0: + caso_fair_value = "Airbag + TwinWin" + + fair_value_value = float(np.mean(pv_digits)) * 100.0 + stdev = standard_deviation([x * 100.0 for x in pv_digits]) + return FairValueResult( + fair_value=fair_value_value, + fair_value_array=[x * 100.0 for x in pv_digits], + stdev=stdev, + simulations=num_sims, + caso_fair_value=caso_fair_value, + ) + +def fair_value_array( + prices_ul: List[float], + cor_mat: List[List[float]], + num_sottostanti: int, + num_sims: int, + tasso_interesse: float, + days_to_maturity: int, + dividends: List[float], + volatility: List[float], + days_to_observation: List[int], + days_to_observation_y_fract: List[float], + coupon_values: List[float], + coupon_triggers: List[float], + autocall_values: List[float], + autocall_triggers: List[float], + memory_flags: List[int], + coupon_in_memory: float, + pdi_style: Optional[str], + pdi_strike: float, + pdi_barrier: float, + capital_value: float, + prot_min_val: float, + airbag: int, + sigma: int, + twinwin: int, + relief: int, + fattore_airbag: float, + one_star: int, + trigger_one_star: float, + cap: float, + leva: float, +) -> FairValueResultArray: + caso_fair_value = "" + pv_digits = [0.0] * num_sims + + pv_digits = simulate_payoffs( + num_sims, + prices_ul, + cor_mat, + days_to_maturity, + tasso_interesse, + dividends, + volatility, + days_to_observation, + days_to_observation_y_fract, + coupon_values, + coupon_triggers, + autocall_values, + autocall_triggers, + memory_flags, + coupon_in_memory, + pdi_style, + pdi_strike, + pdi_barrier, + capital_value, + prot_min_val, + airbag, + sigma, + twinwin, + relief, + fattore_airbag, + one_star, + trigger_one_star, + cap, + ) + + if airbag == 0 and sigma == 0 and relief == 0 and twinwin == 0 and one_star == 0: + caso_fair_value = "Standard" + if airbag == 1 and sigma == 0 and relief == 0 and twinwin == 0 and one_star == 0: + caso_fair_value = "Airbag" + if airbag == 0 and sigma == 1 and relief == 0 and twinwin == 0 and one_star == 0: + caso_fair_value = "Sigma" + if airbag == 0 and sigma == 0 and relief == 1 and twinwin == 0 and one_star == 0: + caso_fair_value = "Relief" + if airbag == 0 and sigma == 0 and relief == 0 and twinwin == 1 and one_star == 0: + caso_fair_value = "TwinWin" + if airbag == 0 and sigma == 0 and relief == 0 and twinwin == 0 and one_star == 1: + caso_fair_value = "OneStar" + if airbag == 1 and sigma == 0 and relief == 0 and twinwin == 0 and one_star == 1: + caso_fair_value = "Airbag + OneStar" + if airbag == 0 and sigma == 1 and relief == 0 and twinwin == 0 and one_star == 1: + caso_fair_value = "Sigma + OneStar" + if airbag == 0 and sigma == 0 and relief == 1 and twinwin == 0 and one_star == 1: + caso_fair_value = "Relief + OneStar" + if airbag == 0 and sigma == 0 and relief == 0 and twinwin == 1 and one_star == 1: + caso_fair_value = "TwinWin + OneStar" + if airbag == 1 and sigma == 0 and relief == 0 and twinwin == 1 and one_star == 0: + caso_fair_value = "Airbag + TwinWin" + + pv_digits = [x * 100.0 for x in pv_digits] + return FairValueResultArray(fair_value_array=pv_digits, caso_fair_value=caso_fair_value) + +def _caso_one_star( + r: float, + days_to_observation_y_fract: List[float], + pdi_style: Optional[str], + pdi_barrier: float, + capital_value: float, + prot_min_val: float, + min_price: float, + min_price_at_maturity: float, + max_price_at_maturity: float, + pv_digit: float, + memory_coupon: float, + coupon_paid: float, + k: int, + trigger_one_star: float, + coupon_triggers: List[float], + coupon_values: List[float], +) -> float: + if min_price_at_maturity < pdi_barrier: + if max_price_at_maturity < trigger_one_star: + payout_capitale = max(prot_min_val, min_price_at_maturity / 100.0) + else: + payout_capitale = capital_value + else: + payout_capitale = capital_value + + payout_cedola = 0.0 + if min_price_at_maturity >= coupon_triggers[k]: + payout_cedola = memory_coupon + coupon_values[k] + + return ( + payout_capitale * math.exp(-r * days_to_observation_y_fract[k]) + + payout_cedola * math.exp(-r * days_to_observation_y_fract[k]) + + coupon_paid + ) + + +def _caso_twinwin( + r: float, + days_to_observation_y_fract: List[float], + pdi_style: Optional[str], + pdi_barrier: float, + capital_value: float, + prot_min_val: float, + min_price: float, + min_price_at_maturity: float, + pv_digit: float, + memory_coupon: float, + coupon_paid: float, + k: int, + pdi_strike: float, + trigger_autocall: float, + cap: float, +) -> float: + if pdi_style == "European" and min_price_at_maturity >= pdi_strike and trigger_autocall == 999: + pv_digit = ( + math.exp(-r * days_to_observation_y_fract[k]) + * (memory_coupon + min(min_price_at_maturity / 100.0, cap)) + + coupon_paid + ) + + if pdi_style == "European" and min_price_at_maturity >= pdi_barrier: + pv_digit = ( + math.exp(-r * days_to_observation_y_fract[k]) + * ((2 * capital_value) + memory_coupon - (min_price_at_maturity / 100.0)) + + coupon_paid + ) + if pdi_style == "European" and min_price_at_maturity < pdi_barrier: + pv_digit = ( + max(prot_min_val, min_price_at_maturity / 100.0) + * math.exp(-r * days_to_observation_y_fract[k]) + + coupon_paid + ) + + if pdi_style == "American" and min_price_at_maturity >= pdi_strike and trigger_autocall == 999: + pv_digit = ( + math.exp(-r * days_to_observation_y_fract[k]) + * (memory_coupon + min(min_price_at_maturity / 100.0, cap)) + + coupon_paid + ) + if pdi_style == "American" and min_price >= pdi_barrier: + pv_digit = ( + math.exp(-r * days_to_observation_y_fract[k]) + * ((2 * capital_value) + memory_coupon - (min_price_at_maturity / 100.0)) + + coupon_paid + ) + if pdi_style == "American" and min_price < pdi_barrier: + pv_digit = ( + max(prot_min_val, min_price_at_maturity / 100.0) + * math.exp(-r * days_to_observation_y_fract[k]) + + coupon_paid + ) + + return pv_digit + + +def _caso_relief( + r: float, + days_to_observation_y_fract: List[float], + pdi_style: Optional[str], + pdi_barrier: float, + capital_value: float, + prot_min_val: float, + min_price: float, + min_price_at_maturity: float, + second_min_price_at_maturity: float, + pv_digit: float, + memory_coupon: float, + coupon_paid: float, + k: int, +) -> float: + if pdi_style == "European" and min_price_at_maturity >= pdi_barrier: + pv_digit = ( + math.exp(-r * days_to_observation_y_fract[k]) * (capital_value + memory_coupon) + + coupon_paid + ) + if pdi_style == "European" and min_price_at_maturity < pdi_barrier: + pv_digit = ( + max(prot_min_val, second_min_price_at_maturity / 100.0) + * math.exp(-r * days_to_observation_y_fract[k]) + + coupon_paid + ) + + if pdi_style == "American" and min_price >= pdi_barrier: + pv_digit = ( + math.exp(-r * days_to_observation_y_fract[k]) * (capital_value + memory_coupon) + + coupon_paid + ) + if pdi_style == "American" and min_price < pdi_barrier: + pv_digit = ( + max(prot_min_val, second_min_price_at_maturity / 100.0) + * math.exp(-r * days_to_observation_y_fract[k]) + + coupon_paid + ) + + return pv_digit + + +def _caso_sigma( + r: float, + days_to_observation_y_fract: List[float], + pdi_style: Optional[str], + pdi_barrier: float, + capital_value: float, + prot_min_val: float, + min_price: float, + min_price_at_maturity: float, + pv_digit: float, + memory_coupon: float, + coupon_paid: float, + k: int, + pdi_strike: float, +) -> float: + if pdi_style == "European" and min_price_at_maturity >= pdi_barrier: + pv_digit = ( + math.exp(-r * days_to_observation_y_fract[k]) * (capital_value + memory_coupon) + + coupon_paid + ) + if pdi_style == "European" and min_price_at_maturity < pdi_barrier: + pv_digit = ( + max(prot_min_val, (min_price_at_maturity + pdi_strike - pdi_barrier) / 100.0) + * math.exp(-r * days_to_observation_y_fract[k]) + + coupon_paid + ) + + if pdi_style == "American" and min_price >= pdi_barrier: + pv_digit = ( + math.exp(-r * days_to_observation_y_fract[k]) * (capital_value + memory_coupon) + + coupon_paid + ) + if pdi_style == "American" and min_price < pdi_barrier: + pv_digit = ( + max(prot_min_val, (min_price_at_maturity + pdi_strike - pdi_barrier) / 100.0) + * math.exp(-r * days_to_observation_y_fract[k]) + + coupon_paid + ) + + return pv_digit + + +def _caso_airbag( + r: float, + days_to_observation_y_fract: List[float], + pdi_style: Optional[str], + pdi_barrier: float, + capital_value: float, + prot_min_val: float, + min_price: float, + min_price_at_maturity: float, + pv_digit: float, + memory_coupon: float, + coupon_paid: float, + k: int, + fattore_airbag: float, +) -> float: + if pdi_style == "European" and min_price_at_maturity >= pdi_barrier: + pv_digit = ( + math.exp(-r * days_to_observation_y_fract[k]) * (capital_value + memory_coupon) + + coupon_paid + ) + if pdi_style == "European" and min_price_at_maturity < pdi_barrier: + pv_digit = ( + max(prot_min_val, min_price_at_maturity / 100.0) + * math.exp(-r * days_to_observation_y_fract[k]) + * fattore_airbag + + coupon_paid + ) + + if pdi_style == "American" and min_price >= pdi_barrier: + pv_digit = ( + math.exp(-r * days_to_observation_y_fract[k]) * (capital_value + memory_coupon) + + coupon_paid + ) + if pdi_style == "American" and min_price < pdi_barrier: + pv_digit = ( + max(prot_min_val, min_price_at_maturity / 100.0) + * math.exp(-r * days_to_observation_y_fract[k]) + * fattore_airbag + + coupon_paid + ) + + return pv_digit + + +def _caso_airbag_one_star( + r: float, + days_to_observation_y_fract: List[float], + pdi_style: Optional[str], + pdi_barrier: float, + capital_value: float, + prot_min_val: float, + min_price: float, + min_price_at_maturity: float, + max_price_at_maturity: float, + pv_digit: float, + memory_coupon: float, + coupon_paid: float, + k: int, + fattore_airbag: float, + trigger_one_star: float, +) -> float: + if max_price_at_maturity >= trigger_one_star: + return ( + math.exp(-r * days_to_observation_y_fract[k]) * (capital_value + memory_coupon) + + coupon_paid + ) + return _caso_airbag( + r, + days_to_observation_y_fract, + pdi_style, + pdi_barrier, + capital_value, + prot_min_val, + min_price, + min_price_at_maturity, + pv_digit, + memory_coupon, + coupon_paid, + k, + fattore_airbag, + ) + + +def _caso_sigma_one_star( + r: float, + days_to_observation_y_fract: List[float], + pdi_style: Optional[str], + pdi_barrier: float, + capital_value: float, + prot_min_val: float, + min_price: float, + min_price_at_maturity: float, + max_price_at_maturity: float, + pv_digit: float, + memory_coupon: float, + coupon_paid: float, + k: int, + pdi_strike: float, + trigger_one_star: float, +) -> float: + if max_price_at_maturity >= trigger_one_star: + return ( + math.exp(-r * days_to_observation_y_fract[k]) * (capital_value + memory_coupon) + + coupon_paid + ) + return _caso_sigma( + r, + days_to_observation_y_fract, + pdi_style, + pdi_barrier, + capital_value, + prot_min_val, + min_price, + min_price_at_maturity, + pv_digit, + memory_coupon, + coupon_paid, + k, + pdi_strike, + ) + + +def _caso_relief_one_star( + r: float, + days_to_observation_y_fract: List[float], + pdi_style: Optional[str], + pdi_barrier: float, + capital_value: float, + prot_min_val: float, + min_price: float, + min_price_at_maturity: float, + second_min_price_at_maturity: float, + max_price_at_maturity: float, + pv_digit: float, + memory_coupon: float, + coupon_paid: float, + k: int, + trigger_one_star: float, +) -> float: + if max_price_at_maturity >= trigger_one_star: + return ( + math.exp(-r * days_to_observation_y_fract[k]) * (capital_value + memory_coupon) + + coupon_paid + ) + return _caso_relief( + r, + days_to_observation_y_fract, + pdi_style, + pdi_barrier, + capital_value, + prot_min_val, + min_price, + min_price_at_maturity, + second_min_price_at_maturity, + pv_digit, + memory_coupon, + coupon_paid, + k, + ) + + +def _caso_twinwin_one_star( + r: float, + days_to_observation_y_fract: List[float], + pdi_style: Optional[str], + pdi_barrier: float, + capital_value: float, + prot_min_val: float, + min_price: float, + min_price_at_maturity: float, + max_price_at_maturity: float, + pv_digit: float, + memory_coupon: float, + coupon_paid: float, + k: int, + pdi_strike: float, + autocall_trigger: float, + cap: float, + trigger_one_star: float, +) -> float: + if max_price_at_maturity >= trigger_one_star: + return ( + math.exp(-r * days_to_observation_y_fract[k]) * (capital_value + memory_coupon) + + coupon_paid + ) + return _caso_twinwin( + r, + days_to_observation_y_fract, + pdi_style, + pdi_barrier, + capital_value, + prot_min_val, + min_price, + min_price_at_maturity, + pv_digit, + memory_coupon, + coupon_paid, + k, + pdi_strike, + autocall_trigger, + cap, + ) + + +def _caso_airbag_twinwin( + r: float, + days_to_observation_y_fract: List[float], + pdi_style: Optional[str], + pdi_barrier: float, + capital_value: float, + prot_min_val: float, + min_price: float, + min_price_at_maturity: float, + pv_digit: float, + memory_coupon: float, + coupon_paid: float, + k: int, + pdi_strike: float, + autocall_trigger: float, + cap: float, + fattore_airbag: float, +) -> float: + if min_price_at_maturity >= pdi_barrier: + if autocall_trigger == 999 and min_price_at_maturity >= pdi_strike: + return ( + math.exp(-r * days_to_observation_y_fract[k]) + * (memory_coupon + min(min_price_at_maturity / 100.0, cap)) + + coupon_paid + ) + return ( + math.exp(-r * days_to_observation_y_fract[k]) + * ((2 * capital_value) + memory_coupon - (min_price_at_maturity / 100.0)) + + coupon_paid + ) + return ( + max(prot_min_val, min_price_at_maturity / 100.0) + * math.exp(-r * days_to_observation_y_fract[k]) + * fattore_airbag + + coupon_paid + ) + + +def _caso_standard( + r: float, + days_to_observation_y_fract: List[float], + pdi_style: Optional[str], + pdi_barrier: float, + capital_value: float, + prot_min_val: float, + min_price: float, + min_price_at_maturity: float, + pv_digit: float, + memory_coupon: float, + coupon_paid: float, + k: int, +) -> float: + if pdi_style == "European" and min_price_at_maturity >= pdi_barrier: + pv_digit = ( + math.exp(-r * days_to_observation_y_fract[k]) * (capital_value + memory_coupon) + + coupon_paid + ) + if pdi_style == "European" and min_price_at_maturity < pdi_barrier: + pv_digit = ( + max(prot_min_val, min_price_at_maturity / 100.0) + * math.exp(-r * days_to_observation_y_fract[k]) + + coupon_paid + ) + + if pdi_style == "American" and min_price >= pdi_barrier: + pv_digit = ( + math.exp(-r * days_to_observation_y_fract[k]) * (capital_value + memory_coupon) + + coupon_paid + ) + if pdi_style == "American" and min_price < pdi_barrier: + pv_digit = ( + max(prot_min_val, min_price_at_maturity / 100.0) + * math.exp(-r * days_to_observation_y_fract[k]) + + coupon_paid + ) + + return pv_digit + +def gbm_multi_equity( + prices_ul: List[float], + cor_mat: List[List[float]], + num_sottostanti: int, + days_to_maturity: int, + tasso_interesse: float, + q_assets: List[float], + vol_assets: List[float], +) -> List[List[float]]: + dt = 1.0 / 252.0 + + mean = np.zeros(num_sottostanti) + cov = np.array(cor_mat, dtype=float) + df_w = np.random.multivariate_normal(mean, cov, size=days_to_maturity) + + prices = np.array(prices_ul, dtype=float) + vol = np.array(vol_assets, dtype=float) + div = np.array(q_assets, dtype=float) + drift = (tasso_interesse - div) - (0.5 * (vol ** 2)) + + increments = drift * dt + vol * math.sqrt(dt) * df_w + cum_sum = np.cumsum(increments, axis=0) + + df_s = np.empty((days_to_maturity + 1, num_sottostanti), dtype=float) + df_s[0] = prices + df_s[1:] = prices * np.exp(cum_sum) + + return df_s + + +def gbm_multi_equity_garch( + prices_ul: List[float], + correl_matrix: List[List[float]], + garch_params: List, + num_sottostanti: int, + days_to_maturity: int, + tasso_interesse: float, + q_assets: List[float], +) -> List[List[float]]: + dt = 1.0 / 252.0 + mean = np.zeros(num_sottostanti) + cov = np.array(correl_matrix, dtype=float) + z = np.random.multivariate_normal(mean, cov, size=days_to_maturity) + + s = [[0.0 for _ in range(num_sottostanti)] for _ in range(days_to_maturity + 1)] + sigma2 = [[0.0 for _ in range(num_sottostanti)] for _ in range(days_to_maturity + 1)] + + for i in range(num_sottostanti): + s[0][i] = prices_ul[i] + sigma2[0][i] = garch_params[i].sigma0 ** 2 + + for t in range(1, days_to_maturity + 1): + for i in range(num_sottostanti): + epsilon = z[t - 1][i] + vol_t = math.sqrt(sigma2[t - 1][i]) + drift = ((tasso_interesse - q_assets[i]) - 0.5 * sigma2[t - 1][i]) * dt + shock = vol_t * math.sqrt(dt) * epsilon + + s[t][i] = s[t - 1][i] * math.exp(drift + shock) + sigma2[t][i] = ( + garch_params[i].omega + + garch_params[i].alpha * (shock ** 2) + + garch_params[i].beta * sigma2[t - 1][i] + ) + + return s + + +def max_array_at_row(jagged_array: List[List[float]], num_sottostanti: int, row_index: int) -> float: + if isinstance(jagged_array, np.ndarray): + return float(jagged_array[row_index].max()) + value = -999999.0 + for j in range(num_sottostanti): + if jagged_array[row_index][j] > value: + value = jagged_array[row_index][j] + return value + + +def min_array_at_row(jagged_array: List[List[float]], num_sottostanti: int, row_index: int) -> float: + if isinstance(jagged_array, np.ndarray): + return float(jagged_array[row_index].min()) + value = 999999.0 + for j in range(num_sottostanti): + if jagged_array[row_index][j] < value: + value = jagged_array[row_index][j] + return value + + +def second_min_array_at_row(jagged_array: List[List[float]], num_sottostanti: int, row_index: int) -> float: + if isinstance(jagged_array, np.ndarray): + row = np.sort(jagged_array[row_index]) + return float(row[1] if row.size >= 2 else row[0]) + temp_array = [jagged_array[row_index][j] for j in range(num_sottostanti)] + temp_array.sort() + if len(temp_array) >= 2: + return temp_array[1] + return temp_array[0] + + +def _min_overall(jagged_array: List[List[float]]) -> float: + if isinstance(jagged_array, np.ndarray): + return float(jagged_array.min()) + return min(min(row) for row in jagged_array) + + +@njit(cache=True) +def _nj_min_row(path, row_index): + return path[row_index].min() + + +@njit(cache=True) +def _nj_max_row(path, row_index): + return path[row_index].max() + + +@njit(cache=True) +def _nj_second_min_row(path, row_index): + row = np.sort(path[row_index]) + if row.shape[0] >= 2: + return row[1] + return row[0] + + +@njit(cache=True) +def _nj_min_overall(path): + return path.min() + + +@njit(cache=True) +def _nj_caso_standard(r, days_to_obs_y_fract, style_is_eur, pdi_barrier, capital_value, prot_min_val, + min_price, min_price_at_maturity, memory_coupon, coupon_paid, k): + pv_digit = 0.0 + if style_is_eur == 1 and min_price_at_maturity >= pdi_barrier: + pv_digit = (math.exp(-r * days_to_obs_y_fract[k]) * (capital_value + memory_coupon)) + coupon_paid + if style_is_eur == 1 and min_price_at_maturity < pdi_barrier: + pv_digit = (max(prot_min_val, min_price_at_maturity / 100.0)) * math.exp(-r * days_to_obs_y_fract[k]) + coupon_paid + + if style_is_eur == 0 and min_price >= pdi_barrier: + pv_digit = (math.exp(-r * days_to_obs_y_fract[k]) * (capital_value + memory_coupon)) + coupon_paid + if style_is_eur == 0 and min_price < pdi_barrier: + pv_digit = (max(prot_min_val, min_price_at_maturity / 100.0)) * math.exp(-r * days_to_obs_y_fract[k]) + coupon_paid + return pv_digit + + +@njit(cache=True) +def _nj_caso_airbag(r, days_to_obs_y_fract, style_is_eur, pdi_barrier, capital_value, prot_min_val, + min_price, min_price_at_maturity, memory_coupon, coupon_paid, k, fattore_airbag): + pv_digit = 0.0 + if style_is_eur == 1 and min_price_at_maturity >= pdi_barrier: + pv_digit = (math.exp(-r * days_to_obs_y_fract[k]) * (capital_value + memory_coupon)) + coupon_paid + if style_is_eur == 1 and min_price_at_maturity < pdi_barrier: + pv_digit = (max(prot_min_val, min_price_at_maturity / 100.0)) * math.exp(-r * days_to_obs_y_fract[k]) * fattore_airbag + coupon_paid + + if style_is_eur == 0 and min_price >= pdi_barrier: + pv_digit = (math.exp(-r * days_to_obs_y_fract[k]) * (capital_value + memory_coupon)) + coupon_paid + if style_is_eur == 0 and min_price < pdi_barrier: + pv_digit = (max(prot_min_val, min_price_at_maturity / 100.0)) * math.exp(-r * days_to_obs_y_fract[k]) * fattore_airbag + coupon_paid + return pv_digit + + +@njit(cache=True) +def _nj_caso_sigma(r, days_to_obs_y_fract, style_is_eur, pdi_barrier, capital_value, prot_min_val, + min_price, min_price_at_maturity, memory_coupon, coupon_paid, k, pdi_strike): + pv_digit = 0.0 + if style_is_eur == 1 and min_price_at_maturity >= pdi_barrier: + pv_digit = (math.exp(-r * days_to_obs_y_fract[k]) * (capital_value + memory_coupon)) + coupon_paid + if style_is_eur == 1 and min_price_at_maturity < pdi_barrier: + pv_digit = (max(prot_min_val, (min_price_at_maturity + pdi_strike - pdi_barrier) / 100.0)) * math.exp(-r * days_to_obs_y_fract[k]) + coupon_paid + + if style_is_eur == 0 and min_price >= pdi_barrier: + pv_digit = (math.exp(-r * days_to_obs_y_fract[k]) * (capital_value + memory_coupon)) + coupon_paid + if style_is_eur == 0 and min_price < pdi_barrier: + pv_digit = (max(prot_min_val, (min_price_at_maturity + pdi_strike - pdi_barrier) / 100.0)) * math.exp(-r * days_to_obs_y_fract[k]) + coupon_paid + return pv_digit + + +@njit(cache=True) +def _nj_caso_relief(r, days_to_obs_y_fract, style_is_eur, pdi_barrier, capital_value, prot_min_val, + min_price, min_price_at_maturity, second_min_price_at_maturity, memory_coupon, coupon_paid, k): + pv_digit = 0.0 + if style_is_eur == 1 and min_price_at_maturity >= pdi_barrier: + pv_digit = (math.exp(-r * days_to_obs_y_fract[k]) * (capital_value + memory_coupon)) + coupon_paid + if style_is_eur == 1 and min_price_at_maturity < pdi_barrier: + pv_digit = (max(prot_min_val, second_min_price_at_maturity / 100.0)) * math.exp(-r * days_to_obs_y_fract[k]) + coupon_paid + + if style_is_eur == 0 and min_price >= pdi_barrier: + pv_digit = (math.exp(-r * days_to_obs_y_fract[k]) * (capital_value + memory_coupon)) + coupon_paid + if style_is_eur == 0 and min_price < pdi_barrier: + pv_digit = (max(prot_min_val, second_min_price_at_maturity / 100.0)) * math.exp(-r * days_to_obs_y_fract[k]) + coupon_paid + return pv_digit + + +@njit(cache=True) +def _nj_caso_twinwin(r, days_to_obs_y_fract, style_is_eur, pdi_barrier, capital_value, prot_min_val, + min_price, min_price_at_maturity, memory_coupon, coupon_paid, k, pdi_strike, trigger_autocall, cap): + pv_digit = 0.0 + if style_is_eur == 1 and min_price_at_maturity >= pdi_strike and trigger_autocall == 999: + pv_digit = (math.exp(-r * days_to_obs_y_fract[k]) * (memory_coupon + (min(min_price_at_maturity / 100.0, cap)))) + coupon_paid + if style_is_eur == 1 and min_price_at_maturity >= pdi_barrier: + pv_digit = (math.exp(-r * days_to_obs_y_fract[k]) * ((2 * capital_value) + memory_coupon - (min_price_at_maturity / 100.0))) + coupon_paid + if style_is_eur == 1 and min_price_at_maturity < pdi_barrier: + pv_digit = (max(prot_min_val, min_price_at_maturity / 100.0)) * math.exp(-r * days_to_obs_y_fract[k]) + coupon_paid + + if style_is_eur == 0 and min_price_at_maturity >= pdi_strike and trigger_autocall == 999: + pv_digit = (math.exp(-r * days_to_obs_y_fract[k]) * (memory_coupon + (min(min_price_at_maturity / 100.0, cap)))) + coupon_paid + if style_is_eur == 0 and min_price >= pdi_barrier: + pv_digit = (math.exp(-r * days_to_obs_y_fract[k]) * ((2 * capital_value) + memory_coupon - (min_price_at_maturity / 100.0))) + coupon_paid + if style_is_eur == 0 and min_price < pdi_barrier: + pv_digit = (max(prot_min_val, min_price_at_maturity / 100.0)) * math.exp(-r * days_to_obs_y_fract[k]) + coupon_paid + return pv_digit + + +@njit(cache=True) +def _nj_caso_one_star(r, days_to_obs_y_fract, pdi_barrier, capital_value, prot_min_val, + min_price_at_maturity, max_price_at_maturity, memory_coupon, coupon_paid, k, trigger_one_star, + coupon_triggers, coupon_values): + if min_price_at_maturity < pdi_barrier: + if max_price_at_maturity < trigger_one_star: + payout_capitale = max(prot_min_val, min_price_at_maturity / 100.0) + else: + payout_capitale = capital_value + else: + payout_capitale = capital_value + + payout_cedola = 0.0 + if min_price_at_maturity >= coupon_triggers[k]: + payout_cedola = memory_coupon + coupon_values[k] + + return payout_capitale * math.exp(-r * days_to_obs_y_fract[k]) + payout_cedola * math.exp(-r * days_to_obs_y_fract[k]) + coupon_paid + + +@njit(cache=True) +def _nj_caso_airbag_one_star(r, days_to_obs_y_fract, pdi_barrier, capital_value, prot_min_val, + min_price, min_price_at_maturity, max_price_at_maturity, memory_coupon, coupon_paid, + k, fattore_airbag, trigger_one_star, style_is_eur): + if max_price_at_maturity >= trigger_one_star: + return (math.exp(-r * days_to_obs_y_fract[k]) * (capital_value + memory_coupon)) + coupon_paid + return _nj_caso_airbag(r, days_to_obs_y_fract, style_is_eur, pdi_barrier, capital_value, prot_min_val, + min_price, min_price_at_maturity, memory_coupon, coupon_paid, k, fattore_airbag) + + +@njit(cache=True) +def _nj_caso_sigma_one_star(r, days_to_obs_y_fract, pdi_barrier, capital_value, prot_min_val, + min_price, min_price_at_maturity, max_price_at_maturity, memory_coupon, coupon_paid, + k, pdi_strike, trigger_one_star, style_is_eur): + if max_price_at_maturity >= trigger_one_star: + return (math.exp(-r * days_to_obs_y_fract[k]) * (capital_value + memory_coupon)) + coupon_paid + return _nj_caso_sigma(r, days_to_obs_y_fract, style_is_eur, pdi_barrier, capital_value, prot_min_val, + min_price, min_price_at_maturity, memory_coupon, coupon_paid, k, pdi_strike) + + +@njit(cache=True) +def _nj_caso_relief_one_star(r, days_to_obs_y_fract, pdi_barrier, capital_value, prot_min_val, + min_price, min_price_at_maturity, second_min_price_at_maturity, max_price_at_maturity, + memory_coupon, coupon_paid, k, trigger_one_star, style_is_eur): + if max_price_at_maturity >= trigger_one_star: + return (math.exp(-r * days_to_obs_y_fract[k]) * (capital_value + memory_coupon)) + coupon_paid + return _nj_caso_relief(r, days_to_obs_y_fract, style_is_eur, pdi_barrier, capital_value, prot_min_val, + min_price, min_price_at_maturity, second_min_price_at_maturity, memory_coupon, coupon_paid, k) + + +@njit(cache=True) +def _nj_caso_twinwin_one_star(r, days_to_obs_y_fract, pdi_barrier, capital_value, prot_min_val, + min_price, min_price_at_maturity, max_price_at_maturity, memory_coupon, coupon_paid, + k, pdi_strike, autocall_trigger, cap, trigger_one_star, style_is_eur): + if max_price_at_maturity >= trigger_one_star: + return (math.exp(-r * days_to_obs_y_fract[k]) * (capital_value + memory_coupon)) + coupon_paid + return _nj_caso_twinwin(r, days_to_obs_y_fract, style_is_eur, pdi_barrier, capital_value, prot_min_val, + min_price, min_price_at_maturity, memory_coupon, coupon_paid, k, pdi_strike, autocall_trigger, cap) + + +@njit(cache=True) +def _nj_caso_airbag_twinwin(r, days_to_obs_y_fract, pdi_barrier, capital_value, prot_min_val, + min_price_at_maturity, memory_coupon, coupon_paid, k, pdi_strike, autocall_trigger, + cap, fattore_airbag): + if min_price_at_maturity >= pdi_barrier: + if autocall_trigger == 999 and min_price_at_maturity >= pdi_strike: + return (math.exp(-r * days_to_obs_y_fract[k]) * (memory_coupon + min(min_price_at_maturity / 100.0, cap))) + coupon_paid + return (math.exp(-r * days_to_obs_y_fract[k]) * ((2 * capital_value) + memory_coupon - (min_price_at_maturity / 100.0))) + coupon_paid + return (max(prot_min_val, min_price_at_maturity / 100.0)) * math.exp(-r * days_to_obs_y_fract[k]) * fattore_airbag + coupon_paid + + +@njit(cache=True) +def _nj_payoff_for_path(path, days_to_obs, days_to_obs_y_fract, coupon_values, coupon_triggers, + autocall_values, autocall_triggers, memory_flags, coupon_in_memory, style_is_eur, + pdi_strike, pdi_barrier, capital_value, prot_min_val, airbag, sigma, twinwin, relief, + fattore_airbag, one_star, trigger_one_star, cap, days_to_maturity, r): + min_price = _nj_min_overall(path) + min_price_at_maturity = _nj_min_row(path, days_to_maturity) + + pv_digit = 0.0 + memory_coupon = coupon_in_memory + coupon_paid = 0.0 + + n_obs = days_to_obs.shape[0] + for k in range(n_obs): + min_by_row = _nj_min_row(path, days_to_obs[k]) + if min_by_row >= autocall_triggers[k]: + pv_digit = (autocall_values[k] + memory_coupon) * math.exp(-r * days_to_obs_y_fract[k]) + coupon_paid + return pv_digit + + if min_by_row >= coupon_triggers[k]: + coupon_paid += (coupon_values[k] + memory_coupon) * math.exp(-r * days_to_obs_y_fract[k]) + memory_coupon = 0.0 + else: + memory_coupon += memory_flags[k] * coupon_values[k] + + if k == n_obs - 1: + if airbag == 0 and sigma == 0 and relief == 0 and twinwin == 0 and one_star == 0: + return _nj_caso_standard(r, days_to_obs_y_fract, style_is_eur, pdi_barrier, + capital_value, prot_min_val, min_price, min_price_at_maturity, + memory_coupon, coupon_paid, k) + if airbag == 1 and sigma == 0 and relief == 0 and twinwin == 0 and one_star == 0: + return _nj_caso_airbag(r, days_to_obs_y_fract, style_is_eur, pdi_barrier, + capital_value, prot_min_val, min_price, min_price_at_maturity, + memory_coupon, coupon_paid, k, fattore_airbag) + if airbag == 0 and sigma == 1 and relief == 0 and twinwin == 0 and one_star == 0: + return _nj_caso_sigma(r, days_to_obs_y_fract, style_is_eur, pdi_barrier, + capital_value, prot_min_val, min_price, min_price_at_maturity, + memory_coupon, coupon_paid, k, pdi_strike) + if airbag == 0 and sigma == 0 and relief == 1 and twinwin == 0 and one_star == 0: + second_min = _nj_second_min_row(path, days_to_maturity) + return _nj_caso_relief(r, days_to_obs_y_fract, style_is_eur, pdi_barrier, + capital_value, prot_min_val, min_price, min_price_at_maturity, + second_min, memory_coupon, coupon_paid, k) + if airbag == 0 and sigma == 0 and relief == 0 and twinwin == 1 and one_star == 0: + return _nj_caso_twinwin(r, days_to_obs_y_fract, style_is_eur, pdi_barrier, + capital_value, prot_min_val, min_price, min_price_at_maturity, + memory_coupon, coupon_paid, k, pdi_strike, autocall_triggers[k], cap) + if airbag == 0 and sigma == 0 and relief == 0 and twinwin == 0 and one_star == 1: + max_price_at_maturity = _nj_max_row(path, days_to_obs[k]) + return _nj_caso_one_star(r, days_to_obs_y_fract, pdi_barrier, capital_value, prot_min_val, + min_price_at_maturity, max_price_at_maturity, memory_coupon, coupon_paid, k, + trigger_one_star, coupon_triggers, coupon_values) + if airbag == 1 and sigma == 0 and relief == 0 and twinwin == 0 and one_star == 1: + max_price_at_maturity = _nj_max_row(path, days_to_obs[k]) + return _nj_caso_airbag_one_star(r, days_to_obs_y_fract, pdi_barrier, capital_value, prot_min_val, + min_price, min_price_at_maturity, max_price_at_maturity, + memory_coupon, coupon_paid, k, fattore_airbag, trigger_one_star, + style_is_eur) + if airbag == 0 and sigma == 1 and relief == 0 and twinwin == 0 and one_star == 1: + max_price_at_maturity = _nj_max_row(path, days_to_obs[k]) + return _nj_caso_sigma_one_star(r, days_to_obs_y_fract, pdi_barrier, capital_value, prot_min_val, + min_price, min_price_at_maturity, max_price_at_maturity, + memory_coupon, coupon_paid, k, pdi_strike, trigger_one_star, + style_is_eur) + if airbag == 0 and sigma == 0 and relief == 1 and twinwin == 0 and one_star == 1: + second_min = _nj_second_min_row(path, days_to_maturity) + max_price_at_maturity = _nj_max_row(path, days_to_obs[k]) + return _nj_caso_relief_one_star(r, days_to_obs_y_fract, pdi_barrier, capital_value, prot_min_val, + min_price, min_price_at_maturity, second_min, max_price_at_maturity, + memory_coupon, coupon_paid, k, trigger_one_star, style_is_eur) + if airbag == 0 and sigma == 0 and relief == 0 and twinwin == 1 and one_star == 1: + max_price_at_maturity = _nj_max_row(path, days_to_obs[k]) + return _nj_caso_twinwin_one_star(r, days_to_obs_y_fract, pdi_barrier, capital_value, prot_min_val, + min_price, min_price_at_maturity, max_price_at_maturity, + memory_coupon, coupon_paid, k, pdi_strike, autocall_triggers[k], + cap, trigger_one_star, style_is_eur) + if airbag == 1 and sigma == 0 and relief == 0 and twinwin == 1 and one_star == 0: + return _nj_caso_airbag_twinwin(r, days_to_obs_y_fract, pdi_barrier, capital_value, prot_min_val, + min_price_at_maturity, memory_coupon, coupon_paid, k, pdi_strike, + autocall_triggers[k], cap, fattore_airbag) + return pv_digit + + +@njit(cache=True) +def _nj_payoffs_for_paths(paths, days_to_obs, days_to_obs_y_fract, coupon_values, coupon_triggers, + autocall_values, autocall_triggers, memory_flags, coupon_in_memory, style_is_eur, + pdi_strike, pdi_barrier, capital_value, prot_min_val, airbag, sigma, twinwin, relief, + fattore_airbag, one_star, trigger_one_star, cap, days_to_maturity, r): + n_sims = paths.shape[0] + out = np.empty(n_sims, dtype=np.float64) + for i in range(n_sims): + out[i] = _nj_payoff_for_path(paths[i], days_to_obs, days_to_obs_y_fract, coupon_values, coupon_triggers, + autocall_values, autocall_triggers, memory_flags, coupon_in_memory, style_is_eur, + pdi_strike, pdi_barrier, capital_value, prot_min_val, airbag, sigma, twinwin, relief, + fattore_airbag, one_star, trigger_one_star, cap, days_to_maturity, r) + return out + + +def _generate_paths_batch(prices_ul, cor_mat, num_sims, days_to_maturity, tasso_interesse, dividends, volatility): + num_assets = prices_ul.shape[0] + dt = 1.0 / 252.0 + sqrt_dt = math.sqrt(dt) + chol = np.linalg.cholesky(cor_mat) + + z = np.random.standard_normal((num_sims, days_to_maturity, num_assets)) + w = np.matmul(z, chol.T) + + drift = (tasso_interesse - dividends) - (0.5 * (volatility ** 2)) + increments = drift[None, None, :] * dt + volatility[None, None, :] * sqrt_dt * w + cum_sum = np.cumsum(increments, axis=1) + + paths = np.empty((num_sims, days_to_maturity + 1, num_assets), dtype=np.float64) + paths[:, 0, :] = prices_ul + paths[:, 1:, :] = prices_ul[None, None, :] * np.exp(cum_sum) + return paths + + +def _simulate_batch(args: Tuple): + (num_sims, prices_ul, cor_mat, days_to_maturity, tasso_interesse, dividends, volatility, days_to_obs, + days_to_obs_y_fract, coupon_values, coupon_triggers, autocall_values, autocall_triggers, memory_flags, + coupon_in_memory, style_is_eur, pdi_strike, pdi_barrier, capital_value, prot_min_val, airbag, sigma, + twinwin, relief, fattore_airbag, one_star, trigger_one_star, cap) = args + + paths = _generate_paths_batch(prices_ul, cor_mat, num_sims, days_to_maturity, tasso_interesse, dividends, volatility) + return _nj_payoffs_for_paths( + paths, + days_to_obs, + days_to_obs_y_fract, + coupon_values, + coupon_triggers, + autocall_values, + autocall_triggers, + memory_flags, + coupon_in_memory, + style_is_eur, + pdi_strike, + pdi_barrier, + capital_value, + prot_min_val, + airbag, + sigma, + twinwin, + relief, + fattore_airbag, + one_star, + trigger_one_star, + cap, + days_to_maturity, + tasso_interesse, + ) + + +def simulate_payoffs( + num_sims, + prices_ul, + cor_mat, + days_to_maturity, + tasso_interesse, + dividends, + volatility, + days_to_obs, + days_to_obs_y_fract, + coupon_values, + coupon_triggers, + autocall_values, + autocall_triggers, + memory_flags, + coupon_in_memory, + pdi_style, + pdi_strike, + pdi_barrier, + capital_value, + prot_min_val, + airbag, + sigma, + twinwin, + relief, + fattore_airbag, + one_star, + trigger_one_star, + cap, +): + if cap is None: + cap = 0.0 + if trigger_one_star is None: + trigger_one_star = 0.0 + if pdi_strike is None: + pdi_strike = 0.0 + if pdi_barrier is None: + pdi_barrier = 0.0 + if capital_value is None: + capital_value = 0.0 + if prot_min_val is None: + prot_min_val = 0.0 + if fattore_airbag is None: + fattore_airbag = 0.0 + if coupon_in_memory is None: + coupon_in_memory = 0.0 + + prices_ul = np.asarray(prices_ul, dtype=np.float64) + cor_mat = np.asarray(cor_mat, dtype=np.float64) + dividends = np.asarray(dividends, dtype=np.float64) + volatility = np.asarray(volatility, dtype=np.float64) + days_to_obs = np.asarray(days_to_obs, dtype=np.int64) + days_to_obs_y_fract = np.asarray(days_to_obs_y_fract, dtype=np.float64) + coupon_values = np.asarray(coupon_values, dtype=np.float64) + coupon_triggers = np.asarray(coupon_triggers, dtype=np.float64) + autocall_values = np.asarray(autocall_values, dtype=np.float64) + autocall_triggers = np.asarray(autocall_triggers, dtype=np.float64) + memory_flags = np.asarray(memory_flags, dtype=np.int64) + + style_is_eur = 1 if pdi_style == "European" else 0 + + cpu_count = os.cpu_count() or 1 + use_parallel = cpu_count > 1 and num_sims >= 20000 + + if not _USE_NUMBA: + use_parallel = False + + if use_parallel: + workers = max(1, cpu_count - 1) + base_batch = max(256, num_sims // (workers * 4)) + batch_size = min(4096, base_batch) + + batches = [] + remaining = num_sims + while remaining > 0: + take = batch_size if remaining >= batch_size else remaining + remaining -= take + batches.append(take) + + args = [ + ( + b, + prices_ul, + cor_mat, + days_to_maturity, + tasso_interesse, + dividends, + volatility, + days_to_obs, + days_to_obs_y_fract, + coupon_values, + coupon_triggers, + autocall_values, + autocall_triggers, + memory_flags, + coupon_in_memory, + style_is_eur, + pdi_strike, + pdi_barrier, + capital_value, + prot_min_val, + airbag, + sigma, + twinwin, + relief, + fattore_airbag, + one_star, + trigger_one_star, + cap, + ) + for b in batches + ] + + results = [] + with ProcessPoolExecutor(max_workers=workers) as executor: + for res in executor.map(_simulate_batch, args): + results.append(res) + return np.concatenate(results) + + batch_size = 4096 + results = [] + remaining = num_sims + while remaining > 0: + take = batch_size if remaining >= batch_size else remaining + remaining -= take + res = _simulate_batch( + ( + take, + prices_ul, + cor_mat, + days_to_maturity, + tasso_interesse, + dividends, + volatility, + days_to_obs, + days_to_obs_y_fract, + coupon_values, + coupon_triggers, + autocall_values, + autocall_triggers, + memory_flags, + coupon_in_memory, + style_is_eur, + pdi_strike, + pdi_barrier, + capital_value, + prot_min_val, + airbag, + sigma, + twinwin, + relief, + fattore_airbag, + one_star, + trigger_one_star, + cap, + ) + ) + results.append(res) + return np.concatenate(results) + +def volatility_certificates(isin: str, num_prezzi_eod: int, enable_warning: bool) -> List[float]: + global _cached_eod_underlyings + + if _cached_eod_underlyings: + list_prezzi = _cached_eod_underlyings + if enable_warning: + num_ul = len(list_prezzi) + tot_prezzi = sum(len(p.prezzi_close) for p in list_prezzi) + if num_ul * num_prezzi_eod > tot_prezzi: + print(f"Volatilita calcolata usando {tot_prezzi // num_ul} prezzi eod") + else: + fs_db = default_sql_query() + hist_price = fs_db.get_hist_price_ul(isin, num_prezzi_eod) + details_ul = fs_db.get_details_ul(isin) + num_ul = len(details_ul) + + if enable_warning and num_ul * num_prezzi_eod != len(hist_price): + print("Attenzione! Rilevati meno di prezzi eod in uno dei sottostanti") + print(f"Volatilita calcolata usando {len(hist_price) // max(num_ul, 1)} prezzi eod") + input("Premere un tasto per continuare...") + + list_prezzi = [] + for dettaglio in details_ul: + prezzi = [r.px_close for r in hist_price if r.id_underlyings == dettaglio.id_underlyings] + list_prezzi.append( + PrezziSottostanti( + id_underlyings=dettaglio.id_underlyings, + sottostante=dettaglio.sottostante, + prezzi_close=prezzi, + ) + ) + + _cached_eod_underlyings = list_prezzi + + stats = compute_underlying_stats(list_prezzi) + return [s.volatility for s in stats] + + +def correl_certificates(isin: str, num_prezzi_eod: int, enable_warning: bool) -> List[List[float]]: + global _cached_eod_underlyings + + fs_db = default_sql_query() + hist_price = fs_db.get_hist_price_ul(isin, num_prezzi_eod) + details_ul = fs_db.get_details_ul(isin) + + num_ul = len(details_ul) + + if enable_warning and num_ul * num_prezzi_eod != len(hist_price): + print("Attenzione! Rilevati meno di prezzi eod in uno dei sottostanti") + print(f"Matrice di correlazione calcolata usando {len(hist_price) // max(num_ul, 1)} prezzi eod") + input("Premere un tasto per continuare...") + + list_prezzi = [] + for dettaglio in details_ul: + arr_prezzi = [ + r.px_close + for r in hist_price + if r.id_underlyings == dettaglio.id_underlyings + ] + list_prezzi.append( + PrezziSottostanti( + id_underlyings=dettaglio.id_underlyings, + sottostante=dettaglio.sottostante, + prezzi_close=arr_prezzi, + ) + ) + + _cached_eod_underlyings = list_prezzi + + correl_matrix = [[0.0 for _ in range(num_ul)] for _ in range(num_ul)] + + for i in range(num_ul): + correl_matrix[i][i] = 1.0 + for j in range(i + 1, num_ul): + a = list_prezzi[i].prezzi_close + b = list_prezzi[j].prezzi_close + if not a or not b: + corr_val = 0.0 + else: + corr_val = float(np.corrcoef(a, b)[0, 1]) + correl_matrix[i][j] = corr_val + + for k in range(num_ul): + if correl_matrix[i][k] == 0.0: + correl_matrix[i][k] = correl_matrix[k][i] + + return correl_matrix + + +def array_to_table(numbers: List[List[float]], column_names: List[str]) -> List[List[float]]: + rows = [] + for i in range(len(numbers)): + row = [round(numbers[i][j], 4) for j in range(len(numbers[i]))] + rows.append(row) + return rows + + +def array_jagged_to_table(numbers: List[List[float]], column_names: List[str]) -> List[List[float]]: + rows = [] + for i in range(len(numbers)): + row = [round(numbers[i][j], 4) for j in range(len(column_names))] + rows.append(row) + return rows diff --git a/pricer/cli.py b/pricer/cli.py new file mode 100644 index 0000000..135a553 --- /dev/null +++ b/pricer/cli.py @@ -0,0 +1,342 @@ +import sys +import time +from typing import List + +from tabulate import tabulate +from . import calc +from .comparison import clone_context, compare_fair_values +from .db import default_sql_query +from .payoffs import PayoffContext + + +def _prompt_with_default(prompt: str, default: str) -> str: + value = input(prompt) + return default if not value else value + + +def _print_table(title: str, headers: List[str], rows: List[List[object]]) -> None: + print() + print(title) + print(tabulate(rows, headers=headers, tablefmt="github")) + + +def get_caso_string(airbag: int, sigma: int, relief: int, twinwin: int, onestar: int) -> str: + componenti = [] + if airbag == 1: + componenti.append("Airbag") + if sigma == 1: + componenti.append("Sigma") + if relief == 1: + componenti.append("Relief") + if twinwin == 1: + componenti.append("TwinWin") + if onestar == 1: + componenti.append("OneStar") + + return " + ".join(componenti) if componenti else "Standard" + + +def restart_or_exit(): + print() + print("Premere Invio per finire o R per riavviare il programma...") + restart = input() + if restart and restart.upper() == "R": + main() + else: + sys.exit(0) + + +def main(): + version = "2.2 - [12/06/2025]" + print(f"Pricer v.{version}") + + fs_db = default_sql_query() + + isin = _prompt_with_default("Inserire ISIN (default IT0006767633): ", "IT0006767633") + + check_isin = fs_db.check_isin(isin) + if check_isin is not None: + check_value = check_isin[0] if check_isin else None + if check_value == "KO": + print(f"ISIN {isin} non processabile!") + print("Certificati nel pricer devono avere le seguenti caratteristiche : ") + print("- Categoria: Coupon,StepUp,Bonus (con Barriera Discreta)") + print("- Status: In Quotazione") + print("- Direction: Long") + print("- Basket Type : Worst") + print("- Currency : EUR") + print("- CedolaVariabile : FALSE") + print("- Darwin : FALSE") + print("- Domino : FALSE") + print("- Magnet : FALSE") + restart_or_exit() + + prezzi_eod = _prompt_with_default( + "Num. Prezzi EOD per calcolo matrice correlazione e volatilita (default 252): ", "252" + ) + num_prezzi_eod = int(prezzi_eod) + + corr_matrix = calc.correl_certificates(isin, num_prezzi_eod, True) + volatility = calc.volatility_certificates(isin, num_prezzi_eod, True) + + is_dividend = _prompt_with_default("Considerare i dividendi ? s/n (default s): ", "s") + + num_sims = int(_prompt_with_default("Inserire # simulazioni montecarlo (default = 10000) : ", "10000")) + t_montecarlo_choice = time.perf_counter() + + details_ul_model = fs_db.get_details_ul(isin) + + nomi_sottostanti = [r.sottostante for r in details_ul_model] + corr_rows = calc.array_to_table(corr_matrix, nomi_sottostanti) + _print_table(f"Matrix Correl {isin}", nomi_sottostanti, corr_rows) + + details_ctf_model = fs_db.get_details_ctf(isin) + details_event_model = fs_db.get_details_event(isin) + details_event_exdate_model = fs_db.get_details_event_exdate(isin) + + ctf = details_ctf_model[0] + tasso_interesse = ctf.tasso_interesse + days_to_maturity = ctf.days_to_maturity + pdi_style = ctf.pdi_style + pdi_strike = ctf.pdi_strike + pdi_barrier = round(ctf.pdi_barrier, 4) + capital_value = ctf.capital_value + nominal_amount = ctf.nominal_amount + bid = ctf.bid + ask = ctf.ask + last_date_price = ctf.last_date_price + coupon_in_memory = ctf.coupon_in_memory + prot_min_val = ctf.prot_min_val + airbag = ctf.air_bag + fattore_airbag = ctf.fattore_airbag + one_star = ctf.one_star + trigger_onestar = round(ctf.trigger_onestar, 4) + twin_win = ctf.twin_win + sigma = ctf.sigma + relief = ctf.relief + domino = ctf.domino + category = ctf.category + cap = ctf.cap + leva = ctf.leva + + prices_ul = [r.spot_price_normalized for r in details_ul_model] + dividends = [r.dividend for r in details_ul_model] + if is_dividend != "s": + dividends = [0.0 for _ in dividends] + num_sottostanti = len(details_ul_model) + + days_to_obs = [r.days_to_obs for r in details_event_model] + days_to_ex_date = [r.days_to_ex_date for r in details_event_exdate_model] + days_to_obs_y_fract = [r.days_to_obs_y_fract for r in details_event_model] + coupon_values = [r.coupon_value for r in details_event_model] + coupon_triggers = [r.coupon_trigger for r in details_event_model] + autocall_values = [r.autocall_value for r in details_event_model] + autocall_triggers = [r.autocall_trigger for r in details_event_model] + memory_flags = [r.memory for r in details_event_model] + num_eventi = len(details_event_model) + + input_ul_rows = [] + for i in range(num_sottostanti): + input_ul_rows.append( + [ + nomi_sottostanti[i], + round(prices_ul[i], 4), + round(dividends[i], 4), + round(volatility[i], 4), + ] + ) + _print_table( + f"Dati Input Sottostanti {isin}", + ["Sottostante", "Price", "Dividend", "Volatility"], + input_ul_rows, + ) + + input_ctf_rows = [ + [ + round(tasso_interesse, 4), + days_to_maturity, + pdi_style, + pdi_barrier, + round(capital_value * nominal_amount, 5), + round(coupon_in_memory, 5), + round(prot_min_val, 3), + ] + ] + _print_table( + f"Dati Input Certificato {isin}", + [ + "TassoInteresse", + "DaysToMaturity", + "PDI Style", + "PDI Barrier", + "Capital Value", + "Coupon In Memoria", + "Prot Min Val %", + ], + input_ctf_rows, + ) + + input_flag_rows = [ + [ + airbag, + round(fattore_airbag, 3), + one_star, + trigger_onestar, + twin_win, + sigma, + relief, + ] + ] + _print_table( + f"Dati Input Flags {isin}", + [ + "AirBag", + "Fattore Airbag", + "OneStar", + "TriggerOnestar", + "TwinWin", + "Sigma", + "Relief", + ], + input_flag_rows, + ) + + input_event_rows = [] + for i in range(num_eventi): + input_event_rows.append( + [ + days_to_obs[i], + round(days_to_obs_y_fract[i], 4), + round(coupon_values[i] * nominal_amount, 6), + round(coupon_triggers[i], 6), + round(autocall_values[i] * nominal_amount, 6), + round(autocall_triggers[i], 6), + memory_flags[i], + ] + ) + _print_table( + f"Dati Input Eventi {isin}", + [ + "DaysToObs.", + "DaysToObsYFract", + "Cpn. Value", + "Cpn. Trigger", + "Autoc. Value", + "Autoc. Trigger", + "Memory", + ], + input_event_rows, + ) + + print("Elaborazione Fair value in corso...") + + context_original = PayoffContext( + days_to_maturity=days_to_maturity, + r=tasso_interesse, + capital_value=capital_value, + prot_min_val=prot_min_val, + pdi_barrier=pdi_barrier, + pdi_style=pdi_style, + coupon_in_memory=coupon_in_memory, + days_to_obs_y_fract=days_to_obs_y_fract, + coupon_triggers=coupon_triggers, + coupon_values=coupon_values, + autocall_triggers=autocall_triggers, + autocall_values=autocall_values, + memory_flags=memory_flags, + caso=get_caso_string(airbag, sigma, relief, twin_win, one_star), + pdi_strike=pdi_strike, + fattore_airbag=fattore_airbag, + trigger_one_star=trigger_onestar, + cap=cap, + days_to_obs=days_to_obs, + airbag=airbag, + sigma=sigma, + relief=relief, + twin_win=twin_win, + one_star=one_star, + leva=leva, + volatility=volatility, + prices_ul=prices_ul, + nominal_amount=nominal_amount, + ) + + context_for_compare = clone_context(context_original) + + fvalues = calc.fair_value_array( + prices_ul, + corr_matrix, + num_sottostanti, + num_sims, + tasso_interesse, + days_to_maturity, + dividends, + volatility, + days_to_obs, + days_to_obs_y_fract, + coupon_values, + coupon_triggers, + autocall_values, + autocall_triggers, + memory_flags, + coupon_in_memory, + pdi_style, + pdi_strike, + pdi_barrier, + capital_value, + prot_min_val, + airbag, + sigma, + twin_win, + relief, + fattore_airbag, + one_star, + trigger_onestar, + cap, + leva, + ).fair_value_array + + fv = [int(value * nominal_amount / 100) for value in fvalues] + + t_engine_choice = time.perf_counter() + print(f"Tempo fino alla scelta del motore: {t_engine_choice - t_montecarlo_choice:.2f} sec") + + compare_fair_values( + num_sims, + prices_ul, + corr_matrix, + num_sottostanti, + days_to_maturity, + tasso_interesse, + dividends, + context_for_compare, + nominal_amount, + isin, + ask, + bid, + last_date_price, + category, + ) + + t_expected_done = time.perf_counter() + print(f"Tempo fino al valore atteso: {t_expected_done - t_engine_choice:.2f} sec") + + print("----------------------------------------") + + if len(days_to_ex_date) > len(days_to_obs): + last_obs = days_to_obs[-1] + last_ex = days_to_ex_date[-1] + + if last_obs < last_ex: + print("ATTENZIONE: POSSIBILE SOTTOSTIMA DEL FAIR VALUE") + print("\n Il calcolo del fair value presume che il coupon sia gia stato pagato,") + print(" ma in realta non e ancora maturato (la Ex-Date non e ancora stata raggiunta).") + print(" Il titolo incorpora ancora quel coupon nel suo prezzo, ma il pricer lo ha gia scontato.") + print("\n Inoltre, se erano presenti coupon in memoria, questi sono stati azzerati nel calcolo,") + print(" come se il pagamento fosse gia avvenuto.") + print(" In realta il diritto al pagamento non e ancora stato acquisito.") + print(" Questo comporta una POSSIBILE SOTTOSTIMA SIGNIFICATIVA del FAIR VALUE.") + print("\n SUGGERIMENTO: verificare manualmente l'ultimo evento osservato") + print(" e valutare l'impatto potenziale sul fair value atteso.") + + restart_or_exit() diff --git a/pricer/comparison.py b/pricer/comparison.py new file mode 100644 index 0000000..cf35ca0 --- /dev/null +++ b/pricer/comparison.py @@ -0,0 +1,107 @@ +from typing import List + +from . import calc +from .payoffs import PayoffContext +from .report import generate_report + + +def clone_context(original: PayoffContext) -> PayoffContext: + return PayoffContext( + days_to_maturity=original.days_to_maturity, + r=original.r, + capital_value=original.capital_value, + prot_min_val=original.prot_min_val, + pdi_barrier=original.pdi_barrier, + pdi_style=original.pdi_style, + coupon_in_memory=original.coupon_in_memory, + days_to_obs_y_fract=list(original.days_to_obs_y_fract), + coupon_triggers=list(original.coupon_triggers), + coupon_values=list(original.coupon_values), + autocall_triggers=list(original.autocall_triggers), + autocall_values=list(original.autocall_values), + memory_flags=list(original.memory_flags), + caso=original.caso, + pdi_strike=original.pdi_strike, + fattore_airbag=original.fattore_airbag, + trigger_one_star=original.trigger_one_star, + cap=original.cap, + days_to_obs=list(original.days_to_obs), + airbag=original.airbag, + sigma=original.sigma, + relief=original.relief, + twin_win=original.twin_win, + one_star=original.one_star, + leva=original.leva, + volatility=list(original.volatility), + prices_ul=list(original.prices_ul), + nominal_amount=original.nominal_amount, + ) + + +def compare_fair_values( + num_simulations: int, + prices_ul: List[float], + corr_matrix: List[List[float]], + num_assets: int, + days_to_maturity: int, + rate: float, + dividends: List[float], + context: PayoffContext, + nominal_amount: float, + isin: str, + ask: float, + bid: float, + last_date, + category: str, +) -> None: + print(f"Motore GBM con payoff engine '{context.caso}'") + + results = calc.simulate_payoffs( + num_simulations, + prices_ul, + corr_matrix, + days_to_maturity, + rate, + dividends, + context.volatility, + context.days_to_obs, + context.days_to_obs_y_fract, + context.coupon_values, + context.coupon_triggers, + context.autocall_values, + context.autocall_triggers, + context.memory_flags, + context.coupon_in_memory, + context.pdi_style, + context.pdi_strike, + context.pdi_barrier, + context.capital_value, + context.prot_min_val, + context.airbag, + context.sigma, + context.twin_win, + context.relief, + context.fattore_airbag, + context.one_star, + context.trigger_one_star, + context.cap, + ) + results = [float(val) * 100.0 for val in results] + + mean = sum(results) / num_simulations + variance = sum((val - mean) ** 2 for val in results) / num_simulations + stddev = variance ** 0.5 + + fair_value_scaled = mean * nominal_amount / 100.0 + fair_values = [int(val * nominal_amount / 100) for val in results] + + generate_report( + isin=isin, + category=category, + fair_value=fair_value_scaled, + fair_values=fair_values, + ask=ask, + bid=bid, + num_sims=num_simulations, + last_date_price=last_date, + ) diff --git a/pricer/config.py b/pricer/config.py new file mode 100644 index 0000000..9a85e4c --- /dev/null +++ b/pricer/config.py @@ -0,0 +1,17 @@ +import json +from pathlib import Path + + +def get_connection_string(name="FirstSolutionDB", settings_path=None): + """Load connection string from appsettings.json in the parent project.""" + if settings_path is None: + local_settings = Path(__file__).resolve().parents[1] / "appsettings.json" + settings_path = local_settings if local_settings.exists() else Path(__file__).resolve().parents[2] / "appsettings.json" + else: + settings_path = Path(settings_path) + + data = json.loads(settings_path.read_text(encoding="utf-8")) + conn_strings = data.get("ConnectionStrings", {}) + if name not in conn_strings: + raise KeyError(f"Connection string '{name}' not found in {settings_path}") + return conn_strings[name] diff --git a/pricer/db.py b/pricer/db.py new file mode 100644 index 0000000..71ceb94 --- /dev/null +++ b/pricer/db.py @@ -0,0 +1,202 @@ +from typing import Any, Dict, Iterable, List, Tuple +import pyodbc +from decimal import Decimal + +from .config import get_connection_string +from . import models + + +def _rows_to_dicts(cursor, rows): + columns = [col[0] for col in cursor.description] + return [dict(zip(columns, row)) for row in rows] + + +def _normalize_keys(row: Dict[str, Any]) -> Dict[str, Any]: + normalized = {} + for k, v in row.items(): + if isinstance(v, Decimal): + normalized[k.lower()] = float(v) + else: + normalized[k.lower()] = v + return normalized + + +class SqlDataAccess: + def load_data(self, stored_proc: str, params: Dict[str, Any], connection_string: str): + placeholders = ", ".join([f"@{k}=?" for k in params.keys()]) + sql = f"EXEC {stored_proc} {placeholders}" if placeholders else f"EXEC {stored_proc}" + values = list(params.values()) + + with pyodbc.connect(connection_string) as conn: + cur = conn.cursor() + cur.execute(sql, values) + rows = cur.fetchall() + return _rows_to_dicts(cur, rows) + + +class SqlQuery: + def __init__(self, connection_string: str): + self._connection_string = connection_string + self._db = SqlDataAccess() + + def get_hist_price_ul(self, isin: str, num_prezzi_eod: int): + rows = self._db.load_data( + "pricer_ULPrices1", + {"Isin": isin, "numPrezzi": num_prezzi_eod}, + self._connection_string, + ) + return [ + models.HistPriceULModel( + id_underlyings=r["idunderlyings"], + sottostante=r["sottostante"], + px_close=r["px_close"], + px_date=r["px_date"], + ) + for r in map(_normalize_keys, rows) + ] + + def get_details_ul(self, isin: str): + rows = self._db.load_data( + "pricer_ULDetails", + {"Isin": isin}, + self._connection_string, + ) + return [ + models.DetailsULModel( + id_underlyings=r["idunderlyings"], + sottostante=r["sottostante"], + maturity_date=r.get("maturitydate"), + last_price=r["lastprice"], + strike=r["strike"], + spot_price_normalized=r["spotpricenormalized"], + dividend=r["dividend"], + volatility=r["volatility"], + days_to_maturity=r.get("daystomaturity", 0), + tasso_interesse=r.get("tassointeresse", 0.0), + ) + for r in map(_normalize_keys, rows) + ] + + def get_details_event(self, isin: str): + rows = self._db.load_data( + "pricer_EventDetails", + {"Isin": isin}, + self._connection_string, + ) + return [ + models.DetailsEventModel( + days_to_obs=r["daystoobs"], + days_to_ex_date=r.get("daystoexdate", 0), + days_to_obs_y_fract=r["daystoobsyfract"], + coupon_value=r["couponvalue"], + coupon_trigger=r["coupontrigger"], + autocall_value=r["autocallvalue"], + autocall_trigger=r["autocalltrigger"], + memory=r["memory"], + ) + for r in map(_normalize_keys, rows) + ] + + def get_details_event_exdate(self, isin: str): + rows = self._db.load_data( + "pricer_EventDetails_ExDate", + {"Isin": isin}, + self._connection_string, + ) + return [ + models.DetailsEventModel( + days_to_obs=r.get("daystoobs", 0), + days_to_ex_date=r["daystoexdate"], + days_to_obs_y_fract=r.get("daystoobsyfract", 0.0), + coupon_value=r.get("couponvalue", 0.0), + coupon_trigger=r.get("coupontrigger", 0.0), + autocall_value=r.get("autocallvalue", 0.0), + autocall_trigger=r.get("autocalltrigger", 0.0), + memory=r.get("memory", 0), + ) + for r in map(_normalize_keys, rows) + ] + + def get_details_ctf(self, isin: str): + rows = self._db.load_data( + "pricer_CTFDetails", + {"Isin": isin}, + self._connection_string, + ) + return [ + models.DetailsCTFModel( + tasso_interesse=r["tassointeresse"], + days_to_maturity=r["daystomaturity"], + pdi_style=r["pdi_style"], + pdi_strike=r["pdi_strike"], + pdi_barrier=r["pdi_barrier"], + capital_value=r["capitalvalue"], + nominal_amount=r["nominalamount"], + bid=r["bid"], + ask=r["ask"], + last_date_price=r["lastdateprice"], + coupon_in_memory=r["couponinmemory"], + prot_min_val=r["protminval"], + air_bag=r["airbag"], + fattore_airbag=r["fattoreairbag"], + one_star=r["onestar"], + trigger_onestar=r["triggeronestar"], + twin_win=r["twinwin"], + sigma=r["sigma"], + relief=r["relief"], + domino=r["domino"], + category=r["category"], + cap=r["cap"], + leva=r["leva"], + ) + for r in map(_normalize_keys, rows) + ] + + def get_isin_universe(self): + rows = self._db.load_data( + "pricer_LoadISINUniverse", + {}, + self._connection_string, + ) + values = [] + for row in rows: + if isinstance(row, dict): + if len(row) == 1: + values.append(next(iter(row.values()))) + else: + values.append(row) + else: + values.append(row) + return values + + def check_isin(self, isin: str): + rows = self._db.load_data( + "pricer_CheckISIN1", + {"Isin": isin}, + self._connection_string, + ) + values = [] + for row in rows: + if isinstance(row, dict): + if len(row) == 1: + values.append(next(iter(row.values()))) + else: + values.append(row) + else: + values.append(row) + return values + + def bulk_update_certificates(self, table_name: str, list_fair_values: List[models.FairValues]) -> None: + if not list_fair_values: + return + + sql = f"UPDATE {table_name} SET FairValue = ? WHERE ISIN = ?" + data = [(fv.fair_value, fv.isin) for fv in list_fair_values] + with pyodbc.connect(self._connection_string) as conn: + cur = conn.cursor() + cur.executemany(sql, data) + conn.commit() + + +def default_sql_query(): + return SqlQuery(get_connection_string("FirstSolutionDB")) diff --git a/pricer/models.py b/pricer/models.py new file mode 100644 index 0000000..1855b65 --- /dev/null +++ b/pricer/models.py @@ -0,0 +1,93 @@ +from dataclasses import dataclass +from datetime import datetime +from typing import List, Optional + + +@dataclass +class HistPriceULModel: + id_underlyings: int + sottostante: str + px_close: float + px_date: datetime + + +@dataclass +class DetailsULModel: + id_underlyings: int + sottostante: str + maturity_date: Optional[datetime] + last_price: float + strike: float + spot_price_normalized: float + dividend: float + volatility: float + days_to_maturity: int + tasso_interesse: float + + +@dataclass +class DetailsEventModel: + days_to_obs: int + days_to_ex_date: int + days_to_obs_y_fract: float + coupon_value: float + coupon_trigger: float + autocall_value: float + autocall_trigger: float + memory: int + + +@dataclass +class DetailsCTFModel: + tasso_interesse: float + days_to_maturity: int + pdi_style: str + pdi_strike: float + pdi_barrier: float + capital_value: float + nominal_amount: float + bid: float + ask: float + last_date_price: datetime + coupon_in_memory: float + prot_min_val: float + air_bag: int + fattore_airbag: float + one_star: int + trigger_onestar: float + twin_win: int + sigma: int + relief: int + domino: int + category: str + cap: float + leva: float + + +@dataclass +class FairValues: + isin: str + fair_value: float + + +@dataclass +class GarchParams: + omega: float + alpha: float + beta: float + sigma0: float + + +@dataclass +class UnderlyingStats: + nome: str + prezzi: List[float] + log_returns: List[float] + volatility: float + + +@dataclass +class PrezziSottostanti: + id_underlyings: int + sottostante: str + prezzi_close: List[float] diff --git a/pricer/payoffs.py b/pricer/payoffs.py new file mode 100644 index 0000000..7365f6c --- /dev/null +++ b/pricer/payoffs.py @@ -0,0 +1,560 @@ +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 + diff --git a/pricer/report.py b/pricer/report.py new file mode 100644 index 0000000..a24494e --- /dev/null +++ b/pricer/report.py @@ -0,0 +1,71 @@ +from typing import List + +from .utils import round_to_multiple + + +def generate_report( + isin: str, + category: str, + fair_value: float, + fair_values: List[int], + ask: float, + bid: float, + num_sims: int, + last_date_price, +) -> None: + rif_prezzo = ask + riferimento = "Ask" + + print() + + if ask == 0 and bid > 0: + rif_prezzo = bid + riferimento = "Bid" + print(f"Ask = 0, preso come valore di riferimento il Bid: {bid:.2f}") + + casi_over_barriera = [] + casi_under_barriera = [] + casi_over_ask = [] + casi_under_ask = [] + + for value in fair_values: + if value < 100: + casi_under_barriera.append(value) + else: + casi_over_barriera.append(value) + + if value >= rif_prezzo: + casi_over_ask.append(value) + else: + casi_under_ask.append(value) + + perc_over_barriera = round_to_multiple(100 * len(casi_over_barriera) / len(fair_values), 0.01) + perc_over_ask = round_to_multiple(100 * len(casi_over_ask) / len(fair_values), 0.01) + perc_under_ask = 100 - perc_over_ask + + print(f"Caso calcolo Fair value {isin} = {category}") + print( + f"Fair value {isin} ({num_sims} iter.) = {fair_value:.2f} (Bid = {bid}, Ask = {ask}, Last updated on {last_date_price})" + ) + print( + f"Sopra {riferimento} nel {perc_over_ask:.2f}% dei casi, sotto {riferimento} nel {perc_under_ask:.2f}% dei casi" + ) + + media_over_ask = round_to_multiple(sum(casi_over_ask) / len(casi_over_ask) if casi_over_ask else 0.0, 0.01) + media_under_ask = round_to_multiple(sum(casi_under_ask) / len(casi_under_ask) if casi_under_ask else 0.0, 0.01) + print(f"Valore medio casi sopra {riferimento}: {media_over_ask:.2f}") + print(f"Valore medio casi sotto {riferimento}: {media_under_ask:.2f}") + + gain_medio = round_to_multiple(media_over_ask - rif_prezzo, 0.01) + loss_medio = round_to_multiple(rif_prezzo - media_under_ask, 0.01) + print(f"Gain medio: {media_over_ask:.2f} - {rif_prezzo:.2f} = {gain_medio:.2f}") + print(f"Loss medio: {rif_prezzo:.2f} - {media_under_ask:.2f} = {loss_medio:.2f}") + + perc_media_gain = round_to_multiple(100 * gain_medio / rif_prezzo, 0.01) + perc_media_loss = round_to_multiple(100 * loss_medio / rif_prezzo, 0.01) + print(f"Perc media Gain: {gain_medio:.2f} / {rif_prezzo:.2f} = {perc_media_gain:.2f}%") + print(f"Perc media Loss: {loss_medio:.2f} / {rif_prezzo:.2f} = {perc_media_loss:.2f}%") + + valore_atteso = round_to_multiple((perc_over_ask / 100) * gain_medio - (perc_under_ask / 100) * loss_medio, 0.01) + perc_valore_atteso = round_to_multiple(100 * valore_atteso / rif_prezzo, 0.01) + print(f"Valore atteso: {valore_atteso:.2f} ({perc_valore_atteso:.2f}% su {riferimento})") diff --git a/pricer/utils.py b/pricer/utils.py new file mode 100644 index 0000000..91642ed --- /dev/null +++ b/pricer/utils.py @@ -0,0 +1,28 @@ +import math +from typing import Iterable, List + + +def round_to_multiple(value: float, multiple: float) -> float: + if multiple == 0: + return value + return round(value / multiple) * multiple + + +def cumulative_sums(values: List[float]) -> List[float]: + if not values: + return [] + results = [0.0] * len(values) + results[0] = values[0] + for i in range(1, len(values)): + results[i] = results[i - 1] + values[i] + return results + + +def standard_deviation(values: Iterable[float]) -> float: + vals = list(values) + n = len(vals) + if n <= 1: + return 0.0 + avg = sum(vals) / n + sum_sq = sum((v - avg) ** 2 for v in vals) + return math.sqrt(sum_sq / (n - 1)) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..b50c68a --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +numpy +pyodbc +tabulate +numba