Files
PythagorasGoal/Old/scripts/analysis/daily_equity_bfill_impact.py
T
Adriano Dal Pastro 14522262e6 chore(reset): v2.0.0 — storico certificato Deribit mainnet, ripartenza pulita
Reset del progetto su fondamenta verificate dopo la scoperta che l'intera
libreria "validata OOS" era artefatto di feed contaminato (print fantasma del
feed Cerbero TESTNET + storico Binance/USDT).

- Storico ricostruito da Deribit MAINNET (ccxt pubblico, tokenless) e
  CERTIFICATO (certify_feed.py): BTC/ETH puliti su TUTTA la storia
  (mediana 2-6 bps vs Coinbase USD), integrita' OHLC + coerenza resample
  (maxΔ 0.00) + cross-venue OK. Alt esclusi (illiquidi/divergenti: LTC/DOGE
  50-82% barre flat; XRP/BNB non certificabili).
- Verdetto sul feed pulito: FADE / PAIRS / XS01 / TSM01 morti (ogni
  portafoglio Sharpe -2.3..-3.0, DD ~40%); solo SH01 e frammenti HONEST
  con segnale residuo, da ri-validare in isolamento.
- Cleanup "restart pulito": strategie, stack live (src/live, src/portfolio,
  runner/executor, yml, docker), ~100 script ricerca/gate, waste/games/
  portfolios, dati non certificati + cache e 60+ diari -> archiviati in Old/
  (preservati, non cancellati). Diario consolidato in un unico documento.
- Skeleton ricerca tenuto: Strategy ABC + indicatori + src/fractal +
  src/backtest/engine + load_data; tool dati certificati (rebuild_history,
  certify_feed, audit_feed, multi_source_check).
- Universo dati ATTIVO: solo BTC/ETH (5m/15m/1h); guardrail fisico
  (load_data su alt -> FileNotFoundError). Esecuzione DISABILITATA, conto flat.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 15:20:59 +00:00

210 lines
9.8 KiB
Python

"""ANALISI DI IMPATTO (sola lettura, da docs/TODO.md): bug bfill di `_daily_equity`.
IL BUG (scripts/analysis/honest_improve2.py:30):
daily = s.resample("1D").last().reindex(idx).ffill().bfill()
La serie `s` e' a PUNTI-TRADE (un valore di capitale per ogni exit). Il `reindex(idx)`
taglia PRIMA di forward-fillare: i giorni di IDX precedenti al primo trade DENTRO la
finestra restano NaN (il ffill non ha un valore precedente in-finestra da propagare) e
il `.bfill()` finale li riempie col capitale DOPO il primo trade in-finestra. Effetti:
1. l'ancora a idx[0] e' il capitale post-primo-trade-in-finestra, NON il capitale
portato avanti dall'ultimo trade PRIMA della finestra;
2. il rendimento del primo trade in-finestra viene CANCELLATO dalla serie daily
(la testa e' piatta al valore post-trade -> pct_change = 0 anche il giorno del trade).
CORREZIONE (qui, solo per confronto): ffill PRIMA del reindex (carry-forward su tutta la
storia trade) + testa pre-primo-trade-assoluto = capitale iniziale 1000. MAI valori dal futuro.
Sleeve canonici interessati (serie a punti-trade -> testa di IDX scoperta):
DIP01_BTC, PR_ETHBTC, PR_ETHBTC_15M, PR_LTCETH, PR_ADAETH, PR_BTCLTC, PR_ETHSOL,
TSM01, XS01 (questi due quasi-densi: punti daily/12h -> impatto atteso ~0).
TR01_basket / ROT02_rot passano da _daily_equity ma con punti PER-BARRA (densi dal
2018) -> verificati comunque qui via monkeypatch runtime (nessun file canonico toccato).
I fade (combine_portfolio.py:52) e SH01 (shape_ml_validate.py:124) usano lo stesso
pattern reindex+bfill ma su equity PER-BARRA con dati che iniziano prima di IDX[0]
-> il bfill e' un no-op (verificato: nessun NaN in testa).
NB: le metriche OOS canoniche affettano la STESSA serie daily a SPLIT (metrics(dr,
lo=SPLIT)); la distorsione sta solo in testa (2021) -> l'OOS e' invariato per
costruzione se il primo trade in-finestra precede lo SPLIT. Questo script lo misura.
Uso: uv run python scripts/analysis/daily_equity_bfill_impact.py
"""
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))
import scripts.analysis.honest_improve2 as hi2
from scripts.analysis.honest_improve2 import _norm, dip_market_gated
from scripts.analysis.combine_portfolio import IDX, SPLIT, OOS_DATE, metrics, port_returns
from scripts.analysis.pairs_research import pairs_sim, pairs_sim_flat
from scripts.analysis.tsmom_research import tsmom_sim
from scripts.strategies.PR01_pairs_reversion import PAIRS as PAIR_DEFS
from scripts.strategies.XS01_cross_sectional import xsec_sim
from scripts.portfolios._defs import PORTFOLIOS
from src.portfolio import weighting as W
INIT = 1000.0
# ---------------- le due convenzioni ----------------
def daily_equity_buggy(ts_list, cap_list, idx):
"""Replica ESATTA di honest_improve2._daily_equity (per parity-check)."""
s = pd.Series(cap_list, index=pd.to_datetime(ts_list, utc=True))
s = s[~s.index.duplicated(keep="last")].sort_index()
return s.resample("1D").last().reindex(idx).ffill().bfill()
def daily_equity_fixed(ts_list, cap_list, idx, init=INIT):
"""CORRETTA: ancora = capitale portato avanti dall'ultimo trade PRIMA della
finestra (ffill prima del reindex); pre-primo-trade assoluto = capitale iniziale."""
s = pd.Series(cap_list, index=pd.to_datetime(ts_list, utc=True))
s = s[~s.index.duplicated(keep="last")].sort_index()
daily = s.resample("1D").last().ffill() # carry-forward su TUTTA la storia
daily = daily.reindex(idx).ffill() # coda oltre l'ultimo trade
return daily.fillna(init) # testa pre-primo-trade: capitale iniziale
def head_info(ts_list, cap_list, idx):
"""(primo giorno con trade dentro IDX, rendimento di testa perso dal bfill %)."""
s = pd.Series(cap_list, index=pd.to_datetime(ts_list, utc=True))
s = s[~s.index.duplicated(keep="last")].sort_index()
raw = s.resample("1D").last().reindex(idx) # senza fill: NaN = nessun trade quel giorno
first = raw.first_valid_index()
if first is None:
return None, 0.0
fixed = daily_equity_fixed(ts_list, cap_list, idx)
lost = (fixed.loc[first] / fixed.iloc[0] - 1) * 100 # ritorno idx[0]->primo trade-day
return first.date(), float(lost)
def m2(eq: pd.Series):
dr = eq.pct_change().fillna(0.0)
return metrics(dr), metrics(dr, lo=SPLIT)
def fmt_pair(label, b, f):
d_sh = f["sharpe"] - b["sharpe"]
d_dd = f["dd"] - b["dd"]
d_rt = f["ret"] - b["ret"]
return (f" {label:<22s}"
f"Sh {b['sharpe']:6.2f}->{f['sharpe']:6.2f} ({d_sh:+.3f}) "
f"DD {b['dd']:6.2f}->{f['dd']:6.2f} ({d_dd:+.3f}pp) "
f"ret {b['ret']:+9.1f}->{f['ret']:+9.1f} ({d_rt:+8.2f}pp)")
def main():
print("=" * 110)
print(" IMPATTO bug bfill _daily_equity (honest_improve2.py:30) — attuale vs corretto")
print(f" IDX {IDX[0].date()} -> {IDX[-1].date()} | OOS da {OOS_DATE} (slice a SPLIT={SPLIT} sui rendimenti daily)")
print("=" * 110)
# ---------------- [1] baseline canonica (bfill cosi' com'e') ----------------
print("\n[1] build_everything() canonico (2-3 min)...")
from scripts.analysis.report_families import build_everything
S, pairs, tsm, shape = build_everything()
base = {**S, **pairs, **tsm, **shape}
# ---------------- [2] ri-simula gli sleeve a punti-trade ----------------
print("[2] ri-simulazione sleeve a punti-trade (parity-check + versione corretta)...")
raw: dict[str, tuple] = {}
d = dip_market_gated("BTC", market_n=0, return_equity=True)
raw["DIP01_BTC"] = (d["eq_ts"], d["eq_v"])
for a, b_, p in PAIR_DEFS:
r = pairs_sim(a, b_, **p)
raw[f"PR_{a}{b_}"] = (r["eq_ts"], r["eq_v"])
r15 = pairs_sim_flat("ETH", "BTC", tf="15m", n=66, z_in=1.674, z_exit=1.0,
max_bars=35, flat_skip=True, pos=0.075)
raw["PR_ETHBTC_15M"] = (r15["eq_ts"], r15["eq_v"])
t = tsmom_sim()
raw["TSM01"] = (t["eq_ts"], t["eq_v"])
x = xsec_sim()
raw["XS01"] = (x["eq_ts"], x["eq_v"])
fixed: dict[str, pd.Series] = {}
print(f"\n {'sleeve':<16s}{'parity(max|diff|)':>18s}{'1o trade in IDX':>17s}{'ret testa perso%':>18s}")
for k, (ts, v) in raw.items():
bug = _norm(daily_equity_buggy(ts, v, IDX))
par = float((bug - base[k]).abs().max())
fixed[k] = _norm(daily_equity_fixed(ts, v, IDX))
first, lost = head_info(ts, v, IDX)
flag = "" if par < 1e-9 else " <-- PARITY FAIL"
print(f" {k:<16s}{par:>18.2e}{str(first):>17s}{lost:>+18.3f}{flag}")
# TR01/ROT02: passano da _daily_equity ma con punti per-barra (densi) ->
# ricalcolo con monkeypatch RUNTIME della funzione (nessun file toccato).
orig_de = hi2._daily_equity
try:
hi2._daily_equity = daily_equity_fixed
tr_f = _norm(hi2._tr_basket_daily(["BNB", "BTC", "DOGE", "SOL", "XRP"], IDX))
rot_f = _norm(hi2._rot_daily_equity(IDX))
finally:
hi2._daily_equity = orig_de
for k, sf in (("TR01_basket", tr_f), ("ROT02_rot", rot_f)):
diff = float((sf - base[k]).abs().max())
print(f" {k:<16s}{'(denso)':>18s}{'—':>17s}{diff:>18.2e} (diff fixed-vs-base: atteso ~0)")
fixed[k] = sf
# ---------------- [3] metriche per sleeve: attuale vs corretto ----------------
print("\n" + "=" * 110)
print(" (3) SLEEVE a punti-trade — FULL e OOS, attuale(bfill) -> corretto(carry-forward)")
print("=" * 110)
rows_oos_delta = {}
for k in fixed:
bf, bo = m2(base[k])
ff, fo = m2(fixed[k])
print(fmt_pair(f"{k} FULL", bf, ff))
print(fmt_pair(f"{k} OOS ", bo, fo))
rows_oos_delta[k] = (ff["sharpe"] - bf["sharpe"], ff["dd"] - bf["dd"],
fo["sharpe"] - bo["sharpe"], fo["dd"] - bo["dd"])
# ---------------- [4] PORT06: attuale vs corretto ----------------
print("\n" + "=" * 110)
print(" (4) PORT06 (cap PAIRS 0.33 + SHAPE 0.0588) — attuale vs corretto")
print("=" * 110)
p = PORTFOLIOS["PORT06"]
def port_m(members):
ids = p.sleeve_ids
dr = pd.DataFrame({i: members[i].pct_change().fillna(0.0) for i in ids})
w = W.weight_vector(p.weighting, ids, dr, weights=p.weights,
caps=p.caps, clusters=p.clusters, lookback=p.vol_lookback)
drp = port_returns({i: members[i] for i in ids}, w)
return metrics(drp), metrics(drp, lo=SPLIT)
members_fix = {**base, **fixed}
bf, bo = port_m(base)
ff, fo = port_m(members_fix)
print(fmt_pair("PORT06 FULL", bf, ff))
print(fmt_pair("PORT06 OOS ", bo, fo))
# ---------------- [5] verdetto ----------------
print("\n" + "=" * 110)
print(" (5) VERDETTO (soglie materialita': >0.1 Sharpe o >0.5pp DD su PORT06 OOS)")
print("=" * 110)
d_sh_oos = abs(fo["sharpe"] - bo["sharpe"])
d_dd_oos = abs(fo["dd"] - bo["dd"])
d_sh_full = abs(ff["sharpe"] - bf["sharpe"])
d_dd_full = abs(ff["dd"] - bf["dd"])
materiale = d_sh_oos > 0.1 or d_dd_oos > 0.5
print(f" PORT06 OOS : dSharpe {fo['sharpe']-bo['sharpe']:+.4f} dDD {fo['dd']-bo['dd']:+.4f}pp"
f" -> {'MATERIALE' if materiale else 'NON materiale'}")
print(f" PORT06 FULL: dSharpe {ff['sharpe']-bf['sharpe']:+.4f} dDD {ff['dd']-bf['dd']:+.4f}pp")
worst = sorted(rows_oos_delta.items(), key=lambda kv: -abs(kv[1][0]) - abs(kv[1][1]) / 10)
print(" Sleeve piu' toccati (dSharpe FULL, dDD FULL, dSharpe OOS, dDD OOS):")
for k, (ds, dd_, dso, ddo) in worst[:5]:
print(f" {k:<16s} FULL {ds:+.3f} / {dd_:+.3f}pp OOS {dso:+.3f} / {ddo:+.3f}pp")
print("\n Nota strutturale: l'OOS canonico e' uno slice a SPLIT della stessa serie daily;")
print(" la distorsione bfill vive solo in testa (prima del primo trade in IDX) -> se il")
print(" primo trade in-finestra precede lo SPLIT, l'OOS e' INVARIATO per costruzione.")
if __name__ == "__main__":
main()