diff --git a/CLAUDE.md b/CLAUDE.md index ea477e1..af7e5e4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -496,6 +496,21 @@ Prima ondata di ricerca onesta su BTC/ETH certificati (5 track, harness condivis rendita, taglia il maxDD 15.3→12.0%, unico split che regge coi costi). ⚠ Il confronto FAVORISCE il crypto per costruzione (7 anni con 2 bull vs 30 anni di GTAA); tassi e aliquote (26%/33%) sono assunzioni dichiarate, da confermare col commercialista (non un parere fiscale). + (7) **HYROTRADER nello specifico** (`r0725_hyro.py`): la firm meglio classificata per questo book + (API reale da funded, nessun limite di tempo, **weekend consentito** — e il weekend porta il 38% + del gross di TP01). **Ipotesi REFUTATA: la regola di consistency 40% NON morde** (costo ≈0pp a + ogni leva/lente) perche' il book accumula il 10% in 130-300 giorni → nessun giorno si avvicina al + 40% del cumulato; ⚠ prima di dichiararlo ho dovuto **provare che il controllo funziona** (un + "costo 0" puo' essere un bug): su un book con profitto concentrato in 1-2 giorni la regola porta + P(pass) da >90% a <5% (test cablato). **Il vincolo vero e' il maxDD 6% STATICO**: config A (book + live) ha maxDD 9.5% > 6% = strutturalmente incompatibile a leva piena — intraday a 1.0x + sopravvive l'**1.4%**; config B (+XS01) ha maxDD 6.2%, Sharpe 1.13. **Funded consigliato: config B + a 0.50x** (intraday P(vivo) 71% vs 32% a 0.75x, E[payout] €7.69 vs €8.66/g — la sopravvivenza + COMPONE su piu' anni). EV del biglietto $100k **positivo in entrambe le lenti** (+$6.789 + close-only, +$1.166 intraday), ma ~69% di perdere la fee nella lente pessimista. Cap $200k/trader + ⇒ ~€15-30/g max → per €50/g servono piu' firm (punto 5). ⚠ Discrepanza col 24/07 (P(vivo) A@0.75x + 58% vs 10% mio): mio wick indipendente = piu' severo + finestra 2024+ dove A e' debole; **la + direzione concorda, sul LIVELLO il numero del 24/07 (recon MTM vero) e' piu' affidabile**. ⚠ Bug catturato in sessione: base di prelievo funded aggiornata giornalmente come HWM mobile → guadagno al checkpoint ≈ 0 → **payout $230/anno invece di ~$7.000**; regole vere = max-loss STATICO dal saldo iniziale + base ripristinata dopo il prelievo. Test di regressione cablato. diff --git a/docs/diary/2026-07-25-rendita-capcurve-prop.md b/docs/diary/2026-07-25-rendita-capcurve-prop.md index 78e704e..aaf0b68 100644 --- a/docs/diary/2026-07-25-rendita-capcurve-prop.md +++ b/docs/diary/2026-07-25-rendita-capcurve-prop.md @@ -289,6 +289,69 @@ vanno confermate col commercialista (**non è un parere fiscale**). --- +## 8) HYROTRADER nello specifico (`r0725_hyro.py`) + +La firm che il 24/07 aveva messo prima in classifica per *questo* book: unica con **API reale** da +funded (sub-account Bybit) → il book gira automatico; **nessun limite di tempo** sull'eval (una +strategia lenta non è penalizzata); **weekend consentito** — non banale, l'ondata 2026-07-17 ha +misurato che il weekend porta il **38% del gross di TP01**. Biglietti $25k/$249, $50k/$379, +$100k/$579, cap $200k, split 80%, fee rimborsata al pass. + +Due regole mordono proprio noi, e nessuna analisi le aveva modellate: **maxDD 6% STATICO** e +**consistency 40%** (nessun giorno > 40% del profitto totale, in eval). + +**Ipotesi refutata: la consistency NON morde.** Avevo previsto una tagliola (SKH01 ha P&L a +scalino, TP01 fa strappi di trend). Misurato: **costo ≈ 0pp** a ogni leva e in entrambe le lenti. +Ragione strutturale: il book accumula il 10% in **130-300 giorni**, quindi nessun singolo giorno +si avvicina al 40% del cumulato. ⚠️ Prima di dichiararlo ho dovuto **provare che il controllo +funziona** (un "costo 0" può essere un bug): `test_consistency_MORDE_su_un_book_grumoso` mostra +che su un book con profitto concentrato in 1-2 giorni la regola porta P(pass) da >90% a <5%. Il +meccanismo c'è; è il book a non essere grumoso abbastanza. *(Il primo probe era sbagliato — strappi +ripetuti si diluiscono a vicenda, e sim_eval fa block-bootstrap, non replay: due iterazioni di test +prima di avere un discriminante valido.)* + +**Il vincolo vero è il DD 6% statico.** Finestra comune 2024+, de-luck ×0.6: + +| | Sharpe | CAGR | vol | maxDD | +|---|---|---|---|---| +| A — book live (TP01/SKH01) | 0.74 | 7.2% | 10.1% | 9.5% | +| B — + XS01 | **1.13** | 10.0% | 8.8% | **6.2%** | + +Config A ha maxDD 9.5% contro un limite di 6%: **strutturalmente incompatibile a leva piena**. + +**Funded $100k, P(vivo 1 anno) / €-giorno:** + +| config | leva | close-only (tetto) | intraday (pavimento) | +|---|---|---|---| +| A | 0.50× | 80.2% / €8.33 | 44.0% / €6.86 | +| A | 0.75× | 51.1% / €12.21 | **10.1%** / — | +| A | 1.00× | 27.1% / €13.89 | **1.4%** / — | +| B | 0.50× | 98.5% / €8.69 | **71.0%** / €7.69 | +| B | 0.75× | 85.4% / €12.78 | 31.6% / €8.66 | +| B | 1.00× | 66.0% / €16.20 | 9.9% / — | + +**EV del biglietto** (config B, eval 1.0×, funded a leva ottima, 1 anno): close-only P(pass) 73.0% +→ EV **+$6.789** sul biglietto $100k; intraday P(pass) 31.1% → EV **+$1.166**. **Positivo in +entrambe le lenti** — ma con ~69% di probabilità di perdere la fee nella lente pessimista. + +**Raccomandazione operativa:** +1. **Config B (con XS01), non il book live attuale.** A 1.0× intraday config A sopravvive l'1.4%: + mandarci il book a 2 sleeve a leva piena è morte quasi certa. +2. **Funded a 0.50×** (non 0.75× come indicava il 24/07 per la config A): E[payout] scende poco + (€7.69 vs €8.66/g) ma la sopravvivenza raddoppia (71% vs 32%) — e su orizzonti pluriennali la + sopravvivenza **compone**, un conto bustato smette di produrre per sempre. +3. Eval a 0.75-1.0×; biglietto $25-50k come test economico — l'EV scala **linearmente** con la + taglia, quindi la ragione per partire piccoli è varianza e apprendimento, non EV. +4. **Cap $200k/trader ⇒ ~€15-30/g anche negli scenari buoni.** Per €50/g servono più firm → §6. + +⚠️ **Discrepanza dichiarata col 24/07:** lì il funded di config A a 0.75× dava P(vivo) 58%, qui +10%. Due cause note: (a) il mio wick è a estrazione indipendente (più severo — §6); (b) finestra +2024+, dove config A è debole (Sharpe 0.74 vs 0.99 sulla finestra piena). **La direzione concorda +(meno leva è meglio), il livello no** — e il numero del 24/07 usa il recon MTM vero, quindi sul +livello è più affidabile del mio. + +--- + ## Bug catturato in sessione **Modello funded, base di prelievo aggiornata ogni giorno come un HWM mobile** → al checkpoint diff --git a/scripts/research/r0725_hyro.py b/scripts/research/r0725_hyro.py new file mode 100644 index 0000000..52ba4d2 --- /dev/null +++ b/scripts/research/r0725_hyro.py @@ -0,0 +1,210 @@ +"""r0725_hyro — HYROTRADER nello specifico: config, leva e la regola di CONSISTENCY (2026-07-25). + +La ricerca del 24/07 ha messo HyroTrader prima in classifica per QUESTO book: + * unica con **API reale** da funded (sub-account Bybit) -> il book gira automatico, non a mano; + * **nessun limite di tempo** sull'eval -> una strategia lenta non e' penalizzata; + * **weekend consentito** -> non banale: l'ondata 2026-07-17 ha misurato che il weekend porta + il **38% del gross di TP01** nel 31% del tempo (de-esporlo e' attivamente dannoso); + * funding = mercato (stessa economia del book Deribit), fee rimborsata al pass, payout ~12h. +Biglietti 1-step: $25k/$249, $50k/$379, $100k/$579. Cap $200k per trader. + +Ma ha due regole che mordono ESATTAMENTE questo book, e nessuna analisi le aveva modellate: + 1. **max drawdown 6% STATICO** (dal saldo iniziale) — il book de-luckato ha maxDD 15.4% a 1.0x; + 2. **CONSISTENCY 40% in eval**: nessun singolo giorno puo' valere piu' del 40% del profitto + totale. E' una tagliola per un book LENTO e GRUMOSO: SKH01 ha P&L per-trade a scalino + (caveat noto "equity daily-step") e TP01 fa mesi piatti seguiti da strappi di trend. Se il + target del 10% arriva con 2-3 giornatone, l'eval **non passa**: si deve continuare a + tradare finche' il profitto totale si diluisce — esponendosi altro tempo al DD 6%. + +Questo script misura: P(pass) con e senza consistency, la leva ottima, e l'EV di ogni biglietto. + +ONESTA': doppia lente. CLOSE-ONLY = tetto ottimista; INTRADAY = pavimento (gap lognormale +calibrato sul recon MTM 1h del 24/07, ma estratto indipendente dal rendimento del giorno -> +breach spuri: vedi il caveat in r0725_prop_ladder.py). La verita' sta in mezzo. Finestra comune +2024+ (dove esiste XS01) = anche la finestra di scoperta di XS01 -> la config B e' favorita. + +Uso: `uv run python scripts/research/r0725_hyro.py` +""" +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)) +sys.path.insert(0, str(ROOT / "scripts" / "research")) + +from src.portfolio.portfolio import combine_outer, metrics # noqa: E402 +from src.portfolio.sleeves import _xsec_returns # noqa: E402 + +import r0724_skh_live_weight as skl # noqa: E402 +from r0725_capcurve import EURUSD, TAX_RATE, tp01_realistic # noqa: E402 +from r0725_prop_ladder import WICK_MU, WICK_SIGMA, WICK_REF_VOL # noqa: E402 + +# --- regole HyroTrader 1-step (ricerca 24/07, fonti primarie) +EV_TARGET, EV_DD, EV_DL = 0.10, 0.06, 0.04 +FU_ML, FU_DL = 0.06, 0.04 +CONSISTENCY = 0.40 # nessun giorno > 40% del profitto totale (solo in eval) +SPLIT = 0.80 +TICKETS = [(25_000, 249.0), (50_000, 379.0), (100_000, 579.0)] +CAP = 200_000 +DELUCK = 0.6 +RNG = np.random.default_rng(20260725) + + +def _d(s: pd.Series) -> pd.Series: + s = s.dropna().sort_index() + if s.index.tz is None: + s.index = s.index.tz_localize("UTC") + return s + + +def configs() -> dict: + skh = _d(skl.skh_book(0, "hourly")) + tp = _d(tp01_realistic(100_000 * 0.75)) + xs = _d(_xsec_returns()).resample("1D").apply(lambda x: (1 + x).prod() - 1) + lo = max(tp.index.min(), skh.index.min(), xs.index.min()) + out = {} + for name, cols, w in ( + ("A book live (TP01/SKH01)", {"TP01": tp, "SKH01": skh}, {"TP01": .75, "SKH01": .25}), + ("B + XS01 (19 alt perp)", {"TP01": tp, "SKH01": skh, "XS01": xs}, + {"TP01": .55, "SKH01": .20, "XS01": .25}), + ): + s = combine_outer(cols, w, lo=lo) + s = s - (1.0 - DELUCK) * float(s.mean()) + out[name] = s + return out + + +def _paths(r: np.ndarray, n_days: int, n_paths: int, block: int = 20) -> np.ndarray: + nb = int(np.ceil(n_days / block)) + st = RNG.integers(0, len(r) - block, size=(n_paths, nb)) + idx = (st[:, :, None] + np.arange(block)[None, None, :]).reshape(n_paths, -1)[:, :n_days] + return r[idx] + + +def _wick(shape, vol: float) -> np.ndarray: + scale = float(np.clip(vol / WICK_REF_VOL, 0.0, 3.0)) + return -np.exp(RNG.normal(WICK_MU, WICK_SIGMA, size=shape)) * scale + + +def sim_eval(r: np.ndarray, k: float, consistency: bool, intraday: bool, + max_days: int = 540, n_paths: int = 8000) -> dict: + """Eval HYRO. Il pass richiede equity >= 1+target E (se consistency) che il MIGLIOR giorno + non superi il 40% del profitto cumulato. Breach su equity intraday.""" + R = _paths(r, max_days, n_paths) * k + G = _wick(R.shape, float(r.std() * np.sqrt(365)) * k) if intraday else np.zeros_like(R) + eq = np.ones(n_paths) + best_day = np.zeros(n_paths) # miglior guadagno giornaliero in USD-equity + alive = np.ones(n_paths, bool) + passed = np.zeros(n_paths, bool) + day_pass = np.full(n_paths, max_days + 1, int) + for t in range(max_days): + prev = eq + eq = np.where(alive & ~passed, eq * (1 + R[:, t]), eq) + low = np.where(alive & ~passed, prev * (1 + R[:, t] + G[:, t]), eq) + gain = np.maximum(eq - prev, 0.0) + best_day = np.where(alive & ~passed, np.maximum(best_day, gain), best_day) + bust = alive & ~passed & ((low < 1.0 - EV_DD) | ((R[:, t] + G[:, t]) < -EV_DL)) + alive &= ~bust + profit = eq - 1.0 + ok = alive & ~passed & (profit >= EV_TARGET) + if consistency: + ok &= best_day <= CONSISTENCY * np.maximum(profit, 1e-9) + day_pass[ok] = t + passed |= ok + return dict(p_pass=float(passed.mean()), + median_days=float(np.median(day_pass[passed])) if passed.any() else float("nan")) + + +def sim_funded(r: np.ndarray, k: float, intraday: bool, notional: float, + years: int = 1, n_paths: int = 8000) -> dict: + n_days = years * 365 + R = _paths(r, n_days, n_paths) * k + G = _wick(R.shape, float(r.std() * np.sqrt(365)) * k) if intraday else np.zeros_like(R) + eq = np.ones(n_paths) + alive = np.ones(n_paths, bool) + payout = np.zeros(n_paths) + for t in range(n_days): + prev = eq + eq = np.where(alive, eq * (1 + R[:, t]), eq) + low = np.where(alive, prev * (1 + R[:, t] + G[:, t]), eq) + bust = alive & ((low < 1.0 - FU_ML) | ((R[:, t] + G[:, t]) < -FU_DL)) + alive &= ~bust + if (t + 1) % 30 == 0: + g = np.where(alive & (eq > 1.0), eq - 1.0, 0.0) + payout += g * SPLIT + eq = np.where(alive & (eq > 1.0), 1.0, eq) + usd = payout * notional + return dict(p_alive=float(alive.mean()), e_payout=float(usd.mean()), + eur_day=float(usd.mean() * (1 - TAX_RATE) / EURUSD / 365.0)) + + +def main() -> None: + cfg = configs() + print("=" * 100) + print(" HYROTRADER — config, leva e la regola di CONSISTENCY 40%") + print("=" * 100) + print(f" regole: target {EV_TARGET:.0%} | maxDD {EV_DD:.0%} STATICO | daily {EV_DL:.0%} | " + f"consistency {CONSISTENCY:.0%} (eval) | split {SPLIT:.0%} | cap ${CAP:,}") + for n, s in cfg.items(): + m = metrics(s) + print(f" {n:>28}: Sharpe {m['sharpe']:>5.2f} CAGR {m['cagr']:>6.1%} " + f"vol {s.std()*np.sqrt(365):>5.1%} maxDD {m['maxdd']:>5.1%}") + + for intraday in (False, True): + lens = "INTRADAY (pavimento)" if intraday else "CLOSE-ONLY (tetto)" + print("\n" + "-" * 100) + print(f" EVAL — {lens}: quanto costa la regola di CONSISTENCY") + print("-" * 100) + print(f" {'config':>28} {'leva':>5} {'P(pass) senza':>14} {'P(pass) CON':>12} " + f"{'costo regola':>13} {'giorni med':>11}") + for n, s in cfg.items(): + r = s.values.astype(float) + for k in (0.5, 0.75, 1.0): + a = sim_eval(r, k, consistency=False, intraday=intraday, n_paths=4000) + b = sim_eval(r, k, consistency=True, intraday=intraday, n_paths=4000) + print(f" {n:>28} {k:>5.2f} {a['p_pass']:>14.1%} {b['p_pass']:>12.1%} " + f"{b['p_pass']-a['p_pass']:>+13.1f}pp {b['median_days']:>11.0f}") + + print(f"\n FUNDED $100k — {lens}") + print(f" {'config':>28} {'leva':>5} {'P(vivo 1a)':>11} {'E[payout]':>11} {'EUR/g':>7}") + for n, s in cfg.items(): + r = s.values.astype(float) + for k in (0.5, 0.75, 1.0): + f = sim_funded(r, k, intraday, 100_000.0, n_paths=4000) + print(f" {n:>28} {k:>5.2f} {f['p_alive']:>11.1%} ${f['e_payout']:>10,.0f} " + f"{f['eur_day']:>7.2f}") + + # --- EV del biglietto (lente intraday = quella su cui si decide, piu' close-only come tetto) + print("\n" + "-" * 100) + print(" EV DEL BIGLIETTO — config B, eval a leva 1.0x, funded a leva ottima, 1 anno") + print(" (la fee e' rimborsata al pass: il costo atteso e' fee x P(fallire l'eval))") + print("-" * 100) + r = cfg["B + XS01 (19 alt perp)"].values.astype(float) + for intraday in (False, True): + lens = "intraday" if intraday else "close-only" + pe = sim_eval(r, 1.0, consistency=True, intraday=intraday, n_paths=4000)["p_pass"] + best = None + for k in (0.5, 0.75, 1.0): + f = sim_funded(r, k, intraday, 100_000.0, n_paths=4000) + if best is None or f["e_payout"] > best[1]["e_payout"]: + best = (k, f) + print(f"\n [{lens}] P(pass eval) {pe:.1%} | leva funded ottima {best[0]:.2f}x " + f"(P(vivo) {best[1]['p_alive']:.1%})") + print(f" {'biglietto':>12} {'prezzo':>8} {'E[payout] 1a':>14} {'costo atteso':>13} " + f"{'EV':>10} {'EUR/g atteso':>13}") + for size, price in TICKETS: + scale = size / 100_000.0 + ev_payout = pe * best[1]["e_payout"] * scale + cost = price * (1.0 - pe) # la fee e' rimborsata al pass -> si perde solo se si fallisce + ev = ev_payout - cost + print(f" ${size:>11,} ${price:>7,.0f} ${ev_payout:>13,.0f} " + f"${cost:>12,.0f} ${ev:>9,.0f} {ev*(1-TAX_RATE)/EURUSD/365:>13.2f}") + + +if __name__ == "__main__": + main() diff --git a/tests/test_capcurve.py b/tests/test_capcurve.py index 28ed9e3..6ad45ca 100644 --- a/tests/test_capcurve.py +++ b/tests/test_capcurve.py @@ -180,3 +180,48 @@ def test_senza_cassa_nessun_conto_e_nessun_reddito(pl): assert r["p_zero"] == 1.0 finally: pl.START_EUR = old + + +# ------------------------------------------- 5) regola di CONSISTENCY HyroTrader +@pytest.fixture(scope="module") +def hy(): + return _load("r0725_hyro", "scripts/research/r0725_hyro.py") + + +def test_consistency_MORDE_su_un_book_grumoso(hy): + """PROVA CHE IL CONTROLLO FUNZIONA. Un UNICO giorno da +12% porta da solo tutto il profitto + e non arriva mai altro: con la regola 40% quel path non deve passare MAI (0.12 > 0.40*0.12), + senza regola passa subito. Se questo test non discrimina, il 'costo ~0pp' misurato sul book + vero sarebbe un bug, non un risultato. + + NB sul probe: sim_eval fa BLOCK BOOTSTRAP (blocchi di 20g), non replay -> uno strappo isolato + finirebbe solo in alcuni path. Qui gli strappi sono spaziati di 20 giorni = esattamente uno per + blocco, cosi' OGNI path ne vede uno ogni 20 giorni in modo deterministico. In 40 giorni: + senza regola si passa al 1o strappo (+12% >= target 10%); con la regola servono 3 strappi + perche' il giorno migliore scenda sotto il 40% del profitto cumulato -> oltre la finestra. + """ + r = np.zeros(200) + r[::20] = 0.12 + senza = hy.sim_eval(r, 1.0, consistency=False, intraday=False, max_days=40, n_paths=200) + con = hy.sim_eval(r, 1.0, consistency=True, intraday=False, max_days=40, n_paths=200) + assert senza["p_pass"] > 0.9, "senza regola un +12% secco deve passare il target 10%" + assert con["p_pass"] < 0.05, "la regola 40% deve bloccare un profitto concentrato in 1-2 giorni" + + +def test_consistency_non_morde_su_un_book_liscio(hy): + """Controprova: un book che accumula il target in tanti giorni piccoli non e' toccato + dalla regola. E' il motivo per cui sul book vero il costo misurato e' ~0.""" + r = np.full(400, 0.0008) # ~+0.08%/g: il 10% arriva in ~120 giorni + senza = hy.sim_eval(r, 1.0, consistency=False, intraday=False, n_paths=200) + con = hy.sim_eval(r, 1.0, consistency=True, intraday=False, n_paths=200) + assert abs(con["p_pass"] - senza["p_pass"]) < 0.02 + + +def test_dd_statico_6pct_e_binding(hy): + """Il maxDD 6% STATICO di HYRO e' il vincolo vero: un book con DD storico sopra il 6% + non puo' girare a leva piena. Sanity: alzando la leva la sopravvivenza funded scende.""" + cfg = hy.configs() + r = list(cfg.values())[0].values.astype(float) + lo = hy.sim_funded(r, 0.5, False, 100_000.0, n_paths=400)["p_alive"] + hi = hy.sim_funded(r, 1.0, False, 100_000.0, n_paths=400)["p_alive"] + assert lo > hi