sistemato equity from traes e pulito codice

This commit is contained in:
fredmaloggia
2025-12-05 17:07:06 +01:00
parent 134d5879f8
commit 757c0b7428

View File

@@ -274,7 +274,7 @@ def fetch_price_history(isins, universe: pd.DataFrame, start_date: str, end_date
return merged, True
gap = abs(p_prev - p_next) / abs(p_next)
if gap > 0.02:
print(f"[WARN] Salto prezzo >2% tra {label_prev} e {label_next} su {prev_last['Date'].date()}{next_first['Date'].date()} (gap {gap:.2%}). Fallback non applicato.")
print(f"[WARN] Salto prezzo >2% tra {label_prev} e {label_next} su {prev_last['Date'].date()} -> {next_first['Date'].date()} (gap {gap:.2%}). Fallback non applicato.")
return df_base, False
return merged, True
@@ -342,6 +342,41 @@ def fetch_price_history(isins, universe: pd.DataFrame, start_date: str, end_date
df_px = df_px.sort_values(["ISIN","Date"]).reset_index(drop=True)
return df_px
def save_price_cache_summary(cache_dir: Path, outfile: Path, pattern: str = "*ETFP.csv"):
"""
Salva un riepilogo delle serie prezzi in cache (senza fallback) con min/max date e numero righe.
pattern di default: solo i simboli ETFP.
"""
try:
if not cache_dir.exists():
print(f"[WARN] Cache prezzi non trovata: {cache_dir}")
return
rows = []
for f in sorted(cache_dir.glob(pattern)):
try:
df = pd.read_csv(f, parse_dates=["Date"])
except Exception as e:
rows.append({"Symbol": f.stem, "Errore": str(e)})
continue
if df.empty:
rows.append({"Symbol": f.stem, "Rows": 0})
continue
rows.append({
"Symbol": f.stem,
"min_date": df["Date"].min().date(),
"max_date": df["Date"].max().date(),
"rows": len(df)
})
if not rows:
print(f"[WARN] Nessun file prezzi in cache ({cache_dir}).")
return
out_df = pd.DataFrame(rows).sort_values("Symbol")
outfile.parent.mkdir(parents=True, exist_ok=True)
out_df.to_excel(outfile, index=False)
print(f"[INFO] Salvato riepilogo prezzi (no fallback) in {outfile} ({len(out_df)} righe)")
except Exception as e:
print(f"[WARN] Impossibile salvare riepilogo prezzi: {e}")
def _to_float_safe(x):
try:
return float(x)
@@ -380,6 +415,7 @@ TRADES_REPORT_XLSX = OUTPUT_DIR / "trades_report.xlsx"
PERF_ATTRIB_XLSX = OUTPUT_DIR / "performance_attribution.xlsx"
DAILY_FROM_TRADES_CSV = OUTPUT_DIR / "daily_from_trades.csv"
DAILY_FROM_TRADES_XLSX = OUTPUT_DIR / "daily_from_trades.xlsx"
WEIGHTS_DAILY_XLSX = OUTPUT_DIR / "weights_daily.xlsx"
FINAL_METRICS_XLSX = OUTPUT_DIR / "final_metrics.xlsx"
# Stored Procedure & parametri
@@ -713,7 +749,7 @@ isins = (
universo[col_isin_uni].astype(str).str.strip()
.replace("", pd.NA).dropna().drop_duplicates().tolist()
)
print(f"📄 ISIN totali in universo: {len(isins)}")
print(f"[INFO] ISIN totali in universo: {len(isins)}")
meta_df = pd.DataFrame({"ISIN": universo[col_isin_uni].astype(str).str.strip()})
meta_df["Nome"] = universo[col_name_uni] if col_name_uni else None
@@ -726,7 +762,7 @@ meta_df = meta_df.drop_duplicates(subset=["ISIN"]).reset_index(drop=True)
# =========================================
conn_str = read_connection_txt("connection.txt")
engine = sa.create_engine(conn_str, fast_executemany=True)
print(" Connessione pronta (SQLAlchemy + pyodbc).")
print("[INFO] Connessione pronta (SQLAlchemy + pyodbc).")
# =========================================
# 3) LOOP ISIN → SP → HURST + PATTERN (SOLO LONG per i trade)
@@ -831,7 +867,7 @@ for i, isin in enumerate(isins, 1):
ok_count += 1
if not first_ok_reported:
print("🔎 Colonne riconosciute sul primo ISIN valido:",
print("[INFO] Colonne riconosciute sul primo ISIN valido:",
"Data:", col_date, "| Rendimenti:", col_ret, "| Prezzo:", col_px,
"| H:", round(H,4) if pd.notna(H) else None, "| Regime:", regime)
first_ok_reported = True
@@ -907,12 +943,12 @@ if "DateLast" in summary_pattern.columns:
summary_pattern.to_excel(OUTPUT_PATTERN_XLSX, index=False)
print(f" Salvato: {OUTPUT_HURST_XLSX} (righe: {len(summary_hurst)})")
print(f" Salvato: {OUTPUT_PATTERN_XLSX} (righe: {len(summary_pattern)})")
print(f"[INFO] Salvato: {OUTPUT_HURST_XLSX} (righe: {len(summary_hurst)})")
print(f"[INFO] Salvato: {OUTPUT_PATTERN_XLSX} (righe: {len(summary_pattern)})")
if errors:
pd.DataFrame(errors).to_csv(ERROR_LOG_CSV, index=False)
print(f" Log errori: {ERROR_LOG_CSV} (tot: {len(errors)})")
print(f"[INFO] Log errori: {ERROR_LOG_CSV} (tot: {len(errors)})")
# =========================================
# 4B) FORWARD-BACKTEST (walk-forward) — SOLO LONG
@@ -955,12 +991,13 @@ def knn_forward_backtest_one_asset(df_isin: pd.DataFrame, col_date: str, col_ret
"""
r = pd.to_numeric(df_isin[col_ret], errors="coerce").astype(float) / 100.0 # rendimenti in decimali (close/close)
idx = df_isin[col_date] if col_date in df_isin.columns else pd.RangeIndex(len(r))
idx = pd.to_datetime(idx).dt.normalize()
if exec_ret is not None:
r_exec = pd.to_numeric(exec_ret, errors="coerce").astype(float)
if not r_exec.index.equals(idx):
r_exec = r_exec.reindex(idx)
r_exec.index = pd.to_datetime(r_exec.index).normalize()
# reindex robusto: usa l'ordine di idx, preserva NaN se manca la data
r_exec = r_exec.reindex(idx)
if len(r_exec) != len(r):
# riallinea sullo stesso index; se mancano date, restano NaN
r_exec = pd.Series(r_exec.values, index=idx).reindex(idx)
else:
r_exec = r
@@ -1150,10 +1187,13 @@ for i, isin in enumerate(isins, 1):
)
px_hist_one = px_hist_one.sort_values("Date")
open_series = px_hist_one[["Date","Open"]].dropna()
open_series["Date"] = pd.to_datetime(open_series["Date"]).dt.normalize()
open_series = open_series.drop_duplicates(subset=["Date"]).set_index("Date")["Open"]
open_ret = open_series.pct_change()
# riallinea sulla stessa sequenza di date del df_isin
exec_ret = open_ret.reindex(pd.to_datetime(df_isin[col_date]))
idx_dates = pd.to_datetime(df_isin[col_date]).dt.normalize()
exec_ret = open_ret.reindex(idx_dates)
exec_ret.index = idx_dates
else:
exec_ret = None
except Exception as e:
@@ -1218,8 +1258,8 @@ for i, isin in enumerate(isins, 1):
end_all = time.perf_counter()
total_elapsed = end_all - start_all
avg_elapsed = total_elapsed / max(len(isins), 1)
print(f"⏱️ Tempo totale: {format_eta(total_elapsed)} ({total_elapsed:.2f} sec)")
print(f"⏱️ Tempo medio per asset: {format_eta(avg_elapsed)} ({avg_elapsed:.2f} sec)")
print(f"[INFO] Tempo totale: {format_eta(total_elapsed)} ({total_elapsed:.2f} sec)")
print(f"[INFO] Tempo medio per asset: {format_eta(avg_elapsed)} ({avg_elapsed:.2f} sec)")
bt_signals_df = pd.concat(bt_signals, ignore_index=True) if bt_signals else pd.DataFrame(
@@ -1231,12 +1271,18 @@ bt_summary_df = pd.DataFrame(bt_summary) if bt_summary else pd.DataFrame(
bt_signals_df.to_excel(FORWARD_BT_SIGNALS_XLSX, index=False)
bt_summary_df.to_excel(FORWARD_BT_SUMMARY_XLSX, index=False)
print(f" Salvato: {FORWARD_BT_SIGNALS_XLSX} ({len(bt_signals_df):,} righe)")
print(f" Salvato: {FORWARD_BT_SUMMARY_XLSX} ({len(bt_summary_df):,} righe)")
print(f"[INFO] Salvato: {FORWARD_BT_SIGNALS_XLSX} ({len(bt_signals_df):,} righe)")
print(f"[INFO] Salvato: {FORWARD_BT_SUMMARY_XLSX} ({len(bt_summary_df):,} righe)")
if errors:
pd.DataFrame(errors).to_csv(ERROR_LOG_CSV, index=False)
print(f" Log errori aggiornato: {ERROR_LOG_CSV} (tot: {len(errors)})")
print(f"[INFO] Log errori aggiornato: {ERROR_LOG_CSV} (tot: {len(errors)})")
# Salva riepilogo prezzi (solo simboli primari, senza fallback) ogni run
try:
save_price_cache_summary(OPEN_CACHE_DIR, OPEN_CACHE_DIR / "prezzi_summary_no_fallback.xlsx")
except Exception as e:
print(f"[WARN] Riepilogo prezzi non creato: {e}")
# Timer per fasi post-backtest (sezione 5 in poi)
start_post_timer(total_steps=4)
@@ -1611,6 +1657,10 @@ wide_sig = (
.fillna(0)
.astype(int)
)
wide_est = (
bt.pivot_table(index="Date", columns="ISIN", values="EstOutcome", aggfunc="last")
.sort_index()
)
# (Opzionale) ricostruzione PnL portafoglio con open->open: disattivata di default perché il PnL
# viene già calcolato a livello di singolo asset usando gli open.
@@ -1718,7 +1768,7 @@ def plot_portfolio_composition(weights: pd.DataFrame,
# Plot
fig, ax = plt.subplots(figsize=(11, 6))
ax.stackplot(W_show.index, [W_show[c].values for c in ordered], labels=ordered, colors=colors)
ax.set_title(f"Composizione portafoglio nel tempo {title}")
ax.set_title(f"Composizione portafoglio nel tempo - {title}")
ax.set_ylim(0, 1)
ax.grid(True, alpha=0.3)
ax.set_ylabel("Peso")
@@ -1746,7 +1796,7 @@ def plot_portfolio_composition(weights: pd.DataFrame,
full = os.path.abspath(save_path)
except Exception:
full = save_path
print(f"💾 Salvato: {full}")
print(f"[INFO] Salvato: {full}")
# Plot salvato senza visualizzazione interattiva
@@ -1795,6 +1845,7 @@ _dynamic_portfolio_cache: dict[int, dict] = {}
def _build_dynamic_portfolio_returns(
wide_pnl: pd.DataFrame,
wide_sig: pd.DataFrame,
wide_est: pd.DataFrame,
top_n: int,
window_bars: int = RANKING_WINDOW_BARS,
rp_lookback: int = RP_LOOKBACK
@@ -1820,33 +1871,36 @@ def _build_dynamic_portfolio_returns(
selection = {}
for dt in dates:
window_df = wide_pnl.loc[:dt].tail(window_bars)
metrics_rows = []
for c in all_cols:
s = pd.to_numeric(window_df[c], errors="coerce").dropna()
if s.empty:
continue
stats = drawdown_stats_simple(s)
stats["ISIN"] = str(c)
metrics_rows.append(stats)
if not metrics_rows:
# Considera solo gli ISIN con segnale attivo oggi
sig_row = wide_sig.loc[dt] if dt in wide_sig.index else pd.Series(dtype=float)
on_cols = [c for c in all_cols if sig_row.get(c, 0) == 1]
if not on_cols:
selection[dt] = []
continue
df_window = pd.DataFrame(metrics_rows)
df_window = _apply_score(df_window)
base_isins_dt = (
df_window.sort_values("Score", ascending=False)
.head(top_n)["ISIN"].astype(str).str.strip().tolist()
)
window_est = wide_est.loc[:dt].tail(window_bars) if not wide_est.empty else pd.DataFrame()
scores = []
for c in on_cols:
s = pd.to_numeric(window_est[c], errors="coerce") if c in window_est.columns else pd.Series(dtype=float)
est_score = s.mean(skipna=True)
if pd.isna(est_score):
continue
scores.append((c, est_score))
if not scores:
selection[dt] = []
continue
scores_sorted = sorted(scores, key=lambda x: x[1], reverse=True)
base_isins_dt = [c for c, _ in scores_sorted[:top_n]]
selection[dt] = base_isins_dt
if not base_isins_dt:
continue
w_eq.loc[dt, base_isins_dt] = 1 / len(base_isins_dt)
rp_hist = window_df[base_isins_dt]
window_pnl = wide_pnl.loc[:dt].tail(window_bars)
rp_hist = window_pnl[base_isins_dt]
rp_w = inverse_vol_weights(rp_hist, window=rp_lookback, max_weight=RP_MAX_WEIGHT)
if not rp_w.empty:
last = rp_w.iloc[-1].fillna(0.0)
@@ -1876,6 +1930,7 @@ def _get_dynamic_portfolio(top_n: int) -> dict:
_dynamic_portfolio_cache[top_n] = _build_dynamic_portfolio_returns(
wide_pnl=wide_pnl,
wide_sig=wide_sig,
wide_est=wide_est,
top_n=top_n,
window_bars=RANKING_WINDOW_BARS,
rp_lookback=RP_LOOKBACK
@@ -1898,23 +1953,21 @@ checkpoint_post_timer("Portafoglio rolling")
# -----------------------------
# 5.4 Equity line + Heatmap (salva PNG)
# -----------------------------
eq_eq, eq_rp = map(equity_from_returns, [ret_eq, ret_rp])
plt.figure(figsize=(10,6))
plt.plot(eq_eq, label="Equal Weight")
plt.plot(eq_rp, label="Risk Parity")
plt.legend(); plt.grid(); plt.title("Equity line - Selezione dinamica (Top N)")
plt.tight_layout()
savefig_safe(str(PLOT_DIR / "equity_line_portafogli.png"), dpi=150)
for name, r, path in [
("Equal Weight", ret_eq, PLOT_DIR / "heatmap_equal_weight.png"),
("Risk Parity", ret_rp, PLOT_DIR / "heatmap_risk_parity.png"),
]:
m = portfolio_metrics(r)
print(f"{name:22s} → CAGR {m['CAGR']*100:5.2f}% | Vol {m['Vol']*100:5.2f}% | Sharpe {m['Sharpe'] if m['Sharpe']==m['Sharpe'] else float('nan'):4.2f} | MaxDD {m['MaxDD']*100:5.2f}%")
plot_heatmap_monthly(r, f"Heatmap mensile {name}", save_path=path)
# (DISATTIVATO) Equity line/heatmap teoriche su ret_eq/ret_rp
# eq_eq, eq_rp = map(equity_from_returns, [ret_eq, ret_rp])
# plt.figure(figsize=(10,6))
# plt.plot(eq_eq, label="Equal Weight")
# plt.plot(eq_rp, label="Risk Parity")
# plt.legend(); plt.grid(); plt.title("Equity line - Selezione dinamica (Top N)")
# plt.tight_layout()
# savefig_safe(str(PLOT_DIR / "equity_line_portafogli.png"), dpi=150)
# for name, r, path in [
# ("Equal Weight", ret_eq, PLOT_DIR / "heatmap_equal_weight.png"),
# ("Risk Parity", ret_rp, PLOT_DIR / "heatmap_risk_parity.png"),
# ]:
# m = portfolio_metrics(r)
# print(f"{name:22s} → CAGR {m['CAGR']*100:5.2f}% | Vol {m['Vol']*100:5.2f}% | Sharpe {m['Sharpe'] if m['Sharpe']==m['Sharpe'] else float('nan'):4.2f} | MaxDD {m['MaxDD']*100:5.2f}%")
# plot_heatmap_monthly(r, f"Heatmap mensile {name}", save_path=path)
# =============================
# 5.4bis Composizione nel tempo (ATTIVI vs CASH)
@@ -1981,7 +2034,7 @@ def plot_portfolio_composition_fixed(weights: pd.DataFrame,
fig, ax = plt.subplots(figsize=(11, 6))
ax.stackplot(W_show.index, [W_show[c].values for c in ordered], labels=ordered, colors=colors)
ax.set_title(f"Composizione portafoglio nel tempo {title}")
ax.set_title(f"Composizione portafoglio nel tempo - {title}")
# limite Y = somma massima osservata (<= 1 se pesi + Cash corretti)
ymax = float(np.nanmax(W_show.sum(1).values))
if not np.isfinite(ymax) or ymax <= 0:
@@ -2001,10 +2054,10 @@ def plot_portfolio_composition_fixed(weights: pd.DataFrame,
try:
os.makedirs(folder, exist_ok=True)
except Exception as e:
print(f"[WARN] mkdir '{folder}': {e} salvo in cwd")
print(f"[WARN] mkdir '{folder}': {e} -> salvo in cwd")
save_path = os.path.basename(save_path)
fig.savefig(save_path, dpi=150, bbox_inches="tight")
print(f"💾 Salvato: {os.path.abspath(save_path)}")
print(f"[INFO] Salvato: {os.path.abspath(save_path)}")
# Plot salvato senza visualizzazione interattiva
@@ -2039,6 +2092,40 @@ w_eq_act = make_active_weights(w_eq, wide_sig, renorm_to_1=False, add_cash=Tru
w_rp_act = make_active_weights(w_rp, wide_sig, renorm_to_1=False, add_cash=True, cash_label="Cash")
w_agg_act = make_active_weights(w_agg, wide_sig, renorm_to_1=False, add_cash=True, cash_label="Cash")
# Export pesi giornalieri (Equal/Risk Parity) con cash normalizzato a 100%
def _export_weights_daily(w_eq_act_df: pd.DataFrame, w_rp_act_df: pd.DataFrame, path=WEIGHTS_DAILY_XLSX):
try:
def _prep(df: pd.DataFrame) -> pd.DataFrame:
if df is None or df.empty:
return pd.DataFrame()
out = df.copy()
if "Cash" not in out.columns:
out["Cash"] = 0.0
out = out.apply(pd.to_numeric, errors="coerce").fillna(0.0)
if out.index.has_duplicates:
out = out[~out.index.duplicated(keep="last")]
out = out.sort_index()
row_sum = out.sum(axis=1).replace(0, np.nan)
out = out.div(row_sum, axis=0).fillna(0.0)
cols = [c for c in out.columns if c != "Cash"] + ["Cash"]
return out[cols]
w_eq_x = _prep(w_eq_act_df)
w_rp_x = _prep(w_rp_act_df)
if w_eq_x.empty and w_rp_x.empty:
print("[INFO] Nessun peso da esportare (weights_daily.xlsx non creato).")
return
with pd.ExcelWriter(path) as xw:
if not w_eq_x.empty:
w_eq_x.to_excel(xw, "Equal_Weight", index=True)
if not w_rp_x.empty:
w_rp_x.to_excel(xw, "Risk_Parity", index=True)
print(f"[INFO] Salvato: {path}")
except Exception as e:
print(f"[WARN] Export weights_daily.xlsx fallito: {e}")
_export_weights_daily(w_eq_act, w_rp_act, path=WEIGHTS_DAILY_XLSX)
# --- 3) Plot + salvataggio ---
plot_portfolio_composition_fixed(w_eq_act, "Equal Weight (attivi + Cash)", str(PLOT_DIR / "composition_equal_weight_active.png"))
plot_portfolio_composition_fixed(w_rp_act, "Risk Parity (attivi + Cash)", str(PLOT_DIR / "composition_risk_parity_active.png"))
@@ -2048,13 +2135,14 @@ checkpoint_post_timer("Pesi/plot portafogli")
# -----------------------------
# 5.5 Report trades — SOLO LONG
# -----------------------------
def make_trades_report(sig: pd.DataFrame, pnl: pd.DataFrame, weights: pd.DataFrame, name: str) -> pd.DataFrame:
def make_trades_report(sig: pd.DataFrame, pnl: pd.DataFrame, weights: pd.DataFrame, name: str) -> tuple[pd.DataFrame, pd.DataFrame]:
"""
Report trade SOLO LONG coerente EOD:
- segnale laggato di 1 barra (apertura dal giorno successivo al primo 1)
- PnL allineato al giorno di esposizione: r = pnl.shift(-1)
- chiusura al primo 0 (CloseDate = dt), e a fine serie CloseDate = ultimo + BDay(1)
- Duration_bars = numero di barre accumulate nel PnL del trade
Ritorna (df_trades, df_daily) dove df_daily contiene PnL giorno per giorno per ogni trade valido.
"""
from pandas.tseries.offsets import BDay
@@ -2071,6 +2159,7 @@ def make_trades_report(sig: pd.DataFrame, pnl: pd.DataFrame, weights: pd.DataFra
pnl = pnl.apply(pd.to_numeric, errors="coerce") # mantieni NaN per buchi open
rows = []
daily_rows = []
for isin in sig.columns:
# 1) Segnale EOD (lag 1)
@@ -2082,7 +2171,7 @@ def make_trades_report(sig: pd.DataFrame, pnl: pd.DataFrame, weights: pd.DataFra
# 3) Pesi (se disponibili)
w = (weights[isin].fillna(0.0) if (isin in weights.columns) else pd.Series(0.0, index=s.index))
in_pos, start, acc = False, None, []
in_pos, start, acc, acc_dates = False, None, [], []
for dt in s.index:
sig_t = int(s.at[dt])
@@ -2093,25 +2182,37 @@ def make_trades_report(sig: pd.DataFrame, pnl: pd.DataFrame, weights: pd.DataFra
print(f"[WARN] Trade derubricato {name} {isin}: open/close price mancante nel range {start.date()}-{dt.date()}")
else:
pnl_val = np.prod([1.0 + x for x in acc]) - 1.0 if acc else 0.0
w_start = float(w.get(start, 0.0))
rows.append(dict(
Strategy=name,
ISIN=isin,
OpenDate=start,
CloseDate=dt,
Direction="long",
Size=float(w.get(start, 0.0)),
Size=w_start,
Duration_bars=len(acc),
**{"PnL_%": pnl_val * 100.0}
))
# salva contributo giornaliero reale (senza spalmare)
for dd, rv in zip(acc_dates, acc):
daily_rows.append({
"Strategy": name,
"ISIN": isin,
"Date": dd,
"Size": w_start,
"PnL_day": float(rv)
})
in_pos, start, acc = False, None, []
acc_dates = []
# APERTURA: primo 1 (laggato) quando non in posizione
if (not in_pos) and (sig_t == 1):
in_pos, start, acc = True, dt, []
in_pos, start, acc, acc_dates = True, dt, [], []
# ACCUMULO: PnL del giorno di esposizione
if in_pos:
acc.append(r.at[dt])
acc_dates.append(dt)
# CHIUSURA A FINE SERIE → prossimo business day
if in_pos:
@@ -2120,34 +2221,46 @@ def make_trades_report(sig: pd.DataFrame, pnl: pd.DataFrame, weights: pd.DataFra
print(f"[WARN] Trade derubricato {name} {isin}: open/close price mancante nel range {start.date()}-{close_dt.date()}")
else:
pnl_val = np.prod([1.0 + x for x in acc]) - 1.0 if acc else 0.0
w_start = float(w.get(start, 0.0))
rows.append(dict(
Strategy=name,
ISIN=isin,
OpenDate=start,
CloseDate=close_dt,
Direction="long",
Size=float(w.get(start, 0.0)),
Size=w_start,
Duration_bars=len(acc),
**{"PnL_%": pnl_val * 100.0}
))
for dd, rv in zip(acc_dates, acc):
daily_rows.append({
"Strategy": name,
"ISIN": isin,
"Date": dd,
"Size": w_start,
"PnL_day": float(rv)
})
# Ordina colonne
cols = ["Strategy","ISIN","OpenDate","CloseDate","Direction","Size","Duration_bars","PnL_%"]
out = pd.DataFrame(rows)
out = out[[c for c in cols if c in out.columns] + [c for c in out.columns if c not in cols]]
return out
daily_df = pd.DataFrame(daily_rows)
if not daily_df.empty:
daily_df["Date"] = pd.to_datetime(daily_df["Date"])
return out, daily_df
# Colonne asset effettivamente usate nel portafoglio principale
asset_cols = [c for c in w_eq.columns if float(pd.to_numeric(w_eq[c], errors="coerce").abs().sum()) > 0.0]
if not asset_cols:
asset_cols = list(wide_pnl.columns)
rep_eq = make_trades_report(wide_sig[[c for c in asset_cols if c in wide_sig.columns]],
wide_pnl[[c for c in asset_cols if c in wide_pnl.columns]],
w_eq_act, "Equal Weight")
rep_rp = make_trades_report(wide_sig[[c for c in asset_cols if c in wide_sig.columns]],
wide_pnl[[c for c in asset_cols if c in wide_pnl.columns]],
w_rp_act, "Risk Parity")
rep_eq, daily_eq = make_trades_report(wide_sig[[c for c in asset_cols if c in wide_sig.columns]],
wide_pnl[[c for c in asset_cols if c in wide_pnl.columns]],
w_eq_act, "Equal Weight")
rep_rp, daily_rp = make_trades_report(wide_sig[[c for c in asset_cols if c in wide_sig.columns]],
wide_pnl[[c for c in asset_cols if c in wide_pnl.columns]],
w_rp_act, "Risk Parity")
with pd.ExcelWriter(TRADES_REPORT_XLSX) as xw:
rep_eq.to_excel(xw, "Equal_Weight", index=False)
@@ -2187,7 +2300,7 @@ perf_attr_df = _build_performance_attribution(pd.concat([rep_eq, rep_rp], ignore
perf_attr_df.to_excel(PERF_ATTRIB_XLSX, index=False)
print(f"[INFO] Performance attribution salvata in {PERF_ATTRIB_XLSX}")
print(f" Report trades salvato in {TRADES_REPORT_XLSX}")
print(f"[INFO] Report trades salvato in {TRADES_REPORT_XLSX}")
# ============================================================
# 5.6 Rebuild DAILY PnL from trades_report (calendarized)
# → per rendere coerente il compounding dei trade con equity/heatmap
@@ -2266,42 +2379,67 @@ trades_dict = {
"Risk_Parity": rep_rp if 'rep_rp' in globals() else pd.DataFrame(),
}
daily_from_trades = rebuild_daily_from_trades_dict(trades_dict)
# Ricostruzione daily dai PnL giornalieri reali dei trade (senza spalmare)
daily_detail = pd.concat([daily_eq, daily_rp], ignore_index=True)
if not daily_detail.empty:
daily_detail["Date"] = pd.to_datetime(daily_detail["Date"])
daily_detail["PnL_day"] = pd.to_numeric(daily_detail["PnL_day"], errors="coerce")
daily_detail["Size"] = pd.to_numeric(daily_detail["Size"], errors="coerce").fillna(0.0)
daily_detail = daily_detail.dropna(subset=["Date", "PnL_day"])
daily_detail["Pnl_contrib"] = daily_detail["PnL_day"] * daily_detail["Size"]
daily_from_trades = (
daily_detail.pivot_table(index="Date", columns="Strategy", values="Pnl_contrib", aggfunc="sum")
.sort_index()
.fillna(0.0)
)
else:
daily_from_trades = pd.DataFrame()
# Salva su disco (CSV + XLSX) per ispezione
if not daily_from_trades.empty:
daily_from_trades.to_csv(DAILY_FROM_TRADES_CSV, index_label="Date")
try:
with pd.ExcelWriter(DAILY_FROM_TRADES_XLSX) as xw:
daily_from_trades.to_excel(xw, "Daily", index=True)
except Exception as e:
print(f"[WARN] Impossibile scrivere {DAILY_FROM_TRADES_XLSX}: {e}")
# Plot equity & heatmap basati sui DAILY da trade (coerenti col compounding)
import matplotlib.pyplot as plt
fig, ax = plt.subplots(figsize=(10,6))
for col, lab in [("Equal_Weight","Equal Weight"),
("Risk_Parity","Risk Parity")]:
if col in daily_from_trades.columns:
col_map = [
("Equal Weight", ["Equal Weight", "Equal_Weight"]),
("Risk Parity", ["Risk Parity", "Risk_Parity"]),
]
def _find_col(df, aliases):
for c in aliases:
if c in df.columns:
return c
return None
fig, ax = plt.subplots(figsize=(10, 6))
for lab, aliases in col_map:
col = _find_col(daily_from_trades, aliases)
if col:
eq = (1.0 + daily_from_trades[col].fillna(0.0)).cumprod() * 100
eq.plot(ax=ax, label=lab)
ax.legend(); ax.grid(True)
ax.set_title("Equity line ricostruita dai trades (calendarizzata)")
ax.legend()
ax.grid(True)
ax.set_title("Equity line ricostruita dai trades (calendarizzata)")
fig.tight_layout()
fig.savefig(PLOT_DIR / "equity_from_trades.png", dpi=150)
plt.close(fig)
print(f"💾 Salvato: {PLOT_DIR / 'equity_from_trades.png'}")
print(f"[INFO] Salvato: {PLOT_DIR / 'equity_from_trades.png'}")
# Heatmap per ciascuna strategia
for col, lab, fname in [
("Equal_Weight","Equal Weight","heatmap_equal_from_trades.png"),
("Risk_Parity","Risk Parity","heatmap_rp_from_trades.png"),
for lab, aliases, fname in [
("Equal Weight", ["Equal Weight", "Equal_Weight"], "heatmap_equal_from_trades.png"),
("Risk Parity", ["Risk Parity", "Risk_Parity"], "heatmap_rp_from_trades.png"),
]:
if col in daily_from_trades.columns:
col = _find_col(daily_from_trades, aliases)
if col:
try:
plot_heatmap_monthly(daily_from_trades[col], f"Heatmap mensile {lab} (da trades)",
save_path=PLOT_DIR / fname)
plot_heatmap_monthly(
daily_from_trades[col],
f"Heatmap mensile - {lab} (da trades)",
save_path=PLOT_DIR / fname,
)
except Exception as e:
print(f"[WARN] Heatmap {lab} da trades: {e}")
else:
@@ -2520,108 +2658,108 @@ def _build_portfolio_returns_for_isins(base_isins_N, wide_pnl):
return ret_eq_N, ret_rp_N
# --- calcolo metriche per TopN 8..15 ---
rows_byN = []
for top_n in range(8, 16):
portN = _get_dynamic_portfolio(top_n)
ret_eq_N = portN["ret_eq"]
ret_rp_N = portN["ret_rp"]
# # --- calcolo metriche per TopN 8..15 --- (DISATTIVATO)
# rows_byN = []
# for top_n in range(8, 16):
# portN = _get_dynamic_portfolio(top_n)
# ret_eq_N = portN["ret_eq"]
# ret_rp_N = portN["ret_rp"]
#
# # (OPZIONALE) se vuoi anche salvare equity/heatmap per ciascun N:
# # _save_equity_plot_byN(ret_eq_N, ret_rp_N, top_n)
# # _save_heatmaps_byN(ret_eq_N, ret_rp_N, top_n)
#
# # Calcola le metriche (come nell'ottimizzatore)
# for strategy_name, rser in [
# ("Equal_Weight", ret_eq_N),
# ("Risk_Parity", ret_rp_N),
# ]:
# m = _calc_all_metrics_from_returns(rser)
# m["TopN"] = top_n
# m["Strategy"] = strategy_name
# rows_byN.append(m)
#
# # DataFrame finale con la colonna TopN
# final_byN_df = pd.DataFrame(rows_byN)[[
# "TopN", "Strategy",
# "Rendimento_Ann", "Volatilita_Ann", "CAGR", "R2_Equity",
# "MaxDD", "DD_Duration_Max", "TTR_from_MDD",
# "AAW", "AUW", "Heal_Index", "H_min_100m_5Y"
# ]].sort_values(["TopN","Strategy"]).reset_index(drop=True)
#
# # Salvataggio: aggiunge/riscrive i fogli in final_metrics.xlsx
# # - mantiene (se vuoi) anche il foglio "Portfolio_Metrics" del caso corrente TOP_N
# try:
# with pd.ExcelWriter(FINAL_METRICS_XLSX, engine="openpyxl", mode="a", if_sheet_exists="replace") as xw:
# final_byN_df.to_excel(xw, "Portfolio_Metrics_By_N", index=False)
# except Exception:
# with pd.ExcelWriter(FINAL_METRICS_XLSX) as xw:
# final_byN_df.to_excel(xw, "Portfolio_Metrics_By_N", index=False)
#
# print(f"✅ Salvato: {FINAL_METRICS_XLSX} (Portfolio_Metrics_By_N) per TopN = 8..15")
# (OPZIONALE) se vuoi anche salvare equity/heatmap per ciascun N:
# _save_equity_plot_byN(ret_eq_N, ret_rp_N, top_n)
# _save_heatmaps_byN(ret_eq_N, ret_rp_N, top_n)
# Calcola le metriche (come nell'ottimizzatore)
for strategy_name, rser in [
("Equal_Weight", ret_eq_N),
("Risk_Parity", ret_rp_N),
]:
m = _calc_all_metrics_from_returns(rser)
m["TopN"] = top_n
m["Strategy"] = strategy_name
rows_byN.append(m)
# DataFrame finale con la colonna TopN
final_byN_df = pd.DataFrame(rows_byN)[[
"TopN", "Strategy",
"Rendimento_Ann", "Volatilita_Ann", "CAGR", "R2_Equity",
"MaxDD", "DD_Duration_Max", "TTR_from_MDD",
"AAW", "AUW", "Heal_Index", "H_min_100m_5Y"
]].sort_values(["TopN","Strategy"]).reset_index(drop=True)
# Salvataggio: aggiunge/riscrive i fogli in final_metrics.xlsx
# - mantiene (se vuoi) anche il foglio "Portfolio_Metrics" del caso corrente TOP_N
try:
with pd.ExcelWriter(FINAL_METRICS_XLSX, engine="openpyxl", mode="a", if_sheet_exists="replace") as xw:
final_byN_df.to_excel(xw, "Portfolio_Metrics_By_N", index=False)
except Exception:
with pd.ExcelWriter(FINAL_METRICS_XLSX) as xw:
final_byN_df.to_excel(xw, "Portfolio_Metrics_By_N", index=False)
print(f"✅ Salvato: {FINAL_METRICS_XLSX} (Portfolio_Metrics_By_N) per TopN = 8..15")
# ======================================================================
# 6bis) Plot per ciascun TopN (8..15): Equity + Heatmap per strategia
# ======================================================================
import os
import numpy as np
import matplotlib.pyplot as plt
OUT_DIR = PLOT_DIR
OUT_DIR.mkdir(parents=True, exist_ok=True)
def _safe_series(r: pd.Series) -> pd.Series:
"""Forza tipo numerico e se tutto NaN, rimpiazza con 0.0 (linea piatta ma plot salvato)."""
r = pd.to_numeric(r, errors="coerce")
if r.notna().sum() == 0:
r = pd.Series(0.0, index=r.index)
return r.fillna(0.0)
def _save_equity_plot_byN(ret_eq, ret_rp, top_n: int):
ret_eq = _safe_series(ret_eq)
ret_rp = _safe_series(ret_rp)
eq_eq = equity_from_returns(ret_eq)
eq_rp = equity_from_returns(ret_rp)
if eq_eq.empty and eq_rp.empty:
eq_eq = pd.Series([100.0], index=[pd.Timestamp("2000-01-01")])
fig, ax = plt.subplots(figsize=(10, 6))
eq_eq.plot(ax=ax, label="Equal Weight")
eq_rp.plot(ax=ax, label="Risk Parity")
ax.legend()
ax.grid(True)
ax.set_title(f"Equity line - TopN={top_n}")
fig.tight_layout()
savefig_safe(str(OUT_DIR / f"equity_topN_{top_n}.png"), dpi=150)
plt.close(fig)
def _save_heatmaps_byN(ret_eq, ret_rp, top_n: int):
ret_eq = _safe_series(ret_eq)
ret_rp = _safe_series(ret_rp)
plot_heatmap_monthly(
ret_eq,
f"Heatmap mensile - Equal Weight (TopN={top_n})",
save_path=OUT_DIR / f"heatmap_equal_topN_{top_n}.png"
)
plot_heatmap_monthly(
ret_rp,
f"Heatmap mensile - Risk Parity (TopN={top_n})",
save_path=OUT_DIR / f"heatmap_rp_topN_{top_n}.png"
)
# Loop 8..15 replicando i plot per ciascuna combinazione
for top_n in range(8, 16):
portN = _get_dynamic_portfolio(top_n)
ret_eq_N = portN["ret_eq"]
ret_rp_N = portN["ret_rp"]
_save_equity_plot_byN(ret_eq_N, ret_rp_N, top_n)
_save_heatmaps_byN(ret_eq_N, ret_rp_N, top_n)
print(f"✅ Plot salvati in: {OUT_DIR}/")
# # ======================================================================
# # 6bis) Plot per ciascun TopN (8..15): Equity + Heatmap per strategia (DISATTIVATO)
# # ======================================================================
# # import os
# # import numpy as np
# # import matplotlib.pyplot as plt
# #
# # OUT_DIR = PLOT_DIR
# # OUT_DIR.mkdir(parents=True, exist_ok=True)
# #
# # def _safe_series(r: pd.Series) -> pd.Series:
# # """Forza tipo numerico e se tutto NaN, rimpiazza con 0.0 (linea piatta ma plot salvato)."""
# # r = pd.to_numeric(r, errors="coerce")
# # if r.notna().sum() == 0:
# # r = pd.Series(0.0, index=r.index)
# # return r.fillna(0.0)
# #
# # def _save_equity_plot_byN(ret_eq, ret_rp, top_n: int):
# # ret_eq = _safe_series(ret_eq)
# # ret_rp = _safe_series(ret_rp)
# #
# # eq_eq = equity_from_returns(ret_eq)
# # eq_rp = equity_from_returns(ret_rp)
# #
# # if eq_eq.empty and eq_rp.empty:
# # eq_eq = pd.Series([100.0], index=[pd.Timestamp("2000-01-01")])
# #
# # fig, ax = plt.subplots(figsize=(10, 6))
# # eq_eq.plot(ax=ax, label="Equal Weight")
# # eq_rp.plot(ax=ax, label="Risk Parity")
# # ax.legend()
# # ax.grid(True)
# # ax.set_title(f"Equity line - TopN={top_n}")
# # fig.tight_layout()
# # savefig_safe(str(OUT_DIR / f"equity_topN_{top_n}.png"), dpi=150)
# # plt.close(fig)
# #
# # def _save_heatmaps_byN(ret_eq, ret_rp, top_n: int):
# # ret_eq = _safe_series(ret_eq)
# # ret_rp = _safe_series(ret_rp)
# #
# # plot_heatmap_monthly(
# # ret_eq,
# # f"Heatmap mensile - Equal Weight (TopN={top_n})",
# # save_path=OUT_DIR / f"heatmap_equal_topN_{top_n}.png"
# # )
# # plot_heatmap_monthly(
# # ret_rp,
# # f"Heatmap mensile - Risk Parity (TopN={top_n})",
# # save_path=OUT_DIR / f"heatmap_rp_topN_{top_n}.png"
# # )
# #
# # # Loop 8..15 replicando i plot per ciascuna combinazione
# # for top_n in range(8, 16):
# # portN = _get_dynamic_portfolio(top_n)
# # ret_eq_N = portN["ret_eq"]
# # ret_rp_N = portN["ret_rp"]
# #
# # _save_equity_plot_byN(ret_eq_N, ret_rp_N, top_n)
# # _save_heatmaps_byN(ret_eq_N, ret_rp_N, top_n)
# #
# # print(f"✅ Plot salvati in: {OUT_DIR}/")
# ======================================================================
# 6ter) Plot composizione (ATTIVI + Cash) per ciascun TopN (8..15)
@@ -2748,17 +2886,16 @@ def _build_weights_for_isins(base_isins_N, crypto_isin_N, wide_pnl):
return W.div(rs, axis=0).fillna(0.0).clip(lower=0.0)
return _norm(w_eq_N), _norm(w_rp_N), _norm(w_agg_N)
# === Loop 8..15: crea pesi, attiva coi Signal, plotta e SALVA in OUT_DIR ===
# === Loop 8..15: crea pesi, attiva coi Signal, plotta e SALVA in OUT_DIR ===
for top_n in range(8, 16):
portN = _get_dynamic_portfolio(top_n)
w_eq_act_N = portN["w_eq_act"]
w_rp_act_N = portN["w_rp_act"]
# path di salvataggio
sp_eq = OUT_DIR / f"composition_equal_topN_{top_n}.png"
sp_rp = OUT_DIR / f"composition_rp_topN_{top_n}.png"
# plot + salvataggio (SOLO Equal e Risk Parity)
plot_portfolio_composition_fixed(w_eq_act_N, f"Equal Weight (attivi + Cash) TopN={top_n}", sp_eq)
plot_portfolio_composition_fixed(w_rp_act_N, f"Risk Parity (attivi + Cash) TopN={top_n}", sp_rp)
# # === Loop 8..15: crea pesi, attiva coi Signal, plotta e SALVA in OUT_DIR === (DISATTIVATO)
# for top_n in range(8, 16):
# portN = _get_dynamic_portfolio(top_n)
# w_eq_act_N = portN["w_eq_act"]
# w_rp_act_N = portN["w_rp_act"]
#
# # path di salvataggio
# sp_eq = OUT_DIR / f"composition_equal_topN_{top_n}.png"
# sp_rp = OUT_DIR / f"composition_rp_topN_{top_n}.png"
#
# # plot + salvataggio (SOLO Equal e Risk Parity)
# plot_portfolio_composition_fixed(w_eq_act_N, f"Equal Weight (attivi + Cash) TopN={top_n}", sp_eq)
# plot_portfolio_composition_fixed(w_rp_act_N, f"Risk Parity (attivi + Cash) TopN={top_n}", sp_rp)