rilascio versione stabile

This commit is contained in:
fredmaloggia
2025-11-26 13:58:58 +01:00
parent 906484cb11
commit 419c6b6ac0
6 changed files with 195 additions and 2270 deletions

View File

@@ -13,6 +13,8 @@ import os
import numpy as np import numpy as np
import pandas as pd import pandas as pd
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
import yaml
import logging
from sqlalchemy import create_engine, text from sqlalchemy import create_engine, text
from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.exc import SQLAlchemyError
@@ -25,10 +27,13 @@ from pypfopt.exceptions import OptimizationError
OUTPUT_DIR = "Output" OUTPUT_DIR = "Output"
PLOT_DIR = "Plot" PLOT_DIR = "Plot"
INPUT_DIR = "Input" INPUT_DIR = "Input"
CONFIG_FILE = "config.yaml"
os.makedirs(OUTPUT_DIR, exist_ok=True) os.makedirs(OUTPUT_DIR, exist_ok=True)
os.makedirs(PLOT_DIR, exist_ok=True) os.makedirs(PLOT_DIR, exist_ok=True)
os.makedirs(INPUT_DIR, exist_ok=True) os.makedirs(INPUT_DIR, exist_ok=True)
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
logger = logging.getLogger(__name__)
def excel_path(filename: str) -> str: def excel_path(filename: str) -> str:
"""Costruisce il percorso completo per un file Excel nella cartella di output.""" """Costruisce il percorso completo per un file Excel nella cartella di output."""
@@ -38,6 +43,87 @@ def plot_path(filename: str) -> str:
"""Costruisce il percorso completo per un file PNG nella cartella Plot.""" """Costruisce il percorso completo per un file PNG nella cartella Plot."""
return os.path.join(PLOT_DIR, filename) return os.path.join(PLOT_DIR, filename)
DEFAULT_VOL_TARGETS = [
{"years": 5, "target_vol": 0.06, "name": "VAR3_5Y"},
{"years": 1, "target_vol": 0.12, "name": "VAR6_1Y"},
{"years": 3, "target_vol": 0.12, "name": "VAR6_3Y"},
{"years": 5, "target_vol": 0.12, "name": "VAR6_5Y"},
{"years": 5, "target_vol": 0.18, "name": "VAR9_5Y"},
]
DEFAULT_ASSET_CLASS_LIMITS = {
'Azionari': 0.75, 'Obbligazionari': 0.75,
'Metalli Preziosi': 0.20, 'Materie Prime': 0.05,
'Immobiliare': 0.05, 'Criptovalute': 0.05, 'Monetari': 0.1
}
def load_targets_and_limits(config_file: str):
"""Legge target di volatilità e limiti asset class dal file di configurazione."""
try:
with open(config_file, "r", encoding="utf-8") as f:
cfg = yaml.safe_load(f) or {}
except FileNotFoundError:
logger.error("File di configurazione mancante: %s", config_file)
sys.exit(1)
vt_cfg = cfg.get("volatility_targets", {})
vt_list = []
if isinstance(vt_cfg, dict):
vt_list = vt_cfg.get("default") or []
elif isinstance(vt_cfg, list):
vt_list = vt_cfg
if not vt_list:
logger.error("Sezione 'volatility_targets' mancante o vuota nel file di configurazione.")
sys.exit(1)
volatility_targets_local = {
(int(item["years"]), float(item["target_vol"])): item["name"]
for item in vt_list
if "years" in item and "target_vol" in item and "name" in item
}
if not volatility_targets_local:
logger.error("Nessun target di volatilita valido trovato in configurazione.")
sys.exit(1)
asset_limits_cfg = cfg.get("asset_class_limits") or {}
if not asset_limits_cfg:
logger.error("Sezione 'asset_class_limits' mancante o vuota nel file di configurazione.")
sys.exit(1)
asset_class_limits_local = {k: float(v) for k, v in asset_limits_cfg.items()}
return volatility_targets_local, asset_class_limits_local
def validate_universe(df_universe: pd.DataFrame):
"""Verifica colonne obbligatorie e duplicati ISIN nel file universo."""
required_cols = ['ISIN', 'Nome', 'Categoria', 'Asset Class']
missing_cols = [c for c in required_cols if c not in df_universe.columns]
if missing_cols:
logger.error("Colonne mancanti nel file universo: %s", ", ".join(missing_cols))
sys.exit(1)
dup_isin = df_universe['ISIN'][df_universe['ISIN'].duplicated()].unique().tolist()
if dup_isin:
logger.warning("ISIN duplicati nel file universo: %s", dup_isin)
empty_isin = df_universe['ISIN'].isna().sum()
if empty_isin:
logger.warning("Righe con ISIN mancante nel file universo: %d", int(empty_isin))
def validate_returns_frame(df_returns: pd.DataFrame, threshold: float = 0.2):
"""Avvisa se i rendimenti hanno molte celle NaN prima del riempimento."""
if df_returns.empty:
logger.error("Nessun dato di rendimento recuperato: final_df vuoto.")
sys.exit(1)
na_ratio = df_returns.isna().mean()
high_na = na_ratio[na_ratio > threshold]
if not high_na.empty:
logger.warning(
"Colonne con >%.0f%% di NaN prima del fill: %s",
threshold * 100,
", ".join([f"{c} ({v:.0%})" for c, v in high_na.items()])
)
# --------------------------------- # ---------------------------------
# Utility per R^2 sullequity line # Utility per R^2 sullequity line
# --------------------------------- # ---------------------------------
@@ -291,6 +377,7 @@ df = pd.read_excel(
usecols=['ISIN', 'Nome', 'Categoria', 'Asset Class', 'PesoMax', 'PesoFisso', 'Codice Titolo'], usecols=['ISIN', 'Nome', 'Categoria', 'Asset Class', 'PesoMax', 'PesoFisso', 'Codice Titolo'],
dtype={'Codice Titolo': str} dtype={'Codice Titolo': str}
) )
validate_universe(df)
# ========================= # =========================
# SERIE STORICHE RENDIMENTI # SERIE STORICHE RENDIMENTI
@@ -320,6 +407,7 @@ for isin in df['ISIN'].unique():
except SQLAlchemyError as e: except SQLAlchemyError as e:
print(f"Errore durante l'esecuzione della stored procedure per {isin}:", e) print(f"Errore durante l'esecuzione della stored procedure per {isin}:", e)
validate_returns_frame(final_df)
final_df.fillna(0, inplace=True) final_df.fillna(0, inplace=True)
# -------- H_min sempre su 5 anni (21 gg = 1 mese) -------- # -------- H_min sempre su 5 anni (21 gg = 1 mese) --------
@@ -328,17 +416,7 @@ five_year_df = final_df.loc[end_date - pd.DateOffset(years=5): end_date]
# ========================= # =========================
# CONFIGURAZIONE OBIETTIVI # CONFIGURAZIONE OBIETTIVI
# ========================= # =========================
volatility_targets = { volatility_targets, asset_class_limits = load_targets_and_limits(CONFIG_FILE)
# (1, 0.06): 'VAR3_1Y',
# (3, 0.06): 'VAR3_3Y',
(5, 0.06): 'VAR3_5Y',
(1, 0.12): 'VAR6_1Y',
(3, 0.12): 'VAR6_3Y',
(5, 0.12): 'VAR6_5Y',
# (1, 0.18): 'VAR9_1Y',
# (3, 0.18): 'VAR9_3Y',
(5, 0.18): 'VAR9_5Y'
}
days_per_year = 252 days_per_year = 252
riskfree_rate = 0.02 riskfree_rate = 0.02
@@ -432,11 +510,6 @@ for (years, target_vol), name in volatility_targets.items():
ef.add_constraint(lambda w, idxs=idxs, maxw=maxw: sum(w[i] for i in idxs) <= maxw) ef.add_constraint(lambda w, idxs=idxs, maxw=maxw: sum(w[i] for i in idxs) <= maxw)
# Vincoli per Asset Class # Vincoli per Asset Class
asset_class_limits = {
'Azionari': 0.75, 'Obbligazionari': 0.75,
'Metalli Preziosi': 0.20, 'Materie Prime': 0.05,
'Immobiliare': 0.05, 'Criptovalute': 0.05, 'Monetari': 0.1
}
for ac, maxw in asset_class_limits.items(): for ac, maxw in asset_class_limits.items():
isin_list = df[df['Asset Class'] == ac]['ISIN'].tolist() isin_list = df[df['Asset Class'] == ac]['ISIN'].tolist()
idxs = [period_df.columns.get_loc(isin) for isin in isin_list if isin in period_df.columns] idxs = [period_df.columns.get_loc(isin) for isin in isin_list if isin in period_df.columns]

View File

@@ -25,23 +25,29 @@ def plot_path(filename: str) -> str:
"""Percorso completo per i file di grafico.""" """Percorso completo per i file di grafico."""
return os.path.join(PLOT_DIR, filename) return os.path.join(PLOT_DIR, filename)
# Configurazione della connessione al database # Configurazione della connessione al database (da connection.txt)
username = 'readonly' params = {}
password = 'e8nqtSa39L4Le3' with open("connection.txt", "r") as f:
host = '26.69.45.60' for line in f:
database = 'FirstSolutionDB' line = line.strip()
port = 1433 if line and not line.startswith('#'):
connection_string = f"mssql+pyodbc://{username}:{password}@{host}:{port}/{database}?driver=ODBC+Driver+17+for+SQL+Server" key, value = line.split('=', 1)
params[key.strip()] = value.strip()
username = params.get('username')
password = params.get('password')
host = params.get('host')
port = params.get('port', '1433')
database = params.get('database')
connection_string = (
f"mssql+pyodbc://{username}:{password}@{host}:{port}/{database}?driver=ODBC+Driver+17+for+SQL+Server"
)
try: try:
# Crea l'Engine
engine = create_engine(connection_string) engine = create_engine(connection_string)
# Usa il contesto con la connessione e il metodo text() per eseguire la query
with engine.connect() as connection: with engine.connect() as connection:
result = connection.execute(text("SELECT 1")) _ = connection.execute(text("SELECT 1"))
print("Connessione al database riuscita.") print("Connessione al database riuscita.")
except SQLAlchemyError as e: except SQLAlchemyError as e:
print("Errore durante la connessione al database:", e) print("Errore durante la connessione al database:", e)
sys.exit() sys.exit()
@@ -98,6 +104,7 @@ riskfree_rate = 0.02
# Ottimizzazione per ciascun target di volatilità e salvataggio dei risultati # Ottimizzazione per ciascun target di volatilità e salvataggio dei risultati
optimized_weights = pd.DataFrame() optimized_weights = pd.DataFrame()
summary_data = []
for (years, target_vol), name in volatility_targets.items(): for (years, target_vol), name in volatility_targets.items():
period_start_date = end_date - pd.DateOffset(years=years) period_start_date = end_date - pd.DateOffset(years=years)
period_df = final_df.loc[period_start_date:end_date] period_df = final_df.loc[period_start_date:end_date]
@@ -135,7 +142,15 @@ for (years, target_vol), name in volatility_targets.items():
ef.efficient_risk(target_volatility=target_vol) ef.efficient_risk(target_volatility=target_vol)
weights = ef.clean_weights() weights = ef.clean_weights()
optimized_weights[name] = pd.Series(weights) optimized_weights[name] = pd.Series(weights)
ef.portfolio_performance(verbose=True, risk_free_rate=riskfree_rate) exp_ret, exp_vol, sharpe = ef.portfolio_performance(verbose=True, risk_free_rate=riskfree_rate)
summary_data.append({
"Portafoglio": name,
"Years": years,
"Target Vol": f"{target_vol:.2%}",
"Expected annual return": f"{exp_ret:.2%}",
"Annual volatility": f"{exp_vol:.2%}",
"Sharpe Ratio": f"{sharpe:.2f}",
})
# Creazione del DataFrame per i risultati # Creazione del DataFrame per i risultati
results = [] results = []
@@ -181,10 +196,12 @@ for (years, target_vol), name in volatility_targets.items():
optimized_weights_with_names = optimized_weights.copy() optimized_weights_with_names = optimized_weights.copy()
optimized_weights_with_names['Nome ETF'] = [df.loc[df['ISIN'] == isin, 'Nome'].values[0] for isin in optimized_weights.index] optimized_weights_with_names['Nome ETF'] = [df.loc[df['ISIN'] == isin, 'Nome'].values[0] for isin in optimized_weights.index]
# Esportazione dei pesi ottimizzati in un file Excel con il nome dell'ETF summary_df = pd.DataFrame(summary_data)
output_path = excel_path("optimized_weights_GBP.xlsx") output_path = excel_path("optimized_weights_GBP.xlsx")
optimized_weights_with_names.to_excel(output_path) with pd.ExcelWriter(output_path, engine='openpyxl', mode='w') as writer:
optimized_weights_with_names.to_excel(writer, sheet_name='Pesi Ottimizzati', index=True)
summary_df.to_excel(writer, sheet_name='metriche', index=False)
print(f"All optimized weights saved to '{output_path}'.") print(f"All optimized weights saved to '{output_path}' (with metriche).")

View File

@@ -11,6 +11,8 @@ import os
import numpy as np import numpy as np
import pandas as pd import pandas as pd
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
import yaml
import logging
from sqlalchemy import create_engine, text from sqlalchemy import create_engine, text
from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.exc import SQLAlchemyError
@@ -23,10 +25,13 @@ from pypfopt.exceptions import OptimizationError
OUTPUT_DIR = "Output" OUTPUT_DIR = "Output"
INPUT_DIR = "Input" INPUT_DIR = "Input"
PLOT_DIR = "Plot" PLOT_DIR = "Plot"
CONFIG_FILE = "config.yaml"
os.makedirs(OUTPUT_DIR, exist_ok=True) os.makedirs(OUTPUT_DIR, exist_ok=True)
os.makedirs(INPUT_DIR, exist_ok=True) os.makedirs(INPUT_DIR, exist_ok=True)
os.makedirs(PLOT_DIR, exist_ok=True) os.makedirs(PLOT_DIR, exist_ok=True)
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
logger = logging.getLogger(__name__)
def excel_path(filename: str) -> str: def excel_path(filename: str) -> str:
"""Costruisce il percorso completo per un file Excel nella cartella di output.""" """Costruisce il percorso completo per un file Excel nella cartella di output."""
@@ -36,6 +41,69 @@ def plot_path(filename: str) -> str:
"""Costruisce il percorso completo per un file PNG nella cartella Plot.""" """Costruisce il percorso completo per un file PNG nella cartella Plot."""
return os.path.join(PLOT_DIR, filename) return os.path.join(PLOT_DIR, filename)
def load_targets_and_limits(config_file: str):
"""Legge target di volatilità e limiti asset class dal file di configurazione (nessun fallback)."""
try:
with open(config_file, "r", encoding="utf-8") as f:
cfg = yaml.safe_load(f) or {}
except FileNotFoundError:
logger.error("File di configurazione mancante: %s", config_file)
sys.exit(1)
vt_cfg = cfg.get("volatility_targets", {})
vt_list = []
if isinstance(vt_cfg, dict):
vt_list = vt_cfg.get("default") or []
elif isinstance(vt_cfg, list):
vt_list = vt_cfg
if not vt_list:
logger.error("Sezione 'volatility_targets' mancante o vuota nel file di configurazione.")
sys.exit(1)
volatility_targets_local = {
(int(item["years"]), float(item["target_vol"])): item["name"]
for item in vt_list
if "years" in item and "target_vol" in item and "name" in item
}
if not volatility_targets_local:
logger.error("Nessun target di volatilita valido trovato in configurazione.")
sys.exit(1)
asset_limits_cfg = cfg.get("asset_class_limits") or {}
if not asset_limits_cfg:
logger.error("Sezione 'asset_class_limits' mancante o vuota nel file di configurazione.")
sys.exit(1)
asset_class_limits_local = {k: float(v) for k, v in asset_limits_cfg.items()}
return volatility_targets_local, asset_class_limits_local
def validate_universe(df_universe: pd.DataFrame):
"""Verifica colonne obbligatorie e duplicati ISIN nel file universo."""
required_cols = ['ISIN', 'Nome', 'Categoria', 'Asset Class']
missing_cols = [c for c in required_cols if c not in df_universe.columns]
if missing_cols:
print(f"[warn] Colonne mancanti nel file universo: {', '.join(missing_cols)}")
dup_isin = df_universe['ISIN'][df_universe['ISIN'].duplicated()].unique().tolist()
if dup_isin:
print(f"[warn] ISIN duplicati nel file universo: {dup_isin}")
empty_isin = df_universe['ISIN'].isna().sum()
if empty_isin:
print(f"[warn] Righe con ISIN mancante nel file universo: {int(empty_isin)}")
def validate_returns_frame(df_returns: pd.DataFrame, threshold: float = 0.2):
"""Avvisa se i rendimenti hanno molte celle NaN prima del riempimento."""
if df_returns.empty:
print("[errore] Nessun dato di rendimento recuperato: final_df vuoto.")
sys.exit(1)
na_ratio = df_returns.isna().mean()
high_na = na_ratio[na_ratio > threshold]
if not high_na.empty:
cols = ", ".join([f"{c} ({v:.0%})" for c, v in high_na.items()])
print(f"[warn] Colonne con >{threshold:.0%} di NaN prima del fill: {cols}")
# --------------------------------- # ---------------------------------
# Utility per R^2 sullequity line # Utility per R^2 sullequity line
# --------------------------------- # ---------------------------------
@@ -233,6 +301,7 @@ df = pd.read_excel(
usecols=['ISIN', 'Nome', 'Categoria', 'Asset Class', 'PesoMax', 'PesoFisso', 'Codice Titolo'], usecols=['ISIN', 'Nome', 'Categoria', 'Asset Class', 'PesoMax', 'PesoFisso', 'Codice Titolo'],
dtype={'Codice Titolo': str} dtype={'Codice Titolo': str}
) )
validate_universe(df)
# ========================= # =========================
# SERIE STORICHE RENDIMENTI # SERIE STORICHE RENDIMENTI
@@ -262,6 +331,7 @@ for isin in df['ISIN'].unique():
except SQLAlchemyError as e: except SQLAlchemyError as e:
print(f"Errore durante l'esecuzione della stored procedure per {isin}:", e) print(f"Errore durante l'esecuzione della stored procedure per {isin}:", e)
validate_returns_frame(final_df)
final_df.fillna(0, inplace=True) final_df.fillna(0, inplace=True)
# -------- H_min sempre su 5 anni (21 gg = 1 mese) -------- # -------- H_min sempre su 5 anni (21 gg = 1 mese) --------
@@ -270,17 +340,7 @@ five_year_df = final_df.loc[end_date - pd.DateOffset(years=5): end_date]
# ========================= # =========================
# CONFIGURAZIONE OBIETTIVI # CONFIGURAZIONE OBIETTIVI
# ========================= # =========================
volatility_targets = { volatility_targets, asset_class_limits = load_targets_and_limits(CONFIG_FILE)
# (1, 0.06): 'VAR3_1Y',
# (3, 0.06): 'VAR3_3Y',
(5, 0.06): 'VAR3_5Y',
(1, 0.12): 'VAR6_1Y',
(3, 0.12): 'VAR6_3Y',
(5, 0.12): 'VAR6_5Y',
# (1, 0.18): 'VAR9_1Y',
# (3, 0.18): 'VAR9_3Y',
(5, 0.18): 'VAR9_5Y'
}
days_per_year = 252 days_per_year = 252
riskfree_rate = 0.02 riskfree_rate = 0.02
@@ -374,11 +434,6 @@ for (years, target_vol), name in volatility_targets.items():
ef.add_constraint(lambda w, idxs=idxs, maxw=maxw: sum(w[i] for i in idxs) <= maxw) ef.add_constraint(lambda w, idxs=idxs, maxw=maxw: sum(w[i] for i in idxs) <= maxw)
# Vincoli per Asset Class # Vincoli per Asset Class
asset_class_limits = {
'Azionari': 0.75, 'Obbligazionari': 0.75,
'Metalli Preziosi': 0.20, 'Materie Prime': 0.05,
'Immobiliare': 0.05, 'Criptovalute': 0.05, 'Monetari': 0.1
}
for ac, maxw in asset_class_limits.items(): for ac, maxw in asset_class_limits.items():
isin_list = df[df['Asset Class'] == ac]['ISIN'].tolist() isin_list = df[df['Asset Class'] == ac]['ISIN'].tolist()
idxs = [period_df.columns.get_loc(isin) for isin in isin_list if isin in period_df.columns] idxs = [period_df.columns.get_loc(isin) for isin in isin_list if isin in period_df.columns]
@@ -429,7 +484,7 @@ for (years, target_vol), name in volatility_targets.items():
print(f"File {output_file_path} saved successfully.") print(f"File {output_file_path} saved successfully.")
# --- Pie chart asset allocation: salva in Output senza mostrare --- # --- Pie chart asset allocation: salva in Output senza mostrare ---
asset_allocations = {asset: 0 for asset in ['Azionari', 'Obbligazionari', 'Metalli Preziosi', 'Materie Prime', 'Immobiliare', 'Criptovalute', 'Monetari']} asset_allocations = {asset: 0 for asset in asset_class_limits}
for isin, weight in weights.items(): for isin, weight in weights.items():
r_sel = df.loc[df['ISIN'] == isin] r_sel = df.loc[df['ISIN'] == isin]
if r_sel.empty: if r_sel.empty:

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,39 +0,0 @@
# Traccia improvements Ottimizzatore
## Struttura e stack
- Repository piatto con script versionati (2.6, 2.5.2, Lite 1.0, 2.2 UK); nessun package/moduli, logica procedurale.
- Stack: Python, pandas, numpy, matplotlib; SQLAlchemy/pyodbc (MSSQL); PyPortfolioOpt; opzionale cvxpy; Excel I/O via pandas; nessun requirements/lockfile.
- IO: Excel input (universo titoli, template), output Excel pesi/metriche in `Output/`, grafici PNG in `Plot/`; credenziali DB in `connection.txt`.
## Flusso v2.6 (principale)
- Setup cartelle e target volatilita' (`volatility_targets`), costanti (`days_per_year`, `riskfree_rate`, `mu_heal_floor`).
- Utility metriche: R2 equity, drawdown (profondita', durata, TTR), Heal index (AAW/AUW), H_min_100, serie/metriche portafoglio.
- Connessione MSSQL via `connection.txt`, stored proc `opt_RendimentoGiornaliero1_ALL` per ciascun ISIN; missing riempiti con 0.
- Per target: metriche per-asset (ann return/vol, CAGR, R2, drawdown, Heal, H_min_100 su 5Y); ottimizzazione EfficientFrontier con vincoli PesoFisso/PesoMax, per Categoria e Asset Class; export Excel gestionale (peso*99) e pie chart.
- Riepilogo portafogli: return/vol attesi, Sharpe, diversificazione, path metrics (Heal, TTR, H_min_100); overlay equity/underwater 5Y.
- Variante PH1+HealProxy (se cvxpy): obiettivo massimizzare Heal proxy con floor rendimento >=85% del baseline; grafici comparativi.
## Pattern e criticita'
- Script monolitici, funzioni locali solo utility; forte duplicazione tra versioni (2.5.x, Lite, 2.6).
- Config hard-coded (target vol, vincoli per categoria/asset class, naming file); nessuna parametrizzazione esterna.
- Credenziali DB in chiaro (`connection.txt`); host/porta/db esposti.
- Dati: scarsa validazione; `fillna(0)` su rendimenti distorce metriche; assenza di controllo su duplicati/tipi/periodi mancanti.
- Performance: chiamata stored proc per ogni ISIN in serie; calcolo covarianze/ottimizzazioni per ogni target; niente caching/parallelismo.
- Resilienza: gestione errori minima; cvxpy opzionale ma fallback silenzioso; logging solo via print; niente test.
- Codice morto: alcune utility (path metrics) non usate altrove.
- Output: scrittura massiva di Excel/PNG senza controllo overwrite/versioning.
## Collo di bottiglia
- IO/DB: loop sequenziale sugli ISIN per fetch rendimenti.
- CPU: covarianze e Solvers EfficientFrontier per ogni combinazione durata/volatilita'.
- IO file: generazione multipla di Excel e plot per portafoglio.
## Direzioni di miglioramento (linee guida, non implementate)
- Sicurezza: rimuovere/securizzare `connection.txt`, usare variabili ambiente o secret store; separare credenziali dal repo.
- Configurazione: estrarre target/vincoli in file di config (yaml/json); centralizzare costanti.
- Gestione dipendenze: aggiungere requirements/lockfile e script setup.
- Architettura: modularizzare (data fetch, metriche, optimizer, export), riuso tra versioni, eliminare duplicati.
- Dati: validazione input, gestione missing diversa da fillna(0), log warning sui buchi/ISIN mancanti.
- Performance: fetch batch/caching, eventuale parallelizzazione o memoization di covarianze/metriche.
- Testing/qualita': introdurre test per metriche e vincoli, logging strutturato, controlli su overwrite output.
- Observability: report riepilogo a schermo/logs strutturati piu' dei soli print.