From 666c90690796542995e9ac5967133009657b859b Mon Sep 17 00:00:00 2001 From: AdrianoDev Date: Fri, 29 May 2026 00:33:00 +0200 Subject: [PATCH] feat(analysis): report.py aggiornato con numero trade per anno Report consolidato: (A) Ret% netto per anno di ogni strategia singola + portafogli (FADE/HONEST/MASTER eq/5050), (B) numero trade per anno per strategia (ingressi per fade/DIP01; ribilanciamenti per TR01/ROT02 a posizione continua), (C) riepilogo portafogli FULL/OOS. Config deployata (MR03/ROT01 in waste, ROT02 top_k=3). Co-Authored-By: Claude Opus 4.8 (1M context) --- scripts/analysis/report.py | 136 +++++++++++++++++++++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 scripts/analysis/report.py diff --git a/scripts/analysis/report.py b/scripts/analysis/report.py new file mode 100644 index 0000000..26110b5 --- /dev/null +++ b/scripts/analysis/report.py @@ -0,0 +1,136 @@ +"""Report aggiornato: risultati per anno + numero trade per anno, tutte le strategie. + +Sezioni: + (A) RET% NETTO per anno — ogni strategia singola + i portafogli (FADE / HONEST / + MASTER equal / MASTER 50-50). Ret% dai rendimenti giornalieri composti. + (B) NUMERO TRADE per anno — per ogni strategia singola. Per le fade e DIP01 è il + numero di ingressi; per TR01 e ROT02 (posizione continua) è il numero di + ribilanciamenti/cambi di stato nell'anno. + (C) RIEPILOGO — TOT%, CAGR, DD, Sharpe (FULL e OOS) dei portafogli. + +Tutto NETTO fee 0.10% RT, leva 3x, pos 15%/sleeve. Finestra comune 2021-2026, +OOS = ultimo 30%. Config = quella deployata (MR03/ROT01 in waste; ROT02 top_k=3). +""" +from __future__ import annotations + +import sys +from pathlib import Path + +import numpy as np +import pandas as pd + +PROJECT_ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(PROJECT_ROOT)) + +from src.data.downloader import load_data +from scripts.analysis.combine_portfolio import ( + build_all_sleeves, port_returns, yearly_returns, metrics, SPLIT, OOS_DATE, IDX, +) +from scripts.analysis.risk_management import strats_for, build_trades +from scripts.analysis.honest_lab import get_df, ema, FEE_RT, LEV, POS +from scripts.analysis.honest_improve import rot_improved +from scripts.analysis.honest_improve2 import dip_market_gated + +YEARS = sorted(set(IDX.year)) + + +# ---------------- trade per anno, per tipo di strategia ---------------- +def fade_trades_year(asset, fn, params) -> dict[int, int]: + df = load_data(asset, "1h") + ts = pd.to_datetime(df["timestamp"], unit="ms", utc=True) + out: dict[int, int] = {} + for i, j, ret in build_trades(fn(df, **params), df, trend_max=3.0): + y = ts.iloc[i].year + out[y] = out.get(y, 0) + 1 + return out + + +def dip_trades_year() -> dict[int, int]: + d = dip_market_gated("BTC", market_n=0) + # yt[anno] = lista dei trade dell'anno -> il conteggio e' la lunghezza + return {int(y): (len(v) if isinstance(v, (list, tuple)) else int(v)) for y, v in d["yt"].items()} + + +def tr_rebalances_year(assets) -> dict[int, int]: + """Cambi di stato (entra/esce dal trend) per anno, sommati sul paniere TR01.""" + out: dict[int, int] = {} + for a in assets: + df = get_df(a, "4h"); c = df["close"].values + ts = pd.to_datetime(df["timestamp"], unit="ms", utc=True) + ef, es = ema(c, 20), ema(c, 100) + sig = np.where(ef > es, 1.0, 0.0); sig[:100] = 0.0 + for i in range(1, len(c)): + if sig[i] != sig[i - 1]: + y = ts.iloc[i].year + out[y] = out.get(y, 0) + 1 + return out + + +def rot_rebalances_year() -> dict[int, int]: + r = rot_improved(lookback=60, top_k=3, regime_n=100) + return {int(y): int(n) for y, n in r["reb"].items()} + + +def main(): + print("Costruzione equity e conteggi (puo' richiedere ~1 min)...\n") + S = build_all_sleeves() + fade = {k: v for k, v in S.items() if k.startswith("MR")} + honest = {k: v for k, v in S.items() if not k.startswith("MR")} + + # rendimenti giornalieri per Ret%/anno + sleeve_ret = {k: v.pct_change().fillna(0.0) for k, v in S.items()} + ports = { + "FADE": port_returns(fade), + "HONEST": port_returns(honest), + "MASTEReq": port_returns(S), + "MAST5050": (port_returns(fade) + port_returns(honest)) / 2, + } + + # ---- (A) RET% per anno ---- + cols_A = list(S) + list(ports) + rety = {**{k: yearly_returns(v) for k, v in sleeve_ret.items()}, + **{k: yearly_returns(v) for k, v in ports.items()}} + print("=" * 132) + print(" (A) RET% NETTO PER ANNO — strategie singole e portafogli | leva 3x pos 15% fee 0.10% RT") + print("=" * 132) + print(f" {'Anno':>5s}" + "".join(f"{c.replace('_',''):>11s}" for c in cols_A)) + print(" " + "-" * 126) + for y in YEARS: + print(f" {y:>5d}" + "".join(f"{rety[c].get(y, 0):>+11.0f}" for c in cols_A)) + + # ---- (B) NUMERO TRADE per anno ---- + tcounts = {} + for asset in ["BTC", "ETH"]: + for nm, (fn, params) in strats_for(asset).items(): + tcounts[f"{nm}_{asset}"] = fade_trades_year(asset, fn, params) + tcounts["DIP01_BTC"] = dip_trades_year() + tcounts["TR01_basket*"] = tr_rebalances_year(["BNB", "BTC", "DOGE", "SOL", "XRP"]) + tcounts["ROT02_rot*"] = rot_rebalances_year() + cols_B = list(tcounts) + print("\n" + "=" * 132) + print(" (B) NUMERO TRADE PER ANNO — fade/DIP01 = ingressi; TR01/ROT02 (*) = ribilanciamenti") + print("=" * 132) + print(f" {'Anno':>5s}" + "".join(f"{c.replace('_',''):>13s}" for c in cols_B)) + print(" " + "-" * 126) + for y in YEARS: + print(f" {y:>5d}" + "".join(f"{tcounts[c].get(y, 0):>13d}" for c in cols_B)) + print(" " + "-" * 126) + print(f" {'TOT':>5s}" + "".join(f"{sum(tcounts[c].values()):>13d}" for c in cols_B)) + + # ---- (C) riepilogo portafogli ---- + print("\n" + "=" * 92) + print(f" (C) RIEPILOGO PORTAFOGLI | OOS da {OOS_DATE}") + print("=" * 92) + print(f" {'portafoglio':<14s}{'Ret%':>9s}{'CAGR':>7s}{'DD%':>7s}{'Shrp':>7s}" + f" | {'oRet%':>9s}{'oDD%':>7s}{'oShrp':>7s}") + print(" " + "-" * 74) + for name, pr in ports.items(): + f, o = metrics(pr), metrics(pr, lo=SPLIT) + print(f" {name:<14s}{f['ret']:>+9.0f}{f['cagr']:>7.0f}{f['dd']:>7.1f}{f['sharpe']:>7.2f}" + f" | {o['ret']:>+9.0f}{o['dd']:>7.1f}{o['sharpe']:>7.2f}") + print("\n MASTEReq (9 sleeve) = configurazione consigliata. (*) TR01/ROT02 = posizione") + print(" continua: il conteggio e' il numero di ribilanciamenti/cambi di stato, non di trade discreti.") + + +if __name__ == "__main__": + main()