primo commit

This commit is contained in:
fredmaloggia
2026-01-05 19:15:03 +01:00
commit f707b9c95b
61 changed files with 2928 additions and 0 deletions

1
pricer/__init__.py Normal file
View File

@@ -0,0 +1 @@
"""Pricer package."""

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

1445
pricer/calc.py Normal file

File diff suppressed because it is too large Load Diff

342
pricer/cli.py Normal file
View File

@@ -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()

107
pricer/comparison.py Normal file
View File

@@ -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,
)

17
pricer/config.py Normal file
View File

@@ -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]

202
pricer/db.py Normal file
View File

@@ -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"))

93
pricer/models.py Normal file
View File

@@ -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]

560
pricer/payoffs.py Normal file
View File

@@ -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

71
pricer/report.py Normal file
View File

@@ -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})")

28
pricer/utils.py Normal file
View File

@@ -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))