7534b08be0
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>
419 lines
21 KiB
Python
419 lines
21 KiB
Python
#!/usr/bin/env python
|
|
"""r0717_wk_overlay — WK-OVERLAY: modificare l'esposizione WEEKEND di TP01 conviene? (2026-07-17)
|
|
|
|
FILONE (lato ESEGUIBILE del book live Deribit, solo componente TP01):
|
|
uscire/ridurre/raddoppiare l'esposizione TP01 nel weekend (ven H_out -> lun H_in) migliora
|
|
Sharpe/DD FULL e HOLD-OUT al netto delle fee extra (~104 trade/anno se esci+rientri)?
|
|
|
|
VARIANTI (target TP01 CANONICAL 1d mappato causalmente sulla griglia 1h):
|
|
a. WK-FLAT : posizione -> 0 dentro [ven H_out, lun H_in)
|
|
b. WK-HALF : x0.5 dentro la finestra
|
|
c. WK-BOOST : x1.5 dentro la finestra (controllo simmetrico; cap leva 2.0)
|
|
d. WK-ONLY : esposizione TP01 SOLO dentro la finestra, flat fuori
|
|
Griglia: H_out in {ven 12,16,20} x H_in in {lun 0,8} = 6 celle/variante.
|
|
|
|
CONTROLLI OBBLIGATORI:
|
|
1. Null de-levering (lezione DVOL 2026-06-26): con fee proporzionali net(k*tgt)=k*net(tgt)
|
|
ESATTO -> il de-levering realizzato lascia lo Sharpe daily INVARIATO; ogni taglio di DD
|
|
a Sharpe <= baseline e' replicabile con un semplice k (o target_vol ridotto) -> REFUTED.
|
|
Qui: sweep k, matching del DD della variante, confronto Sharpe/CAGR a pari DD.
|
|
2. Fee nette 0.10% RT su ogni esposizione mossa + sweep fee 0.00-0.30% RT.
|
|
3. Banda di timing completa (6 celle), selezione IN-SAMPLE-ONLY, deflated_sharpe sui trial.
|
|
4. Per-anno + FULL vs HOLD-OUT (2025+).
|
|
5. Esecuzione a $600: eval_weights_smallcap (min-order $5, capitale per-asset $300).
|
|
6. Statistica onesta: bootstrap a blocchi SETTIMANALI appaiato sul delta di Sharpe +
|
|
placebo = 7 rotazioni giorno-della-settimana della stessa finestra (stessa lunghezza,
|
|
stesse fee, posizionamento calendario diverso).
|
|
|
|
CAUSALITA': il target daily di TP01 (deciso a close del giorno D = D+1d 00:00) viene
|
|
assegnato alla barra 1h la cui close e' >= a quel momento (searchsorted su epoch-ms,
|
|
mai .view su indici tz-aware — lezione pandas 2026-07-01). Il moltiplicatore weekend e'
|
|
funzione deterministica del calendario della barra SUCCESSIVA (quella in cui la posizione
|
|
viene tenuta) -> causale per costruzione. Guard prefix-recompute (causality_ok) incluso.
|
|
|
|
Book/pesi INVARIATI qualunque sia l'esito: questo e' research. Nessun file modificato.
|
|
Run: uv run python scripts/research/r0717_wk_overlay.py
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
from itertools import product
|
|
from pathlib import Path
|
|
|
|
import numpy as np
|
|
import pandas as pd
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "research" / "alt"))
|
|
import altlib as al # noqa: E402
|
|
|
|
from src.strategies.trend_portfolio import CANONICAL, TrendPortfolio, resample_1d # noqa: E402
|
|
|
|
HOLDOUT = al.HOLDOUT
|
|
FEE = al.FEE_SIDE
|
|
CAP = float(CANONICAL["leverage"])
|
|
SEED = 20260717
|
|
H_OUT = (12, 16, 20) # ora di uscita del venerdi' (UTC)
|
|
H_IN = (0, 8) # ora di rientro del lunedi' (UTC)
|
|
ASSETS = ("BTC", "ETH")
|
|
|
|
_DATA: dict[str, tuple[pd.DataFrame, np.ndarray]] = {}
|
|
|
|
|
|
# ===========================================================================
|
|
# TP01 canonico (1d) -> griglia 1h, per tempo di decisione (causale)
|
|
# ===========================================================================
|
|
def tp01_target_1h(df1h: pd.DataFrame) -> np.ndarray:
|
|
"""Target TP01 CANONICAL calcolato sul 1d e mappato sulle barre 1h.
|
|
La barra daily D (open-labeled) chiude a D+1d: il suo target e' noto da quel momento.
|
|
La barra 1h i (open T, close T+1h) porta l'ultimo target daily con close <= T+1h.
|
|
eval_weights poi tiene target[i] durante la barra i+1 -> stesso timing di trade del 1d."""
|
|
d1 = resample_1d(df1h)
|
|
tp = TrendPortfolio(**CANONICAL)
|
|
tgt_d = np.nan_to_num(tp.target_series(d1))
|
|
d_close_ms = d1["timestamp"].astype("int64").values + 86_400_000
|
|
h_close_ms = df1h["timestamp"].astype("int64").values + 3_600_000
|
|
j = np.searchsorted(d_close_ms, h_close_ms, side="right") - 1
|
|
out = np.where(j >= 0, tgt_d[np.clip(j, 0, None)], 0.0)
|
|
return np.nan_to_num(out.astype(float))
|
|
|
|
|
|
def window_mult(dt: pd.DatetimeIndex, start_how: int, end_how: int,
|
|
mult_in: float, mult_out: float = 1.0) -> np.ndarray:
|
|
"""Moltiplicatore per finestra ciclica [start,end) in ore-della-settimana (lun0=0)."""
|
|
how = dt.dayofweek.values * 24 + dt.hour.values
|
|
if start_how < end_how:
|
|
inw = (how >= start_how) & (how < end_how)
|
|
else:
|
|
inw = (how >= start_how) | (how < end_how)
|
|
return np.where(inw, mult_in, mult_out)
|
|
|
|
|
|
def wk_target(df1h: pd.DataFrame, tp1h: np.ndarray, h_out: int, h_in: int,
|
|
mult_in: float, mult_out: float = 1.0, rot_days: int = 0) -> np.ndarray:
|
|
"""Target = TP01 * moltiplicatore weekend. Il moltiplicatore e' valutato sull'OPEN
|
|
della barra SUCCESSIVA (= close della corrente): eval_weights tiene target[i] durante
|
|
la barra i+1, quindi la posizione dentro la finestra e' esattamente mult*TP01."""
|
|
start = (4 * 24 + h_out + 24 * rot_days) % 168
|
|
end = (0 * 24 + h_in + 24 * rot_days) % 168
|
|
dt_next_open = pd.DatetimeIndex(pd.to_datetime(df1h["datetime"], utc=True)) + pd.Timedelta(hours=1)
|
|
m = window_mult(dt_next_open, start, end, mult_in, mult_out)
|
|
return np.clip(tp1h * m, -CAP, CAP)
|
|
|
|
|
|
# ===========================================================================
|
|
# valutazione book 50/50 (griglia 1h -> serie daily composta)
|
|
# ===========================================================================
|
|
def per_asset_net(tgts: dict[str, np.ndarray], fee_side: float = FEE):
|
|
ser, stats = {}, {}
|
|
for a in ASSETS:
|
|
df = _DATA[a][0]
|
|
ev = al.eval_weights(df, tgts[a], fee_side=fee_side)
|
|
ser[a] = pd.Series(ev["net"], index=ev["idx"])
|
|
pos = np.zeros(len(tgts[a])); pos[1:] = tgts[a][:-1]
|
|
turn = np.abs(np.diff(pos, prepend=0.0))
|
|
yrs = len(df) / 24 / 365.25
|
|
stats[a] = dict(turnover_yr=float(turn.sum() / yrs),
|
|
trades_yr=float((turn > 1e-9).sum() / yrs),
|
|
tim=float(np.mean(pos != 0)))
|
|
return ser, stats
|
|
|
|
|
|
def combo_daily(tgts: dict[str, np.ndarray], fee_side: float = FEE):
|
|
ser, stats = per_asset_net(tgts, fee_side)
|
|
J = pd.concat(ser, axis=1, join="inner").fillna(0.0)
|
|
hourly = 0.5 * J[ASSETS[0]] + 0.5 * J[ASSETS[1]]
|
|
return al._to_daily(hourly), stats, hourly
|
|
|
|
|
|
def _cagr(d: pd.Series) -> float:
|
|
if len(d) < 2:
|
|
return float("nan")
|
|
tot = float(np.prod(1.0 + d.values))
|
|
yrs = (d.index[-1] - d.index[0]).days / 365.25
|
|
return tot ** (1 / yrs) - 1 if yrs > 0 and tot > 0 else float("nan")
|
|
|
|
|
|
def blk(d: pd.Series) -> dict:
|
|
if len(d) < 30:
|
|
return dict(sh=float("nan"), cagr=float("nan"), dd=float("nan"))
|
|
return dict(sh=al._sh(d), cagr=_cagr(d), dd=al._dd_ret(d))
|
|
|
|
|
|
def fh(d: pd.Series) -> dict:
|
|
return dict(full=blk(d), hold=blk(d[d.index >= HOLDOUT]),
|
|
ins=blk(d[d.index < HOLDOUT]))
|
|
|
|
|
|
def fmt_fh(s: dict) -> str:
|
|
f, h = s["full"], s["hold"]
|
|
return (f"FULL Sh {f['sh']:+.3f} CAGR {f['cagr'] * 100:+6.2f}% DD {f['dd'] * 100:5.2f}% | "
|
|
f"HOLD Sh {h['sh']:+.3f} CAGR {h['cagr'] * 100:+6.2f}% DD {h['dd'] * 100:5.2f}%")
|
|
|
|
|
|
# ===========================================================================
|
|
# bootstrap a blocchi settimanali appaiato sul delta di Sharpe
|
|
# ===========================================================================
|
|
def paired_week_bootstrap(base_d: pd.Series, var_d: pd.Series, n: int = 2000,
|
|
seed: int = SEED, start=None) -> dict:
|
|
J = pd.concat({"b": base_d, "v": var_d}, axis=1, join="inner").dropna()
|
|
if start is not None:
|
|
J = J[J.index >= start]
|
|
wk = (J.index - pd.to_timedelta(J.index.dayofweek, unit="D")).normalize()
|
|
codes, uniq = pd.factorize(wk)
|
|
groups = [np.where(codes == g)[0] for g in range(len(uniq))]
|
|
rng = np.random.default_rng(seed)
|
|
b, v = J["b"].values, J["v"].values
|
|
deltas = np.empty(n)
|
|
for t in range(n):
|
|
pick = rng.integers(0, len(groups), size=len(groups))
|
|
idx = np.concatenate([groups[p] for p in pick])
|
|
bs, vs = b[idx], v[idx]
|
|
shb = bs.mean() / bs.std() * np.sqrt(365.25) if bs.std() > 0 else 0.0
|
|
shv = vs.mean() / vs.std() * np.sqrt(365.25) if vs.std() > 0 else 0.0
|
|
deltas[t] = shv - shb
|
|
real = al._sh(J["v"]) - al._sh(J["b"])
|
|
return dict(real=float(real), p_le0=float(np.mean(deltas <= 0.0)),
|
|
ci_lo=float(np.percentile(deltas, 2.5)),
|
|
ci_hi=float(np.percentile(deltas, 97.5)), n_weeks=len(groups))
|
|
|
|
|
|
# ===========================================================================
|
|
# main
|
|
# ===========================================================================
|
|
def main() -> None:
|
|
print("=" * 100)
|
|
print("WK-OVERLAY (r0717) — esposizione weekend di TP01: flat / half / boost / weekend-only")
|
|
print(f"griglia 1h, fee {2 * FEE * 100:.2f}% RT, HOLDOUT {HOLDOUT.date()}, book 50/50 BTC+ETH")
|
|
print("=" * 100)
|
|
|
|
for a in ASSETS:
|
|
df = al.get(a, "1h")
|
|
_DATA[a] = (df, tp01_target_1h(df))
|
|
print(f" {a}: {len(df)} barre 1h {df['datetime'].iloc[0]} -> {df['datetime'].iloc[-1]}")
|
|
|
|
# ---- guard causalita' (prefix-recompute) sul target flat ven20->lun00 --------------
|
|
def _guard_fn(df, asset):
|
|
return wk_target(df, tp01_target_1h(df), 20, 0, 0.0)
|
|
ca = al.causality_ok(_guard_fn, tf="1h")
|
|
print(f"\ncausality_ok (prefix-recompute, WK-FLAT ven20->lun00): ok={ca['ok']} "
|
|
f"max_tail_diff={ca['max_tail_diff']}")
|
|
|
|
# ---- linearita' del de-levering (net(k*tgt) == k*net(tgt)) -------------------------
|
|
a0 = ASSETS[0]
|
|
ev1 = al.eval_weights(_DATA[a0][0], _DATA[a0][1])
|
|
evk = al.eval_weights(_DATA[a0][0], 0.5 * _DATA[a0][1])
|
|
lin_err = float(np.max(np.abs(evk["net"] - 0.5 * ev1["net"])))
|
|
print(f"linearita' de-levering: max|net(0.5*tgt) - 0.5*net(tgt)| = {lin_err:.2e} "
|
|
f"(atteso ~0 -> Sharpe daily k-invariante)")
|
|
|
|
# ================= A. BASELINE ======================================================
|
|
print("\n" + "=" * 100)
|
|
print("A. BASELINE — TP01 CANONICAL sul percorso 1h (ribilancio daily 00:00, identico al 1d)")
|
|
print("=" * 100)
|
|
base_tgts = {a: _DATA[a][1] for a in ASSETS}
|
|
base_d, base_stats, base_h = combo_daily(base_tgts)
|
|
base_s = fh(base_d)
|
|
print(f" baseline 1h-grid : {fmt_fh(base_s)} (IS Sh {base_s['ins']['sh']:+.3f})")
|
|
ref = al.tp01_baseline_daily()
|
|
print(f" riferimento 1d : FULL Sh {al._sh(ref):+.3f} DD {al._dd_ret(ref) * 100:.2f}% | "
|
|
f"HOLD Sh {al._sh(ref[ref.index >= HOLDOUT]):+.3f} (tp01_baseline_daily, sanity)")
|
|
for a in ASSETS:
|
|
st = base_stats[a]
|
|
print(f" {a}: turnover/anno {st['turnover_yr']:.1f} (fee drag ~{st['turnover_yr'] * FEE * 100:.2f}%/anno) "
|
|
f"trade/anno {st['trades_yr']:.0f} TIM {st['tim'] * 100:.0f}%")
|
|
|
|
# attribuzione weekend del gross baseline (senza fee): quanto vive nel weekend?
|
|
print("\n Attribuzione GROSS baseline (pos*r, no fee), finestra 'weekend puro' ven20->lun00:")
|
|
gser = {}
|
|
for a in ASSETS:
|
|
df, tgt = _DATA[a]
|
|
c = df["close"].values.astype(float)
|
|
r = al.simple_returns(c)
|
|
pos = np.zeros(len(tgt)); pos[1:] = tgt[:-1]
|
|
gser[a] = pd.Series(pos * r, index=pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True)))
|
|
G = pd.concat(gser, axis=1, join="inner").fillna(0.0)
|
|
gross = 0.5 * G[ASSETS[0]] + 0.5 * G[ASSETS[1]]
|
|
how = gross.index.dayofweek.values * 24 + gross.index.hour.values
|
|
wk_mask = how >= 116 # ven20 -> dom24 == fino a lun00
|
|
tot, wk_part = float(gross.sum()), float(gross[wk_mask].sum())
|
|
yrs_span = (gross.index[-1] - gross.index[0]).days / 365.25
|
|
print(f" quota tempo weekend: {wk_mask.mean() * 100:.1f}% | contributo gross weekend: "
|
|
f"{wk_part * 100:+.1f}pp su {tot * 100:+.1f}pp totali ({wk_part / tot * 100 if tot else 0:.0f}%) "
|
|
f"[~{wk_part / yrs_span * 100:+.2f}pp/anno]")
|
|
by_year = gross.groupby(gross.index.year).apply(lambda s: float(s.sum()))
|
|
by_year_wk = gross[wk_mask].groupby(gross.index[wk_mask].year).apply(lambda s: float(s.sum()))
|
|
print(" per anno (gross pp: weekend / totale): " + " ".join(
|
|
f"{y}:{by_year_wk.get(y, 0.0) * 100:+.1f}/{by_year.get(y, 0.0) * 100:+.1f}"
|
|
for y in by_year.index))
|
|
|
|
# ================= B. GRIGLIA COMPLETA =============================================
|
|
print("\n" + "=" * 100)
|
|
print("B. BANDA DI TIMING — tutte le 6 celle x 4 varianti (delta vs baseline, fee 0.10% RT)")
|
|
print("=" * 100)
|
|
VAR = {"a_FLAT": dict(mult_in=0.0, mult_out=1.0),
|
|
"b_HALF": dict(mult_in=0.5, mult_out=1.0),
|
|
"c_BOOST": dict(mult_in=1.5, mult_out=1.0),
|
|
"d_WKONLY": dict(mult_in=1.0, mult_out=0.0)}
|
|
cells = list(product(H_OUT, H_IN))
|
|
res: dict[str, dict] = {}
|
|
all_full_sh: list[float] = []
|
|
for vname, vm in VAR.items():
|
|
res[vname] = {}
|
|
print(f"\n -- {vname} (mult_in={vm['mult_in']}, mult_out={vm['mult_out']}) --")
|
|
print(f" {'cella':<14} {'ISΔSh':>7} {'FULLΔSh':>8} {'HOLDΔSh':>8} {'ΔCAGR':>7} {'ΔDD':>7} "
|
|
f"{'Sh_full':>8} {'Sh_hold':>8} {'DD':>6} {'trade/y':>8}")
|
|
for ho, hi in cells:
|
|
tgts = {a: wk_target(_DATA[a][0], _DATA[a][1], ho, hi, **vm) for a in ASSETS}
|
|
d, st, _ = combo_daily(tgts)
|
|
s = fh(d)
|
|
tr = np.mean([st[a]["trades_yr"] for a in ASSETS])
|
|
key = f"ven{ho:02d}->lun{hi:02d}"
|
|
res[vname][key] = dict(daily=d, s=s, tgts=tgts, stats=st, ho=ho, hi=hi)
|
|
all_full_sh.append(s["full"]["sh"])
|
|
print(f" {key:<14} {s['ins']['sh'] - base_s['ins']['sh']:+7.3f} "
|
|
f"{s['full']['sh'] - base_s['full']['sh']:+8.3f} "
|
|
f"{s['hold']['sh'] - base_s['hold']['sh']:+8.3f} "
|
|
f"{(s['full']['cagr'] - base_s['full']['cagr']) * 100:+6.2f}pp "
|
|
f"{(s['full']['dd'] - base_s['full']['dd']) * 100:+6.2f}pp "
|
|
f"{s['full']['sh']:+8.3f} {s['hold']['sh']:+8.3f} {s['full']['dd'] * 100:5.1f}% {tr:8.0f}")
|
|
|
|
# ================= C. SELEZIONE IN-SAMPLE + DSR ====================================
|
|
print("\n" + "=" * 100)
|
|
print("C. SELEZIONE IN-SAMPLE-ONLY (max Sharpe pre-2025) + deflated Sharpe sui 24 trial")
|
|
print("=" * 100)
|
|
chosen: dict[str, dict] = {}
|
|
for vname in VAR:
|
|
best = max(res[vname].items(), key=lambda kv: kv[1]["s"]["ins"]["sh"])
|
|
chosen[vname] = dict(key=best[0], **best[1])
|
|
d = best[1]["daily"]
|
|
dsr, sr0 = al.deflated_sharpe(al._sh(d), all_full_sh, d.values)
|
|
chosen[vname]["dsr"] = dsr
|
|
s = best[1]["s"]
|
|
print(f" {vname:<9} cella IS-best={best[0]} {fmt_fh(s)}")
|
|
print(f" ΔSh full {s['full']['sh'] - base_s['full']['sh']:+.3f} / hold "
|
|
f"{s['hold']['sh'] - base_s['hold']['sh']:+.3f} | DSR(24 trial)={dsr:.3f} "
|
|
f"(null-max Sh {sr0:.2f}) — NB: per una famiglia di OVERLAY su TP01 il DSR eredita "
|
|
f"lo Sharpe di trend: il test discriminante e' il placebo/bootstrap, non il DSR")
|
|
|
|
# ================= D. NULL DE-LEVERING =============================================
|
|
print("\n" + "=" * 100)
|
|
print("D. NULL DE-LEVERING — TP01 scalato k a pari DD (lezione DVOL: se eguaglia/batte, REFUTED)")
|
|
print("=" * 100)
|
|
print(f" (net(k*tgt)=k*net(tgt) esatto -> Sharpe daily del de-levered ≈ baseline a OGNI k; "
|
|
f"CAGR/DD scendono con k)")
|
|
ks = np.arange(0.30, 1.5001, 0.0025)
|
|
print(" (per DD > baseline il null speculare e' il RE-levering uniforme k>1 a pari DD; "
|
|
"NB k>1 sfora il cap 2x in alcune barre -> null leggermente ottimista)")
|
|
for vname in VAR:
|
|
ch = chosen[vname]
|
|
v_dd = ch["s"]["full"]["dd"]
|
|
best_k, best_gap, best_blk = None, 9e9, None
|
|
for k in ks:
|
|
dk = al._to_daily(k * base_h)
|
|
bb = blk(dk)
|
|
gap = abs(bb["dd"] - v_dd)
|
|
if gap < best_gap:
|
|
best_k, best_gap, best_blk = k, gap, dict(full=bb, d=dk)
|
|
dl_full = best_blk["full"]
|
|
dl_hold = blk(best_blk["d"][best_blk["d"].index >= HOLDOUT])
|
|
vs = ch["s"]
|
|
refuted = dl_full["sh"] >= vs["full"]["sh"] - 0.02
|
|
print(f" {vname:<9} DD variante {v_dd * 100:.2f}% -> k={best_k:.3f} de-lever: "
|
|
f"Sh {dl_full['sh']:+.3f} CAGR {dl_full['cagr'] * 100:+.2f}% DD {dl_full['dd'] * 100:.2f}% "
|
|
f"(hold Sh {dl_hold['sh']:+.3f})")
|
|
print(f" variante: Sh {vs['full']['sh']:+.3f} CAGR "
|
|
f"{vs['full']['cagr'] * 100:+.2f}% DD {vs['full']['dd'] * 100:.2f}% (hold Sh {vs['hold']['sh']:+.3f})"
|
|
f" -> {'REFUTED (solo de-levering)' if refuted else 'batte il de-levering a pari DD'}")
|
|
|
|
# ================= E. PLACEBO ROTAZIONI ============================================
|
|
print("\n" + "=" * 100)
|
|
print("E. PLACEBO — 7 rotazioni giorno-della-settimana della finestra (stessa durata/fee)")
|
|
print("=" * 100)
|
|
for vname in ("a_FLAT", "c_BOOST", "d_WKONLY"):
|
|
ch = chosen[vname]
|
|
vm = VAR[vname]
|
|
print(f"\n -- {vname} cella {ch['key']} (rot 0 = weekend vero) --")
|
|
rows = []
|
|
for rot in range(7):
|
|
tgts = {a: wk_target(_DATA[a][0], _DATA[a][1], ch["ho"], ch["hi"],
|
|
vm["mult_in"], vm["mult_out"], rot_days=rot) for a in ASSETS}
|
|
d, _, _ = combo_daily(tgts)
|
|
s = fh(d)
|
|
rows.append(dict(rot=rot,
|
|
d_ins=s["ins"]["sh"] - base_s["ins"]["sh"],
|
|
d_full=s["full"]["sh"] - base_s["full"]["sh"],
|
|
d_hold=s["hold"]["sh"] - base_s["hold"]["sh"],
|
|
sh_full=s["full"]["sh"]))
|
|
for r in rows:
|
|
tag = " <== WEEKEND" if r["rot"] == 0 else ""
|
|
print(f" rot+{r['rot']}g: ΔSh IS {r['d_ins']:+.3f} FULL {r['d_full']:+.3f} "
|
|
f"HOLD {r['d_hold']:+.3f} (Sh full {r['sh_full']:+.3f}){tag}")
|
|
for metric in ("d_ins", "d_full", "d_hold"):
|
|
vals = [r[metric] for r in rows]
|
|
rank = 1 + sum(1 for v in vals[1:] if v > vals[0])
|
|
print(f" rank weekend su {metric}: {rank}/7 (1=migliore; P(rank 1 per caso)=0.14)")
|
|
|
|
# ================= F. BOOTSTRAP SETTIMANALE ========================================
|
|
print("\n" + "=" * 100)
|
|
print("F. BOOTSTRAP a blocchi SETTIMANALI appaiato — ΔSharpe variante-baseline (2000 draw)")
|
|
print("=" * 100)
|
|
for vname in VAR:
|
|
ch = chosen[vname]
|
|
bf = paired_week_bootstrap(base_d, ch["daily"])
|
|
bh = paired_week_bootstrap(base_d, ch["daily"], start=HOLDOUT)
|
|
print(f" {vname:<9} FULL ΔSh {bf['real']:+.3f} CI95 [{bf['ci_lo']:+.3f},{bf['ci_hi']:+.3f}] "
|
|
f"P(Δ<=0)={bf['p_le0']:.3f} ({bf['n_weeks']} settimane)")
|
|
print(f" HOLD ΔSh {bh['real']:+.3f} CI95 [{bh['ci_lo']:+.3f},{bh['ci_hi']:+.3f}] "
|
|
f"P(Δ<=0)={bh['p_le0']:.3f} ({bh['n_weeks']} settimane)")
|
|
|
|
# ================= G. FEE SWEEP ====================================================
|
|
print("\n" + "=" * 100)
|
|
print("G. FEE SWEEP — ΔSh FULL vs baseline ALLA STESSA fee (0.00 / 0.10 / 0.20 / 0.30% RT)")
|
|
print("=" * 100)
|
|
fees = (0.0, 0.0005, 0.001, 0.0015)
|
|
hdr = " ".join(f"{2 * f * 100:.2f}%RT" for f in fees)
|
|
print(f" {'variante':<9} {hdr} (ogni colonna: ΔSh full / ΔSh hold)")
|
|
for vname in VAR:
|
|
ch = chosen[vname]
|
|
parts = []
|
|
for f in fees:
|
|
bd, _, _ = combo_daily(base_tgts, fee_side=f)
|
|
vd, _, _ = combo_daily(ch["tgts"], fee_side=f)
|
|
bs_, vs_ = fh(bd), fh(vd)
|
|
parts.append(f"{vs_['full']['sh'] - bs_['full']['sh']:+.3f}/"
|
|
f"{vs_['hold']['sh'] - bs_['hold']['sh']:+.3f}")
|
|
print(f" {vname:<9} " + " ".join(parts))
|
|
|
|
# ================= H. SMALL-CAP $600 ===============================================
|
|
print("\n" + "=" * 100)
|
|
print("H. ESECUZIONE A $600 — eval_weights_smallcap (min-order $5; capitale per-asset $300)")
|
|
print("=" * 100)
|
|
yrs = {a: len(_DATA[a][0]) / 24 / 365.25 for a in ASSETS}
|
|
for label, tg in [("BASELINE", base_tgts)] + [(v, chosen[v]["tgts"]) for v in VAR]:
|
|
for a in ASSETS:
|
|
sc = al.eval_weights_smallcap(_DATA[a][0], tg[a], capital=300.0, min_order=5.0)
|
|
print(f" {label:<9} {a}: modellato Sh {sc['modeled']['sharpe']:+.3f} -> reale Sh "
|
|
f"{sc['realistic']['sharpe']:+.3f} (haircut {sc['sharpe_haircut']:+.3f}) "
|
|
f"trade eseguiti/anno {sc['n_executed_trades'] / yrs[a]:.0f} "
|
|
f"turnover eseguito/anno {sc['executed_turnover_per_year']:.1f}")
|
|
|
|
# ================= I. PER-ANNO =====================================================
|
|
print("\n" + "=" * 100)
|
|
print("I. PER-ANNO — ritorno % (baseline vs celle IS-best)")
|
|
print("=" * 100)
|
|
tab = {"BASE": al._yearly(base_d.values, base_d.index)}
|
|
for vname in VAR:
|
|
tab[vname] = al._yearly(chosen[vname]["daily"].values, chosen[vname]["daily"].index)
|
|
years = sorted(tab["BASE"])
|
|
print(" anno " + "".join(f"{k:>10}" for k in tab))
|
|
for y in years:
|
|
print(f" {y} " + "".join(f"{tab[k].get(y, {}).get('ret', float('nan')) * 100:+9.1f}%" for k in tab))
|
|
print(" DDmax " + "".join(
|
|
f"{max(tab[k][y]['dd'] for y in tab[k]) * 100:9.1f}%" for k in tab))
|
|
|
|
print("\nFATTO. Book/pesi INVARIATI (research only).")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|