research(weekend): ondata ven->lun SCARTATA 4/4 — famiglia calendario SATURA; weekend = 38% del gross TP01 (mai de-esporre)

4 agenti (drift/cond/intra/overlay), 375 trial: il drift weekend e' beta B&H,
i gate condizionali sono TP01 re-timed, l'intraday weekend e' moneta simmetrica
(CME-gap morto anche lordo). Fatto strutturale: TP01-weekend-flat = danno certo
(dSh -0.46, P=1.000) -> ogni proposta "risk-off weekend" parte REFUTED salvo
null de-levering. Chiusa la famiglia calendario su BTC/ETH (SEA+expiry+event-clock
+weekend). Book/pesi INVARIATI. Diario 2026-07-17-weekend-window.md.

gitignore: + data/live/ (log esecuzioni book live, stato runtime del conto reale)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Adriano Dal Pastro
2026-07-18 15:03:44 +00:00
parent 67f7b89e4c
commit 7534b08be0
7 changed files with 1584 additions and 0 deletions
+247
View File
@@ -0,0 +1,247 @@
"""r0717_wk_drift — WK-DRIFT: esposizione weekend pura (ven H_in -> lun H_out), griglia di timing.
Domanda: la finestra ven->lun su BTC/ETH contiene drift direzionale sfruttabile NETTO fee,
robusto al timing? Prior art: SEA (day-of-week) e' gia' morta nel progetto; onere della prova ALTO.
Metodo (harness onesto altlib):
* Famiglia: posizione (long E short = celle separate) da ven H_in a lun H_out,
H_in, H_out in {0,4,8,12,16,20} UTC, asset in {BTC, ETH} -> 6*6*2*2 = 144 trial.
* Causalita': eval_weights fa pos[t] = target[t-1] (target deciso a close[i] e' applicato
al ritorno close[i]->close[i+1]). L'intervallo detenuto da target[i] INIZIA al close
della barra i => l'indicatore di finestra si valuta su datetime[i] + 1h (close time).
Il segnale e' puro calendario (noto in anticipo) -> causale per costruzione; verificato
comunque con causality_ok.
* Selezione cella con SOLO Sharpe in-sample (pre-2025); poi si guarda l'hold-out.
* deflated_sharpe su TUTTA la famiglia (144 trial, unita' daily-compounded coerenti).
* NULL di specialita': stessa finestra (stessa lunghezza, stessi H_in/H_out) ancorata a
ogni altro giorno della settimana (lun->gio, mar->ven, ...) -> il ven->lun e' speciale
o qualsiasi finestra ~3g cattura lo stesso beta di B&H a esposizione ridotta?
+ confronto capture: quota di log-return catturata vs quota di ore esposte.
* day_boundary_robust sulla cella selezionata (effetto calendario che si inverte
spostando il confine UTC = artefatto di etichettatura).
* Sweep fee per-side (0, 0.05%, 0.10%, 0.15%) sulla cella selezionata.
* Per-anno: la cella vive di 1-2 anni o e' positiva ogni anno?
Vincoli: nessun file esistente modificato; dati solo via altlib (BTC/ETH 1h certificati).
"""
from __future__ import annotations
import sys
from pathlib import Path
sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/alt")
import numpy as np
import pandas as pd
import altlib as al
H_GRID = (0, 4, 8, 12, 16, 20)
ASSETS = ("BTC", "ETH")
DIRS = (1, -1) # long e short come celle separate
FRI = 4 # pandas weekday: Mon=0 ... Fri=4
SPAN_DAYS = 3 # ven -> lun = anchor_day d -> (d+3) % 7
DAY_NAMES = {0: "Mon", 1: "Tue", 2: "Wed", 3: "Thu", 4: "Fri", 5: "Sat", 6: "Sun"}
# ---------------------------------------------------------------------------
# Target di finestra calendario (causale: funzione deterministica del clock)
# ---------------------------------------------------------------------------
def make_target(h_in: int, h_out: int, direction: int, day_in: int = FRI):
"""target[i] = direction se l'intervallo detenuto da target[i] — che inizia al
CLOSE della barra i (datetime[i] + 1h) — cade dentro [day_in H_in, day_out H_out).
day_out = (day_in + 3) % 7. Entrata eseguita al close di ven H_in:00, uscita al
close di lun H_out:00 => esposizione esattamente [ven H_in, lun H_out]."""
day_out = (day_in + SPAN_DAYS) % 7
start = day_in * 24 + h_in # hour-of-week di inizio finestra
end = day_out * 24 + h_out # hour-of-week di fine finestra
def fn(df):
dt = pd.to_datetime(df["datetime"], utc=True) + pd.Timedelta(hours=1) # close time barra i
wh = (dt.dt.weekday * 24 + dt.dt.hour).values
if start < end:
ind = (wh >= start) & (wh < end)
else: # la finestra scavalca il confine di settimana
ind = (wh >= start) | (wh < end)
return direction * ind.astype(float)
return fn
def window_hours(h_in: int, h_out: int) -> int:
return (SPAN_DAYS * 24) - h_in + h_out # ven H_in -> lun H_out
# ---------------------------------------------------------------------------
# Metriche per cella (unita' PRIMARIA = Sharpe su ritorni daily-compounded,
# stessa convenzione di candidate_daily/_to_daily del marginal scorer)
# ---------------------------------------------------------------------------
def cell_metrics(asset: str, h_in: int, h_out: int, direction: int,
day_in: int = FRI, fee_side: float = al.FEE_SIDE, keep: bool = False) -> dict:
df = al.get(asset, "1h")
fn = make_target(h_in, h_out, direction, day_in)
ev = al.eval_weights(df, fn(df), fee_side=fee_side)
s = pd.Series(ev["net"], index=ev["idx"])
d = al._to_daily(s)
ins = d[d.index < al.HOLDOUT]
hold = d[d.index >= al.HOLDOUT]
out = dict(asset=asset, h_in=h_in, h_out=h_out, dir=direction, day_in=day_in,
sh_is=al._sh(ins), sh_full=al._sh(d), sh_hold=al._sh(hold))
if keep:
out["ev"] = ev
out["daily"] = d
return out
def band(vals: list[float]) -> str:
v = np.asarray(vals, float)
return f"min {v.min():+.2f} / med {np.median(v):+.2f} / max {v.max():+.2f}"
def main() -> None:
print("=" * 88)
print("WK-DRIFT r0717 — esposizione weekend pura ven->lun, griglia di timing (dati 1h certificati)")
print("=" * 88)
# ================= 1) GRIGLIA COMPLETA (144 trial) =================
rows = []
for a in ASSETS:
for dr in DIRS:
for hi in H_GRID:
for ho in H_GRID:
rows.append(cell_metrics(a, hi, ho, dr))
n_trials = len(rows)
print(f"\n[1] FAMIGLIA: {len(H_GRID)}x{len(H_GRID)} timing x {len(DIRS)} direzioni x "
f"{len(ASSETS)} asset = {n_trials} trial (fee {2*al.FEE_SIDE*100:.2f}% RT)")
longs = [r for r in rows if r["dir"] == 1]
shorts = [r for r in rows if r["dir"] == -1]
print(f" BANDA FULL Sharpe (daily, netto) — tutte le 144 celle : {band([r['sh_full'] for r in rows])}")
print(f" BANDA HOLD Sharpe (2025+, netto) — tutte le 144 celle : {band([r['sh_hold'] for r in rows])}")
print(f" solo LONG (72): FULL {band([r['sh_full'] for r in longs])} | "
f"HOLD {band([r['sh_hold'] for r in longs])}")
print(f" solo SHORT (72): FULL {band([r['sh_full'] for r in shorts])} | "
f"HOLD {band([r['sh_hold'] for r in shorts])}")
n_pos_full = sum(1 for r in rows if r["sh_full"] > 0)
n_pos_both = sum(1 for r in rows if r["sh_full"] > 0 and r["sh_hold"] > 0)
print(f" celle FULL>0: {n_pos_full}/{n_trials} | celle FULL>0 E HOLD>0: {n_pos_both}/{n_trials}")
# ================= 2) SELEZIONE IN-SAMPLE-ONLY =================
chosen = max(rows, key=lambda r: r["sh_is"])
ch = cell_metrics(chosen["asset"], chosen["h_in"], chosen["h_out"], chosen["dir"], keep=True)
ev = ch["ev"]
dirname = "LONG" if ch["dir"] == 1 else "SHORT"
wh = window_hours(ch["h_in"], ch["h_out"])
print(f"\n[2] CELLA SELEZIONATA IN-SAMPLE (max Sharpe pre-2025 sui 144 trial):")
print(f" {dirname} {ch['asset']} ven {ch['h_in']:02d}:00 -> lun {ch['h_out']:02d}:00 UTC "
f"({wh}h esposte = {wh/168*100:.0f}% della settimana)")
print(f" Sharpe daily netto: IS {ch['sh_is']:+.2f} | FULL {ch['sh_full']:+.2f} | HOLD {ch['sh_hold']:+.2f}")
print(f" eval_weights (1h): FULL Sh {ev['full']['sharpe']:+.2f} CAGR {ev['full']['cagr']*100:+.1f}% "
f"maxDD {ev['full']['maxdd']*100:.1f}% ret {ev['full']['ret']*100:+.1f}%")
print(f" HOLD Sh {ev['holdout'].get('sharpe', 0):+.2f} "
f"ret {ev['holdout'].get('ret', 0)*100:+.1f}%")
print(f" time-in-market {ev['time_in_market']*100:.1f}% turnover/anno {ev['turnover_per_year']:.0f} "
f"=> fee drag ~{ev['turnover_per_year']*al.FEE_SIDE*100:.1f}%/anno a nozionale pieno")
top5 = sorted(rows, key=lambda r: -r["sh_is"])[:5]
print(" top-5 in-sample:")
for r in top5:
print(f" {'L' if r['dir']==1 else 'S'} {r['asset']} ven{r['h_in']:02d}->lun{r['h_out']:02d}: "
f"IS {r['sh_is']:+.2f} FULL {r['sh_full']:+.2f} HOLD {r['sh_hold']:+.2f}")
# ================= 3) DEFLATED SHARPE sull'intera famiglia =================
dsr, sr0 = al.deflated_sharpe(ch["sh_full"], [r["sh_full"] for r in rows], ch["daily"].values)
print(f"\n[3] DEFLATED SHARPE (famiglia {n_trials} trial): DSR = {dsr:.3f} "
f"(PASS>=0.95) | max Sharpe atteso sotto il null = {sr0:.2f} "
f"vs osservato {ch['sh_full']:+.2f}")
# ================= 4) NULL DI SPECIALITA' — ancore weekday =================
print(f"\n[4] NULL WEEKDAY — stessa finestra ({ch['h_in']:02d}->+3g {ch['h_out']:02d}, "
f"{dirname} {ch['asset']}) ancorata a ogni giorno:")
anchor = {}
for d0 in range(7):
m = cell_metrics(ch["asset"], ch["h_in"], ch["h_out"], ch["dir"], day_in=d0)
anchor[d0] = m
tag = " <== ven->lun" if d0 == FRI else ""
print(f" {DAY_NAMES[d0]}->{DAY_NAMES[(d0+3)%7]}: IS {m['sh_is']:+.2f} "
f"FULL {m['sh_full']:+.2f} HOLD {m['sh_hold']:+.2f}{tag}")
fri_is, fri_full = anchor[FRI]["sh_is"], anchor[FRI]["sh_full"]
pct_is = np.mean([anchor[d]["sh_is"] <= fri_is for d in range(7)])
pct_full = np.mean([anchor[d]["sh_full"] <= fri_full for d in range(7)])
print(f" percentile ancora-ven fra le 7: IS {pct_is*100:.0f}% FULL {pct_full*100:.0f}%")
# null a livello di FAMIGLIA: mediana IS su tutte le 36 celle timing (long, entrambi gli asset)
print(" null di famiglia (mediana Sharpe IS delle 72 celle LONG BTC+ETH, per ancora):")
fam = {}
for d0 in range(7):
vals = [cell_metrics(a, hi, ho, 1, day_in=d0)["sh_is"]
for a in ASSETS for hi in H_GRID for ho in H_GRID]
fam[d0] = float(np.median(vals))
for d0 in range(7):
tag = " <== ven" if d0 == FRI else ""
print(f" anchor {DAY_NAMES[d0]}: med IS {fam[d0]:+.2f}{tag}")
fam_pct = np.mean([fam[d] <= fam[FRI] for d in range(7)])
print(f" percentile famiglia ancora-ven: {fam_pct*100:.0f}%")
# confronto B&H a pari frazione di tempo: capture di log-return vs quota ore
print(" capture vs B&H (quota del log-return totale catturata dentro la finestra vs quota ore):")
for a in ASSETS:
df = al.get(a, "1h")
c = df["close"].values.astype(float)
lr = np.zeros(len(c)); lr[1:] = np.log(c[1:] / c[:-1])
dt = pd.to_datetime(df["datetime"], utc=True)
idx = pd.DatetimeIndex(dt)
ins_mask = np.asarray(idx < al.HOLDOUT)
bh = al._to_daily(pd.Series(al.simple_returns(c), index=idx))
for d0 in [FRI] + [d for d in range(7) if d != FRI]:
fn = make_target(ch["h_in"], ch["h_out"], 1, day_in=d0)
tgt = fn(df)
pos = np.zeros(len(tgt)); pos[1:] = tgt[:-1] # convenzione eval_weights
inw = pos > 0
t_share = inw.mean()
cap_full = lr[inw].sum() / lr.sum() if lr.sum() != 0 else np.nan
cap_is = (lr[inw & ins_mask].sum() / lr[ins_mask].sum()
if lr[ins_mask].sum() != 0 else np.nan)
if d0 == FRI:
print(f" {a}: B&H daily Sharpe FULL {al._sh(bh):+.2f} | finestra ven: "
f"ore {t_share*100:.0f}% capture FULL {cap_full*100:.0f}% IS {cap_is*100:.0f}%")
else:
print(f" anchor {DAY_NAMES[d0]}: ore {t_share*100:.0f}% "
f"capture FULL {cap_full*100:.0f}% IS {cap_is*100:.0f}%")
# ================= 5) DAY-BOUNDARY ROBUST sulla cella selezionata =================
fn_ch = make_target(ch["h_in"], ch["h_out"], ch["dir"])
dbr = al.day_boundary_robust(fn_ch, tf="1h")
print(f"\n[5] DAY_BOUNDARY_ROBUST (uplift marginale vs TP01 per offset del confine UTC):")
print(f" per_offset = {dbr['per_offset']}")
print(f" spread {dbr.get('spread')} verdetto = {dbr['verdict']}")
cok = al.causality_ok(fn_ch, tf="1h", assets=(ch["asset"],))
print(f" causality_ok: {cok['ok']} (max_tail_diff {cok['max_tail_diff']})")
# ================= 6) SWEEP FEE sulla cella selezionata =================
print(f"\n[6] SWEEP FEE (per-side) sulla cella selezionata:")
for f in al.FEE_SWEEP:
m = cell_metrics(ch["asset"], ch["h_in"], ch["h_out"], ch["dir"], fee_side=f, keep=True)
e = m["ev"]
print(f" fee {f*100:.2f}%/side ({2*f*100:.2f}% RT): Sh daily FULL {m['sh_full']:+.2f} "
f"HOLD {m['sh_hold']:+.2f} | ret FULL {e['full']['ret']*100:+.0f}% "
f"CAGR {e['full']['cagr']*100:+.1f}%")
# ================= 7) PER-ANNO della cella selezionata =================
print(f"\n[7] PER-ANNO (netto {2*al.FEE_SIDE*100:.2f}% RT) della cella selezionata:")
yl = ev["yearly"]
n_posy = sum(1 for v in yl.values() if v["ret"] > 0)
for y, v in yl.items():
print(f" {y}: ret {v['ret']*100:+6.1f}% dd {v['dd']*100:5.1f}%")
print(f" anni positivi: {n_posy}/{len(yl)}")
n_wk = len(ch["daily"]) / 7.02
arith = float(np.sum(ev["net"]))
print(f" media aritmetica per weekend (netto, full): {arith / max(n_wk, 1) * 100:+.3f}% "
f"su ~{n_wk:.0f} finestre")
print("\n" + "=" * 88)
print("FINE r0717_wk_drift — banda, selezione IS-only, DSR, null-weekday, boundary, fee, per-anno")
print("=" * 88)
if __name__ == "__main__":
main()