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 pandas as pd
import matplotlib.pyplot as plt
import yaml
import logging
from sqlalchemy import create_engine, text
from sqlalchemy.exc import SQLAlchemyError
@@ -25,10 +27,13 @@ from pypfopt.exceptions import OptimizationError
OUTPUT_DIR = "Output"
PLOT_DIR = "Plot"
INPUT_DIR = "Input"
CONFIG_FILE = "config.yaml"
os.makedirs(OUTPUT_DIR, exist_ok=True)
os.makedirs(PLOT_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:
"""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."""
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
# ---------------------------------
@@ -291,6 +377,7 @@ df = pd.read_excel(
usecols=['ISIN', 'Nome', 'Categoria', 'Asset Class', 'PesoMax', 'PesoFisso', 'Codice Titolo'],
dtype={'Codice Titolo': str}
)
validate_universe(df)
# =========================
# SERIE STORICHE RENDIMENTI
@@ -320,6 +407,7 @@ for isin in df['ISIN'].unique():
except SQLAlchemyError as e:
print(f"Errore durante l'esecuzione della stored procedure per {isin}:", e)
validate_returns_frame(final_df)
final_df.fillna(0, inplace=True)
# -------- 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
# =========================
volatility_targets = {
# (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'
}
volatility_targets, asset_class_limits = load_targets_and_limits(CONFIG_FILE)
days_per_year = 252
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)
# 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():
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]

View File

@@ -25,23 +25,29 @@ def plot_path(filename: str) -> str:
"""Percorso completo per i file di grafico."""
return os.path.join(PLOT_DIR, filename)
# Configurazione della connessione al database
username = 'readonly'
password = 'e8nqtSa39L4Le3'
host = '26.69.45.60'
database = 'FirstSolutionDB'
port = 1433
connection_string = f"mssql+pyodbc://{username}:{password}@{host}:{port}/{database}?driver=ODBC+Driver+17+for+SQL+Server"
# Configurazione della connessione al database (da connection.txt)
params = {}
with open("connection.txt", "r") as f:
for line in f:
line = line.strip()
if line and not line.startswith('#'):
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:
# Crea l'Engine
engine = create_engine(connection_string)
# Usa il contesto con la connessione e il metodo text() per eseguire la query
with engine.connect() as connection:
result = connection.execute(text("SELECT 1"))
_ = connection.execute(text("SELECT 1"))
print("Connessione al database riuscita.")
except SQLAlchemyError as e:
print("Errore durante la connessione al database:", e)
sys.exit()
@@ -98,6 +104,7 @@ riskfree_rate = 0.02
# Ottimizzazione per ciascun target di volatilità e salvataggio dei risultati
optimized_weights = pd.DataFrame()
summary_data = []
for (years, target_vol), name in volatility_targets.items():
period_start_date = end_date - pd.DateOffset(years=years)
period_df = final_df.loc[period_start_date:end_date]
@@ -135,8 +142,16 @@ for (years, target_vol), name in volatility_targets.items():
ef.efficient_risk(target_volatility=target_vol)
weights = ef.clean_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
results = []
for isin, weight in weights.items():
@@ -181,10 +196,12 @@ for (years, target_vol), name in volatility_targets.items():
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]
# 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")
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 pandas as pd
import matplotlib.pyplot as plt
import yaml
import logging
from sqlalchemy import create_engine, text
from sqlalchemy.exc import SQLAlchemyError
@@ -23,10 +25,13 @@ from pypfopt.exceptions import OptimizationError
OUTPUT_DIR = "Output"
INPUT_DIR = "Input"
PLOT_DIR = "Plot"
CONFIG_FILE = "config.yaml"
os.makedirs(OUTPUT_DIR, exist_ok=True)
os.makedirs(INPUT_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:
"""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."""
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
# ---------------------------------
@@ -233,6 +301,7 @@ df = pd.read_excel(
usecols=['ISIN', 'Nome', 'Categoria', 'Asset Class', 'PesoMax', 'PesoFisso', 'Codice Titolo'],
dtype={'Codice Titolo': str}
)
validate_universe(df)
# =========================
# SERIE STORICHE RENDIMENTI
@@ -262,6 +331,7 @@ for isin in df['ISIN'].unique():
except SQLAlchemyError as e:
print(f"Errore durante l'esecuzione della stored procedure per {isin}:", e)
validate_returns_frame(final_df)
final_df.fillna(0, inplace=True)
# -------- 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
# =========================
volatility_targets = {
# (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'
}
volatility_targets, asset_class_limits = load_targets_and_limits(CONFIG_FILE)
days_per_year = 252
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)
# 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():
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]
@@ -429,7 +484,7 @@ for (years, target_vol), name in volatility_targets.items():
print(f"File {output_file_path} saved successfully.")
# --- 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():
r_sel = df.loc[df['ISIN'] == isin]
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.