491411ac77
Ondata onesta su angoli non coperti: funding-TS (chiude il filone funding su 3 lati), breadth alt (non-ridondante ma DSR 0.43, rivisitabile con storia), XS-residmom (REDUNDANT), pesi+guardia-DD (EW-STR refutato dallo scettico come selezione-sull'hold-out di 2° ordine, firma best-of-15), VRP-refine (filone esaurito), stagionalità-XS (morta allo step statistico). Lezione codificata: weights_tilt_null + combine_outer in src/portfolio (ogni cambio-pesi vs null di tilt casuali cap-respecting + delta in-sample>=0); 5 test nuovi, suite 165/165. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
388 lines
21 KiB
Python
388 lines
21 KiB
Python
"""r0701_funding_ts — FUNDING RATE come segnale TIME-SERIES direzionale su BTC/ETH (NON carry).
|
|
|
|
2026-07-01. Ipotesi: il funding orario Hyperliquid (proxy di POSIZIONAMENTO/sentiment dei perp)
|
|
contiene informazione direzionale a orizzonte giornaliero su BTC/ETH. Famiglia (griglia modesta):
|
|
- FADE : z-score del funding estremo-positivo = affollamento long -> SHORT (e viceversa)
|
|
- FOLLOW : funding in espansione = domanda long persistente -> LONG (sentiment momentum)
|
|
- GATE : trend TP01-like long-flat, FLAT quando il funding e' affollato (z>=thr, de-risk)
|
|
- DIVERGE : momentum prezzo 20d con funding NON affollato -> follow; affollato -> fade
|
|
Griglia: 4 forme x lookback z {7,14,30,60}g x soglia {0.5,1.0,1.5} = 48 celle, solo 1d.
|
|
|
|
PRIOR ART (non ripetuto): FC01 carry cross-sectional delta-neutral -> SCARTATO
|
|
(docs/diary/2026-06-22-funding-carry-hl.md); funding price-clock intraday -> FAIL (onda intraday).
|
|
Qui il funding e' un SEGNALE time-series direzionale su BTC/ETH perp (2 gambe, eseguibile a ~$600),
|
|
non un cashflow da incassare.
|
|
|
|
DATI: data/raw/hlfund_{btc,eth}_1h.parquet (funding orario HL: 2023-05-12 -> 2026-06-22; primi
|
|
~27 giorni a cadenza 8h, poi oraria, 0 gap; certificato nel diario 2026-06-22). Prezzi certificati
|
|
Deribit via altlib.get (1d resampled leak-free).
|
|
|
|
CAUSALITA' (il punto delicato): le barre 1d sono OPEN-LABELED (datetime = 00:00 UTC del giorno D;
|
|
il close della barra D e' noto alle 00:00 di D+1). Il feature-day D aggrega i SOLI stamp funding
|
|
in [D 00:00, D+24h) — l'ultimo alle 23:00 — quindi tutto e' noto PRIMA della decisione al close
|
|
della barra D. eval_weights poi shifta: target[D] e' tenuto durante la barra D+1. Nessun leak
|
|
strutturale; in piu' prefix-check esplicito.
|
|
|
|
VALUTAZIONE su finestra TRONCATA alla copertura funding (2023-05-12..2026-06-21), NON sul frame
|
|
prezzi 2018+: fuori copertura il target sarebbe zero per costruzione e i giorni-zero (a) GONFIANO
|
|
il T del deflated-Sharpe (anti-conservativo) e (b) DILUISCONO cand_insample_sharpe (gate
|
|
has_insample_edge scatterebbe a vuoto). La logica di study_family_honest e' replicata ESATTAMENTE
|
|
sui frame troncati coi primitivi altlib: selezione cella IN-SAMPLE-ONLY (mai sul hold-out) ->
|
|
study_marginal gates (ADDS + robust_oos + has_insample_edge + not is_hedge) -> deflated-Sharpe
|
|
>= 0.95 sull'INTERA griglia. Cross-check con study_marginal non-troncato riportato in coda.
|
|
|
|
CAVEAT STORIA: funding solo dal 2023-05 (~3.1 anni). In-sample pre-HOLDOUT ~1.6 anni (meno il
|
|
warmup z), hold-out 2025-01+ ~1.5 anni. Finestra corta: qualunque PASS andrebbe comunque in
|
|
forward-monitor, e un FAIL su questa finestra non e' appellabile a "regime sfortunato".
|
|
|
|
Run: cd /opt/docker/PythagorasGoal && uv run python scripts/research/r0701_funding_ts.py
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import sys
|
|
from functools import lru_cache
|
|
from pathlib import Path
|
|
|
|
import numpy as np
|
|
import pandas as pd
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parent / "alt"))
|
|
import altlib as al # noqa: E402
|
|
|
|
ASSETS = ("BTC", "ETH")
|
|
FORMS = ("fade", "follow", "gate", "diverge")
|
|
LOOKBACKS = (7, 14, 30, 60)
|
|
THRESHOLDS = (0.5, 1.0, 1.5)
|
|
EARLY_8H_END = pd.Timestamp("2023-06-11", tz="UTC") # fino a qui cadenza 8h (3 stamp/giorno)
|
|
|
|
|
|
# ===========================================================================
|
|
# DATI FUNDING — aggregazione giornaliera causale
|
|
# ===========================================================================
|
|
@lru_cache(maxsize=16)
|
|
def daily_funding(asset: str, back_h: int = 0) -> pd.DataFrame:
|
|
"""Funding giornaliero = SOMMA degli stamp orari nella finestra [D-back_h, D+24h-back_h).
|
|
back_h=0 (default) = giorno UTC pieno [D, D+24h): tutti gli stamp (ultimo 23:00) sono noti
|
|
al close della barra open-labeled D (= 00:00 di D+1) -> causale. back_h>0 sposta la finestra
|
|
INDIETRO (sempre causale) — usato solo dal boundary-shift check.
|
|
'valid' = giorno con copertura piena (>=20 stamp orari; >=3 nell'era 8h iniziale)."""
|
|
p = al.DATA_DIR / f"hlfund_{asset.lower()}_1h.parquet"
|
|
d = pd.read_parquet(p)
|
|
idx = pd.DatetimeIndex(pd.to_datetime(d.index, utc=True)) + pd.Timedelta(hours=back_h)
|
|
day = idx.floor("1D")
|
|
g = pd.Series(d["funding"].values.astype(float), index=day).groupby(level=0)
|
|
out = pd.DataFrame({"fday": g.sum(), "n": g.count()})
|
|
early = out.index <= EARLY_8H_END
|
|
out["valid"] = np.where(early, out["n"] >= 3, out["n"] >= 20)
|
|
return out
|
|
|
|
|
|
@lru_cache(maxsize=1)
|
|
def fund_window() -> tuple:
|
|
"""Intersezione BTC/ETH dei giorni funding validi (a back_h=0)."""
|
|
los, his = [], []
|
|
for a in ASSETS:
|
|
v = daily_funding(a)
|
|
vd = v.index[v["valid"].values]
|
|
los.append(vd.min()); his.append(vd.max())
|
|
return max(los), min(his)
|
|
|
|
|
|
@lru_cache(maxsize=8)
|
|
def get_trunc(asset: str, tf: str = "1d") -> pd.DataFrame:
|
|
"""Prezzi certificati troncati alla copertura funding (vedi docstring modulo)."""
|
|
lo, hi = fund_window()
|
|
df = al.get(asset, tf)
|
|
day = pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True)).floor("1D")
|
|
m = (day >= lo) & (day <= hi)
|
|
return df.loc[m].reset_index(drop=True)
|
|
|
|
|
|
def aligned_fday(df: pd.DataFrame, asset: str, back_h: int = 0) -> np.ndarray:
|
|
"""Funding giornaliero allineato alle barre di df (NaN dove manca/incompleto)."""
|
|
fd = daily_funding(asset, back_h)
|
|
day = pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True)).floor("1D")
|
|
return fd["fday"].where(fd["valid"]).reindex(day).values.astype(float)
|
|
|
|
|
|
# ===========================================================================
|
|
# FAMIGLIA DI SEGNALI — factory(tf, form, lb, thr) -> target_fn(df, asset)
|
|
# ===========================================================================
|
|
def make_target(tf: str = "1d", form: str = "fade", lb: int = 30, thr: float = 1.0,
|
|
back_h: int = 0):
|
|
def target(df: pd.DataFrame, asset: str) -> np.ndarray:
|
|
f = aligned_fday(df, asset, back_h)
|
|
z = al.zscore(f, lb) # causale: rolling fino a i incluso
|
|
c = pd.Series(df["close"].values.astype(float))
|
|
if form == "fade": # affollamento long -> short (e viceversa)
|
|
d = np.where(z >= thr, -1.0, np.where(z <= -thr, 1.0, 0.0))
|
|
elif form == "follow": # funding come sentiment momentum
|
|
d = np.where(z >= thr, 1.0, np.where(z <= -thr, -1.0, 0.0))
|
|
elif form == "gate": # trend long-flat, flat se affollato
|
|
m30 = np.nan_to_num(np.sign(c.pct_change(30).values))
|
|
m90 = np.nan_to_num(np.sign(c.pct_change(90).values))
|
|
trend = ((m30 + m90) > 0).astype(float)
|
|
zz = np.where(np.isfinite(z), z, np.inf) # z ignoto -> conservativo: flat
|
|
d = trend * (zz < thr).astype(float)
|
|
elif form == "diverge": # mossa non affollata -> follow; affollata -> fade
|
|
mom = np.nan_to_num(np.sign(c.pct_change(20).values))
|
|
d = np.where(z >= thr, -mom, np.where(z <= -thr, mom, 0.0))
|
|
else:
|
|
raise ValueError(form)
|
|
return al.vol_target(np.nan_to_num(d), df, 0.20, 30, 2.0)
|
|
return target
|
|
|
|
|
|
# ===========================================================================
|
|
# VALUTAZIONE (replica study_family_honest su frame troncati)
|
|
# ===========================================================================
|
|
def cell_daily(target_fn, fee_side: float = al.FEE_SIDE) -> pd.Series:
|
|
"""Serie daily netta 50/50 BTC+ETH del candidato (convenzione candidate_daily)."""
|
|
series = {}
|
|
for a in ASSETS:
|
|
df = get_trunc(a)
|
|
ev = al.eval_weights(df, target_fn(df, a), fee_side=fee_side)
|
|
series[a] = pd.Series(ev["net"], index=ev["idx"])
|
|
J = pd.concat(series, axis=1, join="inner").fillna(0.0)
|
|
return al._to_daily(0.5 * J["BTC"] + 0.5 * J["ETH"])
|
|
|
|
|
|
def scan_family() -> list[dict]:
|
|
rows = []
|
|
for form in FORMS:
|
|
for lb in LOOKBACKS:
|
|
for thr in THRESHOLDS:
|
|
daily = cell_daily(make_target(form=form, lb=lb, thr=thr))
|
|
ins = daily[daily.index < al.HOLDOUT]
|
|
hold = daily[daily.index >= al.HOLDOUT]
|
|
rows.append(dict(
|
|
form=form, lb=lb, thr=thr,
|
|
insample_sharpe=round(al._sh(ins), 3) if len(ins) > 60 else float("nan"),
|
|
full_sharpe=round(al._sh(daily), 3),
|
|
hold_sharpe=round(al._sh(hold), 3) if len(hold) > 60 else float("nan")))
|
|
return rows
|
|
|
|
|
|
def absolute_study(target_fn, name: str) -> dict:
|
|
"""study_weights-equivalente sui frame troncati (fee sweep 0.00-0.30% RT incluso)."""
|
|
per_asset = {}
|
|
fee_ok_all = True
|
|
for a in ASSETS:
|
|
df = get_trunc(a)
|
|
tgt = target_fn(df, a)
|
|
base = al.eval_weights(df, tgt, fee_side=al.FEE_SIDE)
|
|
sweep = {f"{2 * f * 100:.2f}%RT": al.eval_weights(df, tgt, fee_side=f)["full"]["sharpe"]
|
|
for f in al.FEE_SWEEP}
|
|
fee_ok_all = fee_ok_all and sweep.get("0.20%RT", -9) > 0
|
|
per_asset[a] = dict(full=base["full"], holdout=base["holdout"],
|
|
tim=base["time_in_market"], turnover=base["turnover_per_year"],
|
|
fee_sweep=sweep, yearly=base["yearly"])
|
|
cells = [dict(tf="1d", per_asset=per_asset,
|
|
min_asset_full_sharpe=round(min(per_asset[a]["full"]["sharpe"] for a in ASSETS), 3),
|
|
min_asset_holdout_sharpe=round(min(per_asset[a]["holdout"].get("sharpe", 0.0) for a in ASSETS), 3),
|
|
full_sharpe=round(float(np.mean([per_asset[a]["full"]["sharpe"] for a in ASSETS])), 3),
|
|
fee_survives=fee_ok_all)]
|
|
return dict(name=name, kind="weights", cells=cells, verdict=al._verdict(cells))
|
|
|
|
|
|
def prefix_check(target_fn, tail: int = 60) -> float:
|
|
"""Consistenza online (guardia look-ahead): il target ricalcolato su un prefisso troncato
|
|
deve coincidere col target(full) sugli stessi indici. Ritorna il max scostamento."""
|
|
worst = 0.0
|
|
for a in ASSETS:
|
|
df = get_trunc(a)
|
|
full = np.nan_to_num(np.asarray(target_fn(df, a), float))
|
|
n = len(df)
|
|
for cut in (int(n * 0.80), int(n * 0.92)):
|
|
sub = df.iloc[:cut].reset_index(drop=True)
|
|
s = np.nan_to_num(np.asarray(target_fn(sub, a), float))
|
|
worst = max(worst, float(np.max(np.abs(s[cut - tail:cut] - full[cut - tail:cut]))))
|
|
return worst
|
|
|
|
|
|
def boundary_check(form: str, lb: int, thr: float, offsets=(0, 3, 6, 9, 12)) -> dict:
|
|
"""Lezione day_boundary: sposto INDIETRO di back_h ore la finestra di aggregazione del
|
|
funding (sempre causale). Un effetto di posizionamento reale non cambia segno."""
|
|
B = al.tp01_baseline_daily()
|
|
out = {}
|
|
for off in offsets:
|
|
daily = cell_daily(make_target(form=form, lb=lb, thr=thr, back_h=off))
|
|
J = pd.concat({"B": B, "C": daily}, axis=1, join="inner").dropna()
|
|
up = al._sh(0.75 * J["B"] + 0.25 * J["C"]) - al._sh(J["B"]) if len(J) > 30 else float("nan")
|
|
out[off] = dict(full_sharpe=round(al._sh(daily), 3), uplift_w25=round(up, 3))
|
|
ups = [v["uplift_w25"] for v in out.values() if np.isfinite(v["uplift_w25"])]
|
|
shs = [v["full_sharpe"] for v in out.values()]
|
|
return dict(per_offset=out,
|
|
sharpe_sign_stable=bool(min(shs) * max(shs) >= 0 or max(map(abs, shs)) < 0.1),
|
|
uplift_spread=round(max(ups) - min(ups), 3) if ups else None)
|
|
|
|
|
|
def trend_only_target(df: pd.DataFrame, asset: str = "") -> np.ndarray:
|
|
"""CONTROLLO DECISIVO (lezione TP01-DVOL-overlay): lo STESSO trend long-flat della forma
|
|
'gate' ma SENZA il gate funding. Se fa uguale/meglio, il funding non aggiunge nulla."""
|
|
c = pd.Series(df["close"].values.astype(float))
|
|
m30 = np.nan_to_num(np.sign(c.pct_change(30).values))
|
|
m90 = np.nan_to_num(np.sign(c.pct_change(90).values))
|
|
trend = ((m30 + m90) > 0).astype(float)
|
|
return al.vol_target(trend, df, 0.20, 30, 2.0)
|
|
|
|
|
|
def smallcap_check(target_fn) -> dict:
|
|
out = {}
|
|
for a in ASSETS:
|
|
df = get_trunc(a)
|
|
sc = al.eval_weights_smallcap(df, target_fn(df, a), capital=600.0, min_order=5.0)
|
|
out[a] = dict(modeled_sh=sc["modeled"]["sharpe"], realistic_sh=sc["realistic"]["sharpe"],
|
|
haircut=sc["sharpe_haircut"], n_trades=sc["n_executed_trades"])
|
|
return out
|
|
|
|
|
|
# ===========================================================================
|
|
def main():
|
|
print("=" * 88)
|
|
print("r0701_funding_ts — funding HL come segnale TS direzionale su BTC/ETH (non carry)")
|
|
print("=" * 88)
|
|
|
|
# --- 1. data-first: qualita'/copertura --------------------------------------------
|
|
lo, hi = fund_window()
|
|
print("\n[1] DATI FUNDING")
|
|
for a in ASSETS:
|
|
fd = daily_funding(a)
|
|
v = fd["valid"]
|
|
ann = fd.loc[v, "fday"].mean() * 365.25 * 100
|
|
print(f" {a}: giorni validi {int(v.sum())}/{len(fd)} "
|
|
f"finestra {fd.index[0].date()} -> {fd.index[-1].date()} "
|
|
f"funding medio {ann:+.1f}%/anno "
|
|
f"std daily {fd.loc[v, 'fday'].std() * 1e4:.2f} bps")
|
|
print(f" finestra comune valida: {lo.date()} -> {hi.date()} "
|
|
f"({(hi - lo).days} giorni, ~{(hi - lo).days / 365.25:.1f} anni)")
|
|
n_ins = (al.HOLDOUT - lo).days
|
|
n_hold = (hi - al.HOLDOUT).days
|
|
print(f" in-sample pre-HOLDOUT ~{n_ins}g ({n_ins / 365.25:.1f}a), "
|
|
f"hold-out ~{n_hold}g ({n_hold / 365.25:.1f}a) <-- STORIA CORTA, caveat")
|
|
|
|
# --- 2. scan famiglia (48 celle, selezione IN-SAMPLE-ONLY) -------------------------
|
|
print("\n[2] SCAN FAMIGLIA (4 forme x lb{7,14,30,60} x thr{0.5,1.0,1.5} = 48 celle, 1d)")
|
|
rows = scan_family()
|
|
valid = [r for r in rows if np.isfinite(r["insample_sharpe"])]
|
|
valid.sort(key=lambda r: r["insample_sharpe"], reverse=True)
|
|
print(f" celle valide {len(valid)}/{len(rows)}; top-8 per Sharpe IN-SAMPLE "
|
|
f"(hold-out mostrato SOLO per trasparenza, mai per selezione):")
|
|
print(f" {'form':8s} {'lb':>3s} {'thr':>4s} {'IS':>7s} {'FULL':>7s} {'HOLD':>7s}")
|
|
for r in valid[:8]:
|
|
print(f" {r['form']:8s} {r['lb']:3d} {r['thr']:4.1f} {r['insample_sharpe']:7.2f} "
|
|
f"{r['full_sharpe']:7.2f} {r['hold_sharpe']:7.2f}")
|
|
per_form = {f: max((r["insample_sharpe"] for r in valid if r["form"] == f), default=float("nan"))
|
|
for f in FORMS}
|
|
print(f" best IS per forma: {per_form}")
|
|
|
|
chosen = valid[0]
|
|
print(f"\n CELLA SCELTA (in-sample-only): {chosen['form']} lb={chosen['lb']} thr={chosen['thr']} "
|
|
f"(IS {chosen['insample_sharpe']}, FULL {chosen['full_sharpe']}, HOLD {chosen['hold_sharpe']})")
|
|
fn = make_target(form=chosen["form"], lb=chosen["lb"], thr=chosen["thr"])
|
|
daily = cell_daily(fn)
|
|
|
|
# --- 3. deflated Sharpe sull'INTERA griglia ----------------------------------------
|
|
all_full = [r["full_sharpe"] for r in rows]
|
|
dsr, sr0 = al.deflated_sharpe(al._sh(daily), all_full, daily)
|
|
dsr_pass = bool(np.isfinite(dsr) and dsr >= 0.95)
|
|
print(f"\n[3] DEFLATED SHARPE (griglia {len(rows)} celle): "
|
|
f"DSR={dsr:.3f} (null-max atteso {sr0:.2f}) PASS(>=0.95)={dsr_pass}")
|
|
|
|
# --- 4. assoluto + marginale (gates study_marginal) --------------------------------
|
|
print("\n[4] ASSOLUTO (frame troncati, fee sweep 0.00-0.30% RT)")
|
|
absolute = absolute_study(fn, f"R0701-FUND-{chosen['form'].upper()}")
|
|
print(al.fmt(absolute))
|
|
c0 = absolute["cells"][0]
|
|
for a in ASSETS:
|
|
pa = c0["per_asset"][a]
|
|
print(f" {a}: TIM={pa['tim']} turnover/anno={pa['turnover']} fee_sweep={pa['fee_sweep']}")
|
|
|
|
print("\n[5] MARGINALE vs TP01 (finestra comune col baseline)")
|
|
marg = al.marginal_vs_tp01(daily)
|
|
abs_grade = absolute["verdict"]["grade"]
|
|
earns_slot = (abs_grade != "FAIL" and marg.get("marginal_verdict") == "ADDS"
|
|
and marg.get("robust_oos", False) and marg.get("beats_noise_null", False)
|
|
and not marg.get("is_hedge", False))
|
|
rep = dict(name=f"R0701-FUND {chosen['form']} lb{chosen['lb']} thr{chosen['thr']}",
|
|
absolute=absolute, marginal=marg, abs_grade=abs_grade,
|
|
marginal_verdict=marg.get("marginal_verdict"), earns_slot=earns_slot)
|
|
print(al.fmt_marginal(rep))
|
|
earns_honest = bool(earns_slot and dsr_pass)
|
|
print(f"\n EARNS_SLOT (marginal) = {earns_slot} EARNS_SLOT_HONEST (con DSR) = {earns_honest}")
|
|
|
|
# --- 5bis. controllo di attribuzione: il funding aggiunge qualcosa al trend nudo? ----
|
|
print("\n[5bis] CONTROLLO DECISIVO — trend long-flat IDENTICO ma SENZA gate funding")
|
|
d_tr = cell_daily(trend_only_target)
|
|
tr_ins, tr_hold = d_tr[d_tr.index < al.HOLDOUT], d_tr[d_tr.index >= al.HOLDOUT]
|
|
JJ = pd.concat({"G": daily, "T": d_tr}, axis=1, join="inner").dropna()
|
|
print(f" trend NUDO: IS {al._sh(tr_ins):.2f} FULL {al._sh(d_tr):.2f} HOLD {al._sh(tr_hold):.2f}")
|
|
print(f" trend+GATE: IS {chosen['insample_sharpe']:.2f} FULL {chosen['full_sharpe']:.2f} "
|
|
f"HOLD {chosen['hold_sharpe']:.2f}")
|
|
print(f" corr(gated, nudo) = {JJ['G'].corr(JJ['T']):.3f} "
|
|
f"delta FULL = {al._sh(JJ['G']) - al._sh(JJ['T']):+.3f} "
|
|
f"delta HOLD = {al._sh(JJ['G'][JJ.index >= al.HOLDOUT]) - al._sh(JJ['T'][JJ.index >= al.HOLDOUT]):+.3f}")
|
|
attribution = dict(trend_nudo=dict(IS=round(al._sh(tr_ins), 3), FULL=round(al._sh(d_tr), 3),
|
|
HOLD=round(al._sh(tr_hold), 3)),
|
|
corr_gated_nudo=round(float(JJ["G"].corr(JJ["T"])), 3),
|
|
delta_full=round(al._sh(JJ["G"]) - al._sh(JJ["T"]), 3),
|
|
delta_hold=round(al._sh(JJ["G"][JJ.index >= al.HOLDOUT])
|
|
- al._sh(JJ["T"][JJ.index >= al.HOLDOUT]), 3))
|
|
|
|
# --- 5ter. la migliore cella PURO-funding (fade/follow/diverge, senza trend) ---------
|
|
pure = [r for r in valid if r["form"] != "gate"]
|
|
bp = pure[0] if pure else None
|
|
if bp:
|
|
print(f"\n[5ter] MIGLIOR CELLA PURO-FUNDING (no trend): {bp['form']} lb={bp['lb']} "
|
|
f"thr={bp['thr']} IS {bp['insample_sharpe']} FULL {bp['full_sharpe']} "
|
|
f"HOLD {bp['hold_sharpe']}")
|
|
d_bp = cell_daily(make_target(form=bp["form"], lb=bp["lb"], thr=bp["thr"]))
|
|
m_bp = al.marginal_vs_tp01(d_bp)
|
|
print(f" marginale vs TP01: {m_bp.get('marginal_verdict')} corr {m_bp.get('corr_full')} "
|
|
f"IS-edge {m_bp.get('cand_insample_sharpe')} "
|
|
f"uplift w25 full {m_bp['blends']['w25']['uplift_full']:+.3f} / "
|
|
f"hold {m_bp['blends']['w25']['uplift_hold']:+.3f}")
|
|
|
|
# --- 6. realism: prefix / boundary / smallcap ---------------------------------------
|
|
print("\n[6] REALISM CHECKS (cella scelta)")
|
|
worst = prefix_check(fn)
|
|
print(f" prefix-consistency (guardia look-ahead): max diff = {worst:.2e} "
|
|
f"({'OK' if worst < 1e-9 else 'ATTENZIONE'})")
|
|
bnd = boundary_check(chosen["form"], chosen["lb"], chosen["thr"])
|
|
print(f" boundary-shift (finestra funding -0/3/6/9/12h): {bnd['per_offset']}")
|
|
print(f" sharpe_sign_stable={bnd['sharpe_sign_stable']} uplift_spread={bnd['uplift_spread']}")
|
|
sc = smallcap_check(fn)
|
|
print(f" smallcap $600 (min order $5): {sc}")
|
|
|
|
# --- 7. cross-check non troncato (footnote) -----------------------------------------
|
|
print("\n[7] CROSS-CHECK study_marginal NON troncato (frame 2018+, diluito dagli zeri "
|
|
"pre-copertura: footnote, non il giudizio primario)")
|
|
sm_full = al.study_marginal(f"R0701-FUND-XCHK {chosen['form']}", fn, tf="1d")
|
|
print(f" abs={sm_full['abs_grade']} marginal={sm_full['marginal_verdict']} "
|
|
f"earns_slot={sm_full['earns_slot']}")
|
|
|
|
# --- 8. verdetto --------------------------------------------------------------------
|
|
summary = dict(
|
|
chosen=dict(form=chosen["form"], lb=chosen["lb"], thr=chosen["thr"]),
|
|
insample_sharpe=chosen["insample_sharpe"], full_sharpe=chosen["full_sharpe"],
|
|
hold_sharpe=chosen["hold_sharpe"], dsr=round(float(dsr), 3), dsr_pass=dsr_pass,
|
|
abs_grade=abs_grade, marginal_verdict=marg.get("marginal_verdict"),
|
|
corr_tp01_full=marg.get("corr_full"), cand_insample_sharpe=marg.get("cand_insample_sharpe"),
|
|
has_insample_edge=marg.get("has_insample_edge"), is_hedge=marg.get("is_hedge"),
|
|
robust_oos=marg.get("robust_oos"), multicut=marg.get("multicut_uplift"),
|
|
earns_slot=earns_slot, earns_slot_honest=earns_honest,
|
|
smallcap=sc, boundary_uplift_spread=bnd["uplift_spread"],
|
|
attribution_vs_trend_nudo=attribution,
|
|
best_pure_funding=(dict(form=bp["form"], lb=bp["lb"], thr=bp["thr"],
|
|
IS=bp["insample_sharpe"], FULL=bp["full_sharpe"],
|
|
HOLD=bp["hold_sharpe"]) if bp else None),
|
|
n_cells=len(rows), history_years=round((hi - lo).days / 365.25, 1))
|
|
print("\n[8] SUMMARY JSON")
|
|
print(json.dumps(summary, default=str))
|
|
return summary
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|