diff --git a/docs/diary/2026-07-24-goal-50eur-roadmap.md b/docs/diary/2026-07-24-goal-50eur-roadmap.md new file mode 100644 index 0000000..51558b5 --- /dev/null +++ b/docs/diary/2026-07-24-goal-50eur-roadmap.md @@ -0,0 +1,93 @@ +# 2026-07-24 — Goal "50 EUR/giorno in qualche mese": analisi onesta e roadmap + +**Contesto.** Goal dichiarato: rendita aggiuntiva che arrivi a **50 EUR/giorno entro qualche +mese**, senza precludere alcuna modalità. Branch `goal/50eur-day`. Questo diario è la sintesi +di: matematica del target sui ritorni reali del book, Monte Carlo prop-firm, 3 ricerche web +parallele (prop firm 2026, yield/basis a piccola scala, monetizzazione del track record), +e audit delle migliorie interne possibili. + +**Verdetto in una riga:** 50 EUR/g = **EUR 18.250/anno**; ai CAGR onesti del book (10-20%) +servono **EUR 73k-183k di capitale**. Da $600 in "qualche mese" per via di trading sul +capitale proprio **non esiste** — l'unica famiglia di vie compatibile con "mesi" è +**moltiplicare il nozionale gestito** (conto funded / capitale di terzi), ognuna con +probabilità e rischi quantificati sotto. + +--- + +## A) Il muro del capitale (script `r0724_goal50_math.py`, parte A) + +| CAGR netto | capitale per 50 EUR/g | +|---|---| +| 8% | ~EUR 228k | +| 10% | ~EUR 182k | +| 15% | ~EUR 122k | +| 20% | ~EUR 91k | +| 25% | ~EUR 73k | + +Tempo da $600 (CAGR 15%, compounding + depositi mensili): **0/mese → 38.7 anni; 500/mese → +9.6 anni; 1.000/mese → 6.3 anni; 2.000/mese → 3.8 anni; 5.000/mese → 1.8 anni.** + +## B) La leva non è la scorciatoia (parte B) + +Book Deribit live (TP01 .75 + SKH01 .25, modellato): Sharpe 1.77, **vol nativa ~11%**, +CAGR 20.5%, maxDD 9.4%. Il target su capitale piccolo implica: $600 → 3.315%/anno +(leva ~160x); $5k → 398%/anno (~19x, vol ~212%); $20k → 99%/anno (~4.8x, vol ~53%). +Tutte fuori dalla zona di sopravvivenza (Kelly documentato: il book gira a ~0.01-0.27 Kelly). +La frontiera 2026-07-03 ha già stabilito che il book gira alla sua vol nativa — alzare +`target_vol` scala DD in proporzione, non crea rendimento. + +## C) Prop-firm / conto funded — l'unica via "da mesi" quantificabile (parte C) + +Monte Carlo (block bootstrap 20g dei ritorni giornalieri reali del book, 20k path, +regole tipiche: target 8-10%, maxDD 10% statico/trailing, daily loss 4-5%): + +| scenario | leva eval | P(pass entro 1a) | mediana giorni | +|---|---|---|---| +| book modellato | 1.0x | 81-86% | 111-132 g | +| book modellato | 1.5x | 78-85% | **72-89 g** | +| book modellato | 2.0x | 72-81% | 46-62 g | +| de-luck ×0.6 | 1.0x | 63-72% | 113-137 g | +| de-luck ×0.6 | 1.5x | 62-71% | **70-87 g** | +| de-luck ×0.6 | 2.0x | 56-65% | 43-58 g | + +Fase funded ($100k nozionale, split 80%, 1 anno): **modellato** P(conto vivo) 88.9%, +payout p10/p50/p90 = EUR 8.9/42.3/93.4 al giorno; **de-luck ×0.6** P(vivo) 67.7%, +p10/p50/p90 = EUR 1.3/**31.1**/77.9 al giorno. A $200k nozionale i payout raddoppiano +(mediana de-luck ~EUR 62/g) — il target dei 50 EUR/g sta tra $150k e $200k funded. + +**Caveat onesti:** (1) daily-loss simulato su barre daily → i wick intraday rendono le +P(pass) OTTIMISTE; (2) le regole reali (consistency, min trading days, divieto bot/API, +weekend) possono uccidere lo stile low-frequency → vedi ricerca firm 2026 sotto; +(3) payout = reddito VOLATILE, non "rendita" (p10 de-luck ≈ EUR 1/g). + +## D) Vault / copy-trading (parte D) + +A perf-share 10% sopra HWM servono **$1-2M di TVL altrui** per 50 EUR/g → richiede anni +di track record pubblico, non mesi. Vincolo legale per residente italiano (gestione di +capitale altrui = art. 166 TUF) approfondito nella ricerca dedicata sotto. + +--- + +## Migliorie interne implementate/decise oggi + +1. **Gate di deploy PRE-REGISTRATO per STATARB-RESID** (`r0724_statarb_deploy_gate.py`). + Forward a oggi: 25 barre, +4.42%, Sharpe fwd +5.84 (caldissimo → regredirà), DD 1.4%, + haircut 0.01pp. Regola fissata OGGI (day 25) per evitare selection-on-forward: + **decisione il 2026-09-27 (90g)**; Sh≥0.5 → candidato deploy (pesi {10,15,20}% via + `weights_tilt_null`, maxDD combinato <15%); 0≤Sh<0.5 → UNA proroga di 90g; Sh<0 → ritiro. + Guardie: haircut <0.5pp, config congelata (W=45, sgn=+1), DD fwd <10%. +2. **Audit infra:** i messaggi "open/completed orders request timed out" nel cron daily sono + warning benigni di `ib_async` al connect col Gateway IB (conto paper DU) — cosmetici, + nessuna azione. Book live 743/743 giri completati; errori cerbero-mcp solo storici. +3. **Conferme di sistema (non ri-litigate):** soffitto direzionale ~1.3 (10+ ondate); + book già quasi-risk-parity; alzare vol = de-levering inverso (frontiera 2026-07-03); + il vincolo binding è il CAPITALE, non la ricerca. Il cap dinamico equity/2 è già + pronto ad assorbire depositi. + +## Ricerche web (3 agenti paralleli) — RISULTATI + +_(sezione completata sotto, dopo il rientro degli agenti)_ + +## Roadmap + +_(sintesi finale dopo i risultati degli agenti)_ diff --git a/scripts/research/r0724_goal50_math.py b/scripts/research/r0724_goal50_math.py new file mode 100644 index 0000000..109e3f8 --- /dev/null +++ b/scripts/research/r0724_goal50_math.py @@ -0,0 +1,196 @@ +"""r0724_goal50_math — la matematica ONESTA del target 50 EUR/giorno (2026-07-24). + +Domanda del goal: "rendita aggiuntiva che entro qualche mese arrivi a 50 EUR/giorno". +Questo script NON cerca un edge nuovo: quantifica le vie possibili col book che ABBIAMO, +usando i rendimenti giornalieri REALI del book Deribit (TP01 0.75 + SKH01 0.25) e +scenari de-luckati. Quattro parti: + + A) IL MURO — capitale richiesto per 50 EUR/g ai CAGR onesti; tempo da 600$ con depositi. + B) LA LEVA NON E' LA SCORCIATOIA — vol/DD implicati dal target su capitale piccolo. + C) PROP-FIRM EVAL — Monte Carlo (block bootstrap dei ritorni reali del book) di una + evaluation tipo crypto-prop: P(pass), giorni mediani, poi fase funded su 100k$: + distribuzione EUR/giorno e P(perdere il conto entro 1 anno). + D) VAULT / COPY — TVL necessario per 50 EUR/g a perf-share 10%. + +Onesta': i ritorni del book sono MODELLATI (il path live SKH01 e' peggiore: FULL 1.19/ +HOLD 1.15/DD~25% vs modellato ~1.4-1.6) -> ogni scenario gira anche con haircut sul +drift (x0.6) per il de-luck. EURUSD assunto 1.09 (parametro). +""" +from __future__ import annotations + +import sys +from pathlib import Path + +import numpy as np +import pandas as pd + +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT)) + +from src.portfolio.portfolio import StrategyPortfolio, metrics # noqa: E402 +from src.portfolio.sleeves import deribit_book_sleeves # noqa: E402 + +EURUSD = 1.09 # 1 EUR = 1.09 USD (assunzione dichiarata) +TARGET_EUR_DAY = 50.0 +TARGET_EUR_YR = TARGET_EUR_DAY * 365 +TARGET_USD_YR = TARGET_EUR_YR * EURUSD +RNG = np.random.default_rng(724) + + +def eur_day(usd_yr: float) -> float: + return usd_yr / EURUSD / 365.0 + + +# ----------------------------------------------------------------------------- A +def part_a() -> None: + print("=" * 100) + print(f" A) IL MURO — 50 EUR/g = {TARGET_EUR_YR:,.0f} EUR/anno = ${TARGET_USD_YR:,.0f}/anno") + print("=" * 100) + print(" capitale richiesto (rendita = capitale x CAGR netto):") + for cagr in (0.08, 0.10, 0.15, 0.20, 0.25): + cap = TARGET_USD_YR / cagr + print(f" CAGR {cagr:>4.0%} -> capitale ${cap:>10,.0f} (~EUR {cap / EURUSD:,.0f})") + print("\n tempo per arrivarci da $600 (compounding mensile + deposito mensile, CAGR 15%):") + for dep in (0, 500, 1000, 2000, 5000): + eq, months = 600.0, 0 + target_cap = TARGET_USD_YR / 0.15 + while eq < target_cap and months < 12 * 60: + eq = eq * (1 + 0.15) ** (1 / 12) + dep * EURUSD + months += 1 + yrs = months / 12 + lab = f"{yrs:.1f} anni" if months < 720 else ">60 anni" + print(f" deposito EUR {dep:>5}/mese -> {lab}") + print("\n VERDETTO A: da $600, 'qualche mese' per compounding NON esiste. Il capitale") + print(" proprio arriva al target solo con depositi da migliaia/mese per molti anni.") + + +# ----------------------------------------------------------------------------- B +def part_b(book: pd.Series) -> None: + m = metrics(book) + print("\n" + "=" * 100) + print(" B) LA LEVA NON E' LA SCORCIATOIA") + print("=" * 100) + sh = m["sharpe"] + vol = book.std() * np.sqrt(365) + cagr = (1 + book).prod() ** (365 / len(book)) - 1 + print(f" book Deribit (TP01 .75 + SKH01 .25) modellato: Sharpe {sh:.2f} vol {vol:.0%} CAGR {cagr:.1%}") + for cap_usd in (600, 2000, 5000, 20000): + need = TARGET_USD_YR / cap_usd # rendimento annuo richiesto + lev = need / max(cagr, 1e-9) # multiplo di leva sul book + vol_imp = vol * lev + print(f" su ${cap_usd:>6,}: servono {need:>7.0%}/anno -> leva ~{lev:>5.1f}x il book " + f"-> vol implicita ~{vol_imp:>6.0%} -> rovina quasi certa" if lev > 3 else + f" su ${cap_usd:>6,}: servono {need:>7.0%}/anno -> leva ~{lev:>5.1f}x il book " + f"-> vol implicita ~{vol_imp:>6.0%}") + print(" VERDETTO B: il target su capitale piccolo implica vol a 3 cifre = rovina.") + print(" (Kelly gia' documentato: il book gira a ~0.01-0.27 Kelly a seconda dello sleeve.)") + + +# ----------------------------------------------------------------------------- C +def _block_bootstrap_paths(r: np.ndarray, n_days: int, n_paths: int, block: int = 20, + drift_scale: float = 1.0) -> np.ndarray: + """Percorsi bootstrap a blocchi dai ritorni giornalieri reali del book. + + drift_scale < 1 = haircut de-luck: riduce la media conservando la vol + (r' = (r - mu) + mu*scale). + """ + mu = r.mean() + r_adj = (r - mu) + mu * drift_scale + n = len(r_adj) + n_blocks = int(np.ceil(n_days / block)) + starts = RNG.integers(0, n - block, size=(n_paths, n_blocks)) + idx = (starts[:, :, None] + np.arange(block)[None, None, :]).reshape(n_paths, -1)[:, :n_days] + return r_adj[idx] + + +def _simulate_eval(paths: np.ndarray, target: float, max_dd: float, daily_loss: float, + trailing: bool, max_days: int) -> dict: + """Simula una evaluation: passa se equity tocca (1+target) prima di violare + il max drawdown (statico da 1.0 o trailing dal max) o il daily loss limit.""" + n_paths, n_days = paths.shape + eq = np.cumprod(1 + paths, axis=1) + hwm = np.maximum.accumulate(np.maximum(eq, 1.0), axis=1) if trailing else 1.0 + dd_breach = eq < (hwm * (1 - max_dd) if trailing else (1 - max_dd)) + dl_breach = paths < -daily_loss + fail = dd_breach | dl_breach + passed = eq >= 1 + target + + first_fail = np.where(fail.any(axis=1), fail.argmax(axis=1), max_days + 1) + first_pass = np.where(passed.any(axis=1), passed.argmax(axis=1), max_days + 1) + ok = first_pass < first_fail + p_pass = ok.mean() + days = np.median(first_pass[ok]) if ok.any() else np.nan + return {"p_pass": p_pass, "median_days": days, + "p_fail": (first_fail <= np.minimum(first_pass, max_days)).mean()} + + +def part_c(book: pd.Series) -> None: + print("\n" + "=" * 100) + print(" C) PROP-FIRM EVALUATION — Monte Carlo sui ritorni REALI del book (block bootstrap 20g)") + print("=" * 100) + r = book.to_numpy() + n_paths, max_days = 20_000, 365 + + rules = [ + ("target 8% / DD 10% statico / daily 4%", 0.08, 0.10, 0.04, False), + ("target 10% / DD 10% statico / daily 5%", 0.10, 0.10, 0.05, False), + ("target 10% / DD 10% TRAILING / daily 5%", 0.10, 0.10, 0.05, True), + ] + for drift_scale, lab in ((1.0, "book modellato"), (0.6, "de-luck x0.6 (onesto)")): + base = _block_bootstrap_paths(r, max_days, n_paths, drift_scale=drift_scale) + for lev in (1.0, 1.5, 2.0): + paths = base * lev + print(f"\n --- {lab}, leva eval x{lev:.1f} (vol ~{book.std() * np.sqrt(365) * lev:.0%}) ---") + for name, tgt, dd, dl, tr in rules: + res = _simulate_eval(paths, tgt, dd, dl, tr, max_days) + d = f"{res['median_days']:.0f}g" if np.isfinite(res["median_days"]) else "n/a" + print(f" {name:<44} P(pass entro 1a) {res['p_pass']:>5.1%} mediana {d}") + + # fase funded: 100k, split 80%, anno di ritorni; conto perso se DD>10% + print("\n --- fase FUNDED: $100k nozionale, split 80%, orizzonte 1 anno ---") + for drift_scale, lab in ((1.0, "book modellato"), (0.6, "de-luck x0.6")): + paths = _block_bootstrap_paths(r, 365, n_paths, drift_scale=drift_scale) + eq = np.cumprod(1 + paths, axis=1) + hwm = np.maximum.accumulate(np.maximum(eq, 1.0), axis=1) + blown = (eq < hwm * 0.90) | (paths < -0.05) + alive = ~blown.any(axis=1) + pnl = (eq[:, -1] - 1) * 100_000 * 0.80 + pnl_alive = pnl[alive] + q = np.percentile(pnl_alive, [10, 50, 90]) if alive.any() else [np.nan] * 3 + print(f" {lab:<24} P(conto vivo a 1a) {alive.mean():>5.1%} payout se vivo " + f"p10/p50/p90: EUR {eur_day(q[0]) :>5.1f} / {eur_day(q[1]):>5.1f} / {eur_day(q[2]):>5.1f} al giorno") + print("\n NB: regole reali (consistency, min trading days, no-bot) possono uccidere lo stile") + print(" low-frequency — il verdetto finale dipende dalla ricerca web sulle firm 2026.") + + +# ----------------------------------------------------------------------------- D +def part_d() -> None: + print("\n" + "=" * 100) + print(" D) VAULT / COPY-TRADING — perf-share 10% sopra HWM") + print("=" * 100) + for cagr in (0.10, 0.15, 0.20): + tvl = TARGET_USD_YR / (cagr * 0.10) + print(f" CAGR {cagr:.0%}: per 50 EUR/g servono ${tvl / 1e6:,.1f}M di TVL altrui") + print(" VERDETTO D: TVL a 7 cifre richiede anni di track record pubblico, non mesi.") + + +def main() -> None: + port = StrategyPortfolio(deribit_book_sleeves()) + book = port.combined_daily().dropna() + book = book[book.index >= "2019-03-01"] + m = metrics(book) + print(f"book Deribit live (modellato): {len(book)} giorni {book.index[0].date()} -> " + f"{book.index[-1].date()} Sharpe {m['sharpe']:.2f} maxDD {m['maxdd']:.1%}") + part_a() + part_b(book) + part_c(book) + part_d() + print("\n" + "=" * 100) + print(" SINTESI ONESTA: 50 EUR/g in qualche mese e' fuori portata del capitale proprio.") + print(" L'unica via quantificabile in MESI e' moltiplicare il nozionale gestito (prop/") + print(" funded), con P(successo) e regole da verificare; tutto il resto e' anni+capitale.") + print("=" * 100) + + +if __name__ == "__main__": + main() diff --git a/scripts/research/r0724_statarb_deploy_gate.py b/scripts/research/r0724_statarb_deploy_gate.py new file mode 100644 index 0000000..6b324d1 --- /dev/null +++ b/scripts/research/r0724_statarb_deploy_gate.py @@ -0,0 +1,79 @@ +"""r0724_statarb_deploy_gate — gate di deploy PRE-REGISTRATO per STATARB-RESID (2026-07-24). + +PERCHE' PRE-REGISTRATO: la regola si fissa a forward-day 25 (oggi), PRIMA di vedere +il giorno 90. Decidere "a occhio" dopo aver visto il numero = selection-on-forward, +lo stesso bias che il progetto ha gia' pagato (gate SELECTION-ON-HOLDOUT, 2026-06-29). + +REGOLA (immutabile — ogni modifica va motivata nel diario come violazione): + - Data decisione: 2026-09-27 (90 giorni forward dal 2026-06-29). Non prima. + - Metrica: Sharpe annualizzato di net_modeled su TUTTA la finestra forward + (nessuna sottofinestra), da data/paper_statarb/returns.jsonl. + - Esiti: + Sharpe >= 0.5 -> CANDIDATO AL DEPLOY: proporre peso {10,15,20}% del book + Deribit, giudicato con weights_tilt_null + stima maxDD + combinata < 15%. Deploy solo se il tilt-null passa. + 0 <= Sh < 0.5 -> ESTENDI una sola volta di altri 90g (decisione 2026-12-26). + Alla seconda scadenza: o >=0.5 o RITIRO. Niente terza proroga. + Sharpe < 0 -> RITIRO dal forward-monitor (l'edge DSR 0.929 non ha confermato). + - Guardie accessorie (tutte richieste per il deploy): + * fill-haircut cumulato |MODELED - REAL| < 0.5 pp (eseguibilita' reale a $600); + * config invariata (W=45, sgn=+1) — ogni ritocco azzera la finestra; + * maxDD forward < 10% (coerenza col profilo di ricerca, DD backtest ~11%). + +Uso: `uv run python scripts/research/r0724_statarb_deploy_gate.py` +(si puo' lanciare in qualsiasi momento: ricorda la data-decisione e valuta lo stato). +""" +from __future__ import annotations + +import json +from datetime import date +from pathlib import Path + +import numpy as np + +ROOT = Path(__file__).resolve().parents[2] +RETURNS = ROOT / "data" / "paper_statarb" / "returns.jsonl" + +START = date(2026, 6, 29) # inizio forward (config congelata) +DECISION = date(2026, 9, 27) # 90g forward — PRIMA data utile di decisione +DECISION_EXT = date(2026, 12, 26) # unica proroga ammessa +SH_DEPLOY = 0.5 +HAIRCUT_MAX_PP = 0.5 +DD_MAX = 0.10 + + +def main() -> None: + rows = [json.loads(x) for x in RETURNS.read_text().splitlines() if x.strip()] + rm = np.array([r["net_modeled"] for r in rows]) + rr = np.array([r["net_real"] for r in rows]) + n = len(rm) + sh = float(rm.mean() / rm.std() * np.sqrt(365)) if rm.std() > 0 else 0.0 + eq = np.cumprod(1 + rm) + dd = float(np.max((np.maximum.accumulate(eq) - eq) / np.maximum.accumulate(eq))) + haircut_pp = abs(float((np.prod(1 + rm) - np.prod(1 + rr)) * 100)) + today = date.today() + + print("=" * 88) + print(" STATARB-RESID — gate di deploy PRE-REGISTRATO (fissato 2026-07-24, forward-day 25)") + print("=" * 88) + print(f" finestra forward : {rows[0]['dt'][:10]} -> {rows[-1]['dt'][:10]} ({n} barre)") + print(f" Sharpe fwd (mod) : {sh:+.2f} (soglia deploy >= {SH_DEPLOY})") + print(f" maxDD fwd : {dd:.1%} (guardia < {DD_MAX:.0%})") + print(f" fill-haircut cum : {haircut_pp:.2f} pp (guardia < {HAIRCUT_MAX_PP} pp)") + print(f" data decisione : {DECISION} (proroga unica: {DECISION_EXT})") + if today < DECISION: + print(f"\n -> NESSUNA DECISIONE OGGI ({today}): mancano {(DECISION - today).days} giorni.") + print(" Il numero corrente NON autorizza deploy anticipato (regola pre-registrata).") + else: + if sh >= SH_DEPLOY and dd < DD_MAX and haircut_pp < HAIRCUT_MAX_PP: + print("\n -> GATE SUPERATO: procedere con weights_tilt_null sui pesi candidati " + "{10,15,20}% e stima maxDD combinata (<15%) prima del deploy.") + elif sh >= 0: + print("\n -> SOTTO SOGLIA ma >=0: estensione unica fino al " + f"{DECISION_EXT} (se gia' estesa: RITIRO).") + else: + print("\n -> Sharpe forward NEGATIVO: RITIRO dal forward-monitor.") + + +if __name__ == "__main__": + main()