refactoring

This commit is contained in:
fredmaloggia
2026-05-24 12:24:30 +02:00
parent 1422cc5fb2
commit 3c3f2a7705
8 changed files with 2272 additions and 742 deletions

View File

@@ -1,24 +1,38 @@
# -*- coding: utf-8 -*-
"""
Equity/Reconciliation Builder from Audit Log
Equity / Reconciliation Builder from Audit Log
================================================
- Legge trades_audit_log.csv (OPEN/CLOSE; EntryAmount base=100; EntryIndex opzionale)
- Scarica rendimenti giornalieri via stored procedure (connection.txt)
- Converte i rendimenti in decimali coerenti (percentuali => /100; log-return => expm1)
- Ricostruisce i rendimenti giornalieri per strategia come MEDIA PONDERATA sui trade attivi
- Salva:
- daily_returns_by_strategy.csv
- equity_by_strategy.csv
- debug_daily_by_strategy.csv
- equity_by_strategy.png, drawdown_by_strategy.png
- Mostra anche a video i grafici
Legge il trades_audit_log.csv prodotto dalla pipeline di produzione e ricostruisce:
- daily_returns_by_strategy.csv: rendimenti giornalieri per strategia
(media ponderata sui trade attivi)
- equity_by_strategy.csv: curve di equity composte (base 100)
- debug_daily_by_strategy.csv: scomposizione num/den/ret per debug
- plot/equity_by_strategy.png
- plot/drawdown_by_strategy.png
Modifiche v2.1 (questa versione):
- Supporto MULTI-STRATEGIA: legge dinamicamente la whitelist da
pattern_knn_config.json -> equity_log.strategy_whitelist. Le strategie attese
sono ora 4 (Equal_Weight, Risk_Parity, Equal_Weight_v2, Risk_Parity_v2).
- Parser audit log tollerante a formato date misto (ISO + DD/MM/YYYY) per
retrocompatibilita' con il log esistente.
- Rimossa duplicazione codice / pulizia delle import ridondanti.
- Esecuzione idempotente: tutti i file output vengono rigenerati ad ogni run.
Esecuzione: python equity_from_log.py
"""
from __future__ import annotations
from pathlib import Path
import pandas as pd
import numpy as np
import shutil
from pathlib import Path
from typing import List, Optional
import numpy as np
import pandas as pd
import sqlalchemy as sa
from sqlalchemy import text as sql_text
from shared_utils import (
detect_column,
@@ -27,147 +41,126 @@ from shared_utils import (
require_section,
)
# =============================================================================
# PATH & OUTPUT
# CONFIG & PATHS
# =============================================================================
BASE_DIR = Path(__file__).resolve().parent
CONFIG = None
BASE_DIR = Path(__file__).resolve().parent
try:
CONFIG = load_config()
PATHS_CONFIG = require_section(CONFIG, "paths")
except Exception as exc: # pragma: no cover - best effort
print(f"[WARN] Config non disponibile ({exc}); uso i percorsi di default.")
CONFIG = None
PATHS_CONFIG = {}
OUTPUT_DIR = BASE_DIR / PATHS_CONFIG.get("output_dir", "output")
PLOT_DIR = BASE_DIR / PATHS_CONFIG.get("plot_dir", "plot")
AUDIT_LOG_CSV = BASE_DIR / PATHS_CONFIG.get("audit_log_csv", OUTPUT_DIR / "trades_audit_log.csv")
OUTPUT_DIR = BASE_DIR / PATHS_CONFIG.get("output_dir", "output")
PLOT_DIR = BASE_DIR / PATHS_CONFIG.get("plot_dir", "plot")
AUDIT_LOG_CSV = BASE_DIR / PATHS_CONFIG.get(
"audit_log_csv", OUTPUT_DIR / "trades_audit_log.csv"
)
CONNECTION_TXT = BASE_DIR / PATHS_CONFIG.get("connection_txt", "connection.txt")
OUT_DAILY_CSV = OUTPUT_DIR / "daily_returns_by_strategy.csv"
OUT_DAILY_CSV = OUTPUT_DIR / "daily_returns_by_strategy.csv"
OUT_EQUITY_CSV = OUTPUT_DIR / "equity_by_strategy.csv"
OUT_DEBUG_CSV = OUTPUT_DIR / "debug_daily_by_strategy.csv"
PLOT_EQUITY = PLOT_DIR / "equity_by_strategy.png"
PLOT_DD = PLOT_DIR / "drawdown_by_strategy.png"
OUT_DEBUG_CSV = OUTPUT_DIR / "debug_daily_by_strategy.csv"
PLOT_EQUITY = PLOT_DIR / "equity_by_strategy.png"
PLOT_DD = PLOT_DIR / "drawdown_by_strategy.png"
DROPBOX_EXPORT_DIR = Path(r"C:\Users\Admin\Dropbox\Condivisa Lavoro\Segnali di trading su ETF")
DROPBOX_EXPORT_DIR = Path(
r"C:\Users\Admin\Dropbox\Condivisa Lavoro\Segnali di trading su ETF"
)
# Stored procedure defaults (sovrascritti se presenti in config)
SP_NAME_DEFAULT = "opt_RendimentoGiornaliero1_ALL"
SP_N_DEFAULT = 1260
PTF_CURR_DEFAULT = "EUR"
if CONFIG is not None:
try:
DB_CONFIG = require_section(CONFIG, "db")
SP_NAME_DEFAULT = str(DB_CONFIG.get("stored_proc", SP_NAME_DEFAULT))
SP_N_DEFAULT = int(DB_CONFIG.get("n_bars", SP_N_DEFAULT))
PTF_CURR_DEFAULT = str(DB_CONFIG.get("ptf_curr", PTF_CURR_DEFAULT))
except KeyError:
pass
# Whitelist strategie (con fallback alle 4 attese in v2.1)
DEFAULT_STRATEGIES = ["Equal_Weight", "Risk_Parity", "Equal_Weight_v2", "Risk_Parity_v2"]
EQUITY_CFG = CONFIG.get("equity_log", {}) if CONFIG else {}
raw_whitelist = EQUITY_CFG.get("strategy_whitelist") if isinstance(EQUITY_CFG, dict) else None
if raw_whitelist:
cleaned = [str(x).strip() for x in raw_whitelist if str(x).strip()]
VALID_STRATEGIES = cleaned if cleaned else DEFAULT_STRATEGIES
else:
VALID_STRATEGIES = DEFAULT_STRATEGIES
# =============================================================================
# DROPBOX EXPORT
# =============================================================================
def copy_to_dropbox(src: Path, dst_dir: Path = DROPBOX_EXPORT_DIR) -> bool:
if not src or not dst_dir:
return False
if not src.exists():
print(f"[WARN] file non trovato per copia Dropbox: {src}")
if not src or not dst_dir or not src.exists():
if src and not src.exists():
print(f"[WARN] file non trovato per copia Dropbox: {src}")
return False
try:
dst_dir.mkdir(parents=True, exist_ok=True)
dst = dst_dir / src.name
shutil.copy2(src, dst)
shutil.copy2(src, dst_dir / src.name)
print(f"[DROPBOX] Copiato {src.name} in {dst_dir}")
return True
except Exception as exc:
print(f"[WARN] impossibile copiare {src} su {dst_dir}: {exc}")
return False
# Stored procedure
SP_NAME_DEFAULT = "opt_RendimentoGiornaliero1_ALL"
SP_N_DEFAULT = 1260
PTF_CURR_DEFAULT = "EUR"
try:
DB_CONFIG = require_section(CONFIG, "db") if CONFIG else {}
except Exception as exc: # pragma: no cover - best effort
print(f"[WARN] Config DB non disponibile ({exc}); uso i default interni.")
DB_CONFIG = {}
else:
SP_NAME_DEFAULT = str(DB_CONFIG.get("stored_proc", SP_NAME_DEFAULT))
SP_N_DEFAULT = int(DB_CONFIG.get("n_bars", SP_N_DEFAULT))
PTF_CURR_DEFAULT = str(DB_CONFIG.get("ptf_curr", PTF_CURR_DEFAULT))
DEFAULT_STRATEGIES = ["Equal_Weight", "Risk_Parity"]
VALID_STRATEGIES = DEFAULT_STRATEGIES
EQUITY_CFG = CONFIG.get("equity_log", {}) if CONFIG else {}
raw_whitelist = EQUITY_CFG.get("strategy_whitelist") if isinstance(EQUITY_CFG, dict) else None
if raw_whitelist:
whitelist = [str(x).strip() for x in raw_whitelist if str(x).strip()]
if whitelist:
VALID_STRATEGIES = whitelist
# =============================================================================
# AUDIT LOG LOADER (FORMAT CHECKS)
# AUDIT LOG LOADER (PARSER ROBUSTO)
# =============================================================================
REQUIRED_AUDIT_COLS = ["Strategy", "ISIN", "Action", "TradeDate"]
CANONICAL_AUDIT_COLS = [
"Strategy",
"ISIN",
"Action",
"TradeDate",
"EntryIndex",
"EntryAmount",
"SizeWeight",
"Price",
"PnL_%",
"ExitReason",
"LinkedOpenDate",
"Duration_bars",
"Notes",
]
NUMERIC_COLS = [
"EntryIndex",
"EntryAmount",
"SizeWeight",
"Price",
"PnL_%",
"Duration_bars",
"Strategy", "ISIN", "Action", "TradeDate",
"EntryIndex", "EntryAmount", "SizeWeight", "Price",
"PnL_%", "ExitReason", "LinkedOpenDate", "Duration_bars", "Notes",
]
NUMERIC_COLS = ["EntryIndex", "EntryAmount", "SizeWeight", "Price", "PnL_%", "Duration_bars"]
def _clean_numeric_series(s: pd.Series) -> pd.Series:
"""Conversione robusta a numerico, tollera separatori italiani/europei."""
if pd.api.types.is_numeric_dtype(s):
return s
txt = s.astype(str).str.strip()
txt = txt.str.replace("%", "", regex=False)
txt = s.astype(str).str.strip().str.replace("%", "", regex=False)
txt = txt.replace({"": np.nan, "nan": np.nan, "None": np.nan})
def _fix_one(val: str) -> str:
def _fix_one(val):
if val is None or (isinstance(val, float) and np.isnan(val)):
return val
v = str(val).strip()
if not v:
return v
dot_n = v.count(".")
comma_n = v.count(",")
# Heuristic:
# - multiple dots with no commas => dots are thousands separators
dot_n, comma_n = v.count("."), v.count(",")
if dot_n > 1 and comma_n == 0:
return v.replace(".", "")
# - both comma and dot present => decide decimal by last separator
return v.replace(".", "") # dots are thousands sep
if dot_n > 0 and comma_n > 0:
last_dot = v.rfind(".")
last_comma = v.rfind(",")
if last_comma > last_dot:
# comma as decimal, dots as thousands
return v.replace(".", "").replace(",", ".")
# dot as decimal, commas as thousands
return v.replace(",", "")
# - only comma present => comma as decimal
return (v.replace(".", "").replace(",", ".")
if v.rfind(",") > v.rfind(".")
else v.replace(",", ""))
if comma_n > 0 and dot_n == 0:
return v.replace(",", ".")
return v.replace(",", ".") # comma is decimal
return v
cleaned = txt.map(_fix_one)
return pd.to_numeric(cleaned, errors="coerce")
return pd.to_numeric(txt.map(_fix_one), errors="coerce")
def _parse_mixed_dates(series: pd.Series) -> pd.Series:
s = series.astype(str).str.strip()
s = s.replace({"": np.nan, "nan": np.nan, "None": np.nan})
dt_iso = pd.to_datetime(s, format="%Y-%m-%d", errors="coerce")
dt_iso_ts = pd.to_datetime(s, format="%Y-%m-%d %H:%M:%S", errors="coerce")
dt_dmy = pd.to_datetime(s, format="%d/%m/%Y", errors="coerce")
dt_dmy_ts = pd.to_datetime(s, format="%d/%m/%Y %H:%M:%S", errors="coerce")
return dt_iso.fillna(dt_iso_ts).fillna(dt_dmy).fillna(dt_dmy_ts)
"""Parser per date in formato misto ISO + europeo."""
s = series.astype(str).str.strip().replace({"": np.nan, "nan": np.nan, "None": np.nan})
return (pd.to_datetime(s, format="%Y-%m-%d", errors="coerce")
.fillna(pd.to_datetime(s, format="%Y-%m-%d %H:%M:%S", errors="coerce"))
.fillna(pd.to_datetime(s, format="%d/%m/%Y", errors="coerce"))
.fillna(pd.to_datetime(s, format="%d/%m/%Y %H:%M:%S", errors="coerce")))
def load_audit_log(path: Path) -> pd.DataFrame:
@@ -189,8 +182,7 @@ def load_audit_log(path: Path) -> pd.DataFrame:
if not header or "TradeDate" not in header:
header = CANONICAL_AUDIT_COLS.copy()
rows = []
mixed_rows = 0
rows, mixed_rows = [], 0
for line in lines[1:]:
if not line or not line.strip():
continue
@@ -210,7 +202,7 @@ def load_audit_log(path: Path) -> pd.DataFrame:
df = pd.DataFrame(rows, columns=header)
if mixed_rows > 0:
print(f"[WARN] Audit log con {mixed_rows} righe in formato legacy/misto: normalizzate in lettura.")
print(f"[WARN] Audit log con {mixed_rows} righe in formato legacy: normalizzate in lettura.")
missing = [c for c in REQUIRED_AUDIT_COLS if c not in df.columns]
if missing:
@@ -219,70 +211,63 @@ def load_audit_log(path: Path) -> pd.DataFrame:
f"Colonne trovate: {list(df.columns)}"
)
# Normalize key columns
# Normalizzazione
df["Action"] = df["Action"].astype(str).str.upper().str.strip()
df["Strategy"] = df["Strategy"].astype(str).str.strip()
df["ISIN"] = df["ISIN"].astype(str).str.strip()
# Dates
df["TradeDate"] = _parse_mixed_dates(df["TradeDate"])
if "LinkedOpenDate" in df.columns:
df["LinkedOpenDate"] = _parse_mixed_dates(df["LinkedOpenDate"])
# Drop rows with invalid dates
# Pulizia righe
before = len(df)
df = df.dropna(subset=["TradeDate"])
dropped = before - len(df)
if dropped > 0:
if (dropped := before - len(df)) > 0:
print(f"[WARN] Rimosse {dropped} righe con TradeDate non valido.")
# Keep only OPEN/CLOSE if present
if "Action" in df.columns:
before = len(df)
df = df[df["Action"].isin(["OPEN", "CLOSE"])]
dropped = before - len(df)
if dropped > 0:
print(f"[WARN] Rimosse {dropped} righe con Action non valida.")
before = len(df)
df = df[df["Action"].isin(["OPEN", "CLOSE"])]
if (dropped := before - len(df)) > 0:
print(f"[WARN] Rimosse {dropped} righe con Action non valida.")
# Numeric cleanup
# Conversione numerici
for col in NUMERIC_COLS:
if col in df.columns:
df[col] = _clean_numeric_series(df[col])
return df
# =============================================================================
# FETCH RENDIMENTI DAL DB
# =============================================================================
def fetch_returns_from_db(isins, start_date, end_date) -> pd.DataFrame:
import sqlalchemy as sa
from sqlalchemy import text as sql_text
# DB FETCH RETURNS
# =============================================================================
def fetch_returns_from_db(
isins: List[str], start_date, end_date
) -> pd.DataFrame:
"""Scarica i rendimenti giornalieri per gli ISIN dell'audit log."""
conn_str = read_connection_txt(CONNECTION_TXT)
engine = sa.create_engine(conn_str, fast_executemany=True)
sp = SP_NAME_DEFAULT
nbar = SP_N_DEFAULT
ptf = PTF_CURR_DEFAULT
sql_sp = sql_text(f"EXEC {sp} @ISIN = :isin, @n = :n, @PtfCurr = :ptf")
engine = sa.create_engine(conn_str, fast_executemany=True)
sql_sp = sql_text(
f"EXEC {SP_NAME_DEFAULT} @ISIN = :isin, @n = :n, @PtfCurr = :ptf"
)
frames = []
with engine.begin() as conn:
for isin in isins:
try:
df = pd.read_sql_query(sql_sp, conn, params={"isin": isin, "n": nbar, "ptf": ptf})
except Exception as e:
print(f"[ERROR] SP {sp} fallita per {isin}: {e}")
df = pd.read_sql_query(
sql_sp, conn,
params={"isin": isin, "n": SP_N_DEFAULT, "ptf": PTF_CURR_DEFAULT},
)
except Exception as exc:
print(f"[ERROR] SP {SP_NAME_DEFAULT} fallita per {isin}: {exc}")
continue
if df.empty:
continue
col_date = detect_column(df, ["Date", "Data", "Datetime", "Timestamp", "Time"])
col_ret = detect_column(df, ["Ret", "Return", "Rendimento", "Rend", "Ret_%", "RET"])
col_px = detect_column(df, ["Close", "AdjClose", "Price", "Px", "Last", "Prezzo", "Chiusura"])
col_ret = detect_column(df, ["Ret", "Return", "Rendimento", "Rend", "Ret_%", "RET"])
col_px = detect_column(df, ["Close", "AdjClose", "Price", "Px", "Last", "Prezzo", "Chiusura"])
if not col_date:
continue
@@ -295,26 +280,23 @@ def fetch_returns_from_db(isins, start_date, end_date) -> pd.DataFrame:
elif col_px:
px = pd.to_numeric(df[col_px], errors="coerce").astype(float).replace(0, np.nan)
log_r = np.log(px / px.shift(1))
r = np.expm1(log_r) # log-return -> semplice decimale
out = pd.DataFrame({"Date": df[col_date], "ISIN": isin, "Ret": r})
out = pd.DataFrame({
"Date": df[col_date], "ISIN": isin,
"Ret": np.expm1(log_r),
})
else:
continue
frames.append(out)
if not frames:
return pd.DataFrame(index=pd.DatetimeIndex([], name="Date"))
long = pd.concat(frames, ignore_index=True).dropna(subset=["Date"])
mask = (
(long["Date"].dt.date >= start_date)
& (long["Date"].dt.date <= end_date)
)
mask = (long["Date"].dt.date >= start_date) & (long["Date"].dt.date <= end_date)
long = long.loc[mask]
wide = long.pivot(index="Date", columns="ISIN", values="Ret").sort_index()
# Auto-detect percent vs decimal
if not wide.empty:
max_abs = np.nanmax(np.abs(wide.values))
if np.isfinite(max_abs) and max_abs > 0.5:
@@ -322,40 +304,50 @@ def fetch_returns_from_db(isins, start_date, end_date) -> pd.DataFrame:
return wide
# =============================================================================
# RICOSTRUZIONE DAILY RETURNS
# RICOSTRUZIONE DAILY RETURNS PER STRATEGIA
# =============================================================================
def rebuild_daily_from_log(audit: pd.DataFrame, returns_wide: pd.DataFrame) -> pd.DataFrame:
def rebuild_daily_from_log(
audit: pd.DataFrame, returns_wide: pd.DataFrame
) -> pd.DataFrame:
"""
Per ogni strategia ricostruisce il rendimento giornaliero come media
ponderata sui trade attivi: r_strat(d) = sum(amount_i * ret_isin_i(d)) /
sum(amount_i), dove i somma sui trade aperti nel giorno d.
"""
strategies = sorted(audit["Strategy"].dropna().astype(str).unique())
if not strategies:
return pd.DataFrame(index=returns_wide.index, columns=[])
idx = returns_wide.index
daily_num = pd.DataFrame(0.0, index=idx, columns=strategies)
daily_den = pd.DataFrame(0.0, index=idx, columns=strategies)
# Mappa chiusure ISIN+OpenDate -> CloseDate
closes = audit[audit["Action"] == "CLOSE"].copy()
if not closes.empty:
if "LinkedOpenDate" in closes.columns:
closes["_key"] = (
closes["ISIN"].astype(str)
+ "|"
closes["ISIN"].astype(str) + "|"
+ pd.to_datetime(closes["LinkedOpenDate"]).dt.strftime("%Y-%m-%d")
)
else:
closes["_key"] = (
closes["ISIN"].astype(str)
+ "|"
closes["ISIN"].astype(str) + "|"
+ pd.to_datetime(closes["TradeDate"]).dt.strftime("%Y-%m-%d")
)
closes["TradeDate"] = pd.to_datetime(closes["TradeDate"])
closes_agg = closes.sort_values("TradeDate").groupby("_key", as_index=False)["TradeDate"].last()
closes_agg = (
closes.sort_values("TradeDate")
.groupby("_key", as_index=False)["TradeDate"]
.last()
)
close_map = closes_agg.set_index("_key")
else:
close_map = pd.DataFrame().set_index(pd.Index([], name="_key"))
# debug counters
# Counters per debug
total_opens = 0
used_opens = 0
skipped_missing_isin = 0
@@ -363,14 +355,12 @@ def rebuild_daily_from_log(audit: pd.DataFrame, returns_wide: pd.DataFrame) -> p
skipped_bad_window = 0
for strat in strategies:
aud_s = audit[audit["Strategy"] == strat]
opens = aud_s[aud_s["Action"] == "OPEN"].copy()
opens = audit[(audit["Strategy"] == strat) & (audit["Action"] == "OPEN")].copy()
if opens.empty:
continue
opens["_key"] = (
opens["ISIN"].astype(str)
+ "|"
opens["ISIN"].astype(str) + "|"
+ pd.to_datetime(opens["TradeDate"]).dt.strftime("%Y-%m-%d")
)
@@ -397,8 +387,9 @@ def rebuild_daily_from_log(audit: pd.DataFrame, returns_wide: pd.DataFrame) -> p
close_val = close_map.loc[key, "TradeDate"]
if isinstance(close_val, pd.Series):
close_val = close_val.iloc[-1]
d_close = pd.Timestamp(close_val).normalize()
exit_idx = int(ser.index.searchsorted(d_close, side="left"))
exit_idx = int(ser.index.searchsorted(
pd.Timestamp(close_val).normalize(), side="left"
))
else:
exit_idx = len(ser)
@@ -406,9 +397,8 @@ def rebuild_daily_from_log(audit: pd.DataFrame, returns_wide: pd.DataFrame) -> p
skipped_bad_window += 1
continue
idx_seg = ser.index[entry_idx:exit_idx]
idx_seg = ser.index[entry_idx:exit_idx]
vals_seg = ser.values[entry_idx:exit_idx]
daily_num.loc[idx_seg, strat] += entry_amount * vals_seg
daily_den.loc[idx_seg, strat] += entry_amount
used_opens += 1
@@ -417,11 +407,12 @@ def rebuild_daily_from_log(audit: pd.DataFrame, returns_wide: pd.DataFrame) -> p
mask = daily_den > 0
daily[mask] = daily_num[mask] / daily_den[mask]
# Salva debug
debug = pd.concat(
{f"num_{c}": daily_num[c] for c in strategies}
| {f"den_{c}": daily_den[c] for c in strategies}
| {f"ret_{c}": daily[c] for c in strategies},
axis=1
| {f"ret_{c}": daily[c] for c in strategies},
axis=1,
)
debug.to_csv(OUT_DEBUG_CSV, index_label="Date")
@@ -431,56 +422,14 @@ def rebuild_daily_from_log(audit: pd.DataFrame, returns_wide: pd.DataFrame) -> p
f"EntryAmount<=0: {skipped_bad_amount}, "
f"finestra non valida: {skipped_bad_window}"
)
return daily
# =============================================================================
# MAIN
# PLOT
# =============================================================================
def main():
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
PLOT_DIR.mkdir(parents=True, exist_ok=True)
if not AUDIT_LOG_CSV.exists():
raise FileNotFoundError("Missing trades_audit_log.csv")
# parsing robusto con controllo formato
audit = load_audit_log(AUDIT_LOG_CSV)
if audit.empty:
raise SystemExit("Audit log vuoto.")
if "Strategy" not in audit.columns:
raise SystemExit("Colonna 'Strategy' mancante nell'audit log.")
# === filtro whitelist: solo strategie volute ===
audit["Strategy"] = audit["Strategy"].astype(str)
before = len(audit)
audit = audit[audit["Strategy"].isin(VALID_STRATEGIES)]
removed = before - len(audit)
if removed > 0:
print(
f"[INFO] Filtrate {removed} righe con strategie non incluse in {VALID_STRATEGIES}."
)
if audit.empty:
raise SystemExit(f"Nessuna riga con strategie in {VALID_STRATEGIES} nell'audit log.")
start_date = (audit["TradeDate"].min() - pd.Timedelta(days=10)).date()
end_date = (audit["TradeDate"].max() + pd.Timedelta(days=10)).date()
isins = sorted(audit["ISIN"].dropna().astype(str).unique())
ret_wide = fetch_returns_from_db(isins, start_date, end_date)
if ret_wide.empty:
raise RuntimeError("Nessun rendimento recuperato dal DB nell'intervallo richiesto.")
daily = rebuild_daily_from_log(audit, ret_wide).sort_index()
daily.to_csv(OUT_DAILY_CSV, index_label="Date")
equity = (1.0 + daily.fillna(0.0)).cumprod() * 100.0
equity.to_csv(OUT_EQUITY_CSV, index_label="Date")
def _plot_equity(equity: pd.DataFrame, out_path: Path) -> None:
import matplotlib.pyplot as plt
# Equity
plt.figure(figsize=(10, 6))
for col in equity.columns:
plt.plot(equity.index, equity[col], label=col)
@@ -488,11 +437,12 @@ def main():
plt.grid(True)
plt.legend()
plt.tight_layout()
plt.savefig(str(PLOT_EQUITY), dpi=150)
plt.savefig(str(out_path), dpi=150)
plt.close()
copy_to_dropbox(PLOT_EQUITY)
# Drawdown
def _plot_drawdown(equity: pd.DataFrame, out_path: Path) -> None:
import matplotlib.pyplot as plt
dd = equity / equity.cummax() - 1.0
plt.figure(figsize=(10, 5))
for col in dd.columns:
@@ -501,16 +451,66 @@ def main():
plt.grid(True)
plt.legend()
plt.tight_layout()
plt.savefig(str(PLOT_DD), dpi=150)
plt.savefig(str(out_path), dpi=150)
plt.close()
# =============================================================================
# MAIN
# =============================================================================
def main() -> None:
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
PLOT_DIR.mkdir(parents=True, exist_ok=True)
if not AUDIT_LOG_CSV.exists():
raise FileNotFoundError("Missing trades_audit_log.csv")
audit = load_audit_log(AUDIT_LOG_CSV)
if audit.empty:
raise SystemExit("Audit log vuoto.")
if "Strategy" not in audit.columns:
raise SystemExit("Colonna 'Strategy' mancante nell'audit log.")
# Filtro whitelist
audit["Strategy"] = audit["Strategy"].astype(str)
before = len(audit)
audit = audit[audit["Strategy"].isin(VALID_STRATEGIES)]
if (removed := before - len(audit)) > 0:
print(
f"[INFO] Filtrate {removed} righe con strategie non incluse "
f"in {VALID_STRATEGIES}."
)
if audit.empty:
raise SystemExit(
f"Nessuna riga con strategie in {VALID_STRATEGIES} nell'audit log."
)
start_date = (audit["TradeDate"].min() - pd.Timedelta(days=10)).date()
end_date = (audit["TradeDate"].max() + pd.Timedelta(days=10)).date()
isins = sorted(audit["ISIN"].dropna().astype(str).unique())
ret_wide = fetch_returns_from_db(isins, start_date, end_date)
if ret_wide.empty:
raise RuntimeError("Nessun rendimento recuperato dal DB.")
# Daily returns + equity
daily = rebuild_daily_from_log(audit, ret_wide).sort_index()
daily.to_csv(OUT_DAILY_CSV, index_label="Date")
equity = (1.0 + daily.fillna(0.0)).cumprod() * 100.0
equity.to_csv(OUT_EQUITY_CSV, index_label="Date")
# Plots
_plot_equity(equity, PLOT_EQUITY)
_plot_drawdown(equity, PLOT_DD)
# Dropbox export
copy_to_dropbox(PLOT_EQUITY)
copy_to_dropbox(PLOT_DD)
print("Salvati:")
print(" -", OUT_DAILY_CSV)
print(" -", OUT_EQUITY_CSV)
print(" -", OUT_DEBUG_CSV)
print(" -", PLOT_EQUITY)
print(" -", PLOT_DD)
for path in [OUT_DAILY_CSV, OUT_EQUITY_CSV, OUT_DEBUG_CSV, PLOT_EQUITY, PLOT_DD]:
print(" -", path)
print(" -", DROPBOX_EXPORT_DIR / PLOT_EQUITY.name)
print(" -", DROPBOX_EXPORT_DIR / PLOT_DD.name)