diff --git a/config/live.json b/config/live.json index 84715ca..7f3841f 100644 --- a/config/live.json +++ b/config/live.json @@ -1,7 +1,9 @@ { - "_nota": "Config esecuzione LIVE del BOOK DERIBIT (TP01+SKH01 nettati in software). execution_enabled=true + --execute -> ordini REALI. ARMATO 2026-06-23: esecutore scripts/live/book_execute.py via cron ORARIO scripts/cron_book.sh (SKH01 e' a 230m). cap/asset $300 split 75/25, disaster-SL on-book -30% sulla posizione netta. Tutto flat all'arming -> nessun ordine finche' un segnale non arma.", + "_nota": "Config esecuzione LIVE del BOOK DERIBIT (TP01+SKH01 nettati in software). execution_enabled=true + --execute -> ordini REALI. ARMATO 2026-06-23: esecutore scripts/live/book_execute.py via cron ORARIO scripts/cron_book.sh (SKH01 e' a 230m). disaster-SL on-book -30% sulla posizione netta. Tutto flat all'arming -> nessun ordine finche' un segnale non arma.", + "_nota_cap": "Cap notional per-asset DINAMICO (frontiera 2026-07-03): con max_notional_per_asset_frac=0.5 il cap = equity/2, cosi' cresce col capitale e un deposito non resta strozzato. A ~$600 equity/2=~$300 -> INERTE (identico al vecchio cap fisso). Il cap dinamico si usa solo con equity reale fidata; su fallback/offline si ripiega su max_notional_per_asset_usd (protezione downside). Per tornare al cap fisso: rimuovere max_notional_per_asset_frac.", "execution_enabled": true, "max_notional_per_asset_usd": 300, + "max_notional_per_asset_frac": 0.5, "min_order_usd": 5, "disaster_sl_pct": 0.30 } diff --git a/docs/diary/2026-07-03-cap-dynamic.md b/docs/diary/2026-07-03-cap-dynamic.md new file mode 100644 index 0000000..22dfea4 --- /dev/null +++ b/docs/diary/2026-07-03-cap-dynamic.md @@ -0,0 +1,47 @@ +# 2026-07-03 — Cap notional per-asset DINAMICO (= equity/2): operazionalizza la decisione frontier + +**Contesto.** La frontiera del 2026-07-03 (`docs/diary/2026-07-03-book-frontier.md`) aveva identificato +**una sola** azione config con impatto reale: portare `max_notional_per_asset_usd` da $300 fisso a +**equity/2**, perché il cap fisso strozza il book man mano che si deposita (paradosso: più capitale = +book più freddo — a $5k il cap $300 fa girare il libro al ~49% del target). Il diario la lasciava +"da applicare AL deposito". Qui la **operazionalizzo in codice** rendendo il cap dinamico, così è +sempre corretta e non richiede un edit manuale al deposito. + +## Cosa è cambiato + +- **`src/live/book.py` — `_cap()`**: se in config c'è `max_notional_per_asset_frac` (opt-in), il cap + per-asset diventa **`equity × frac`** (0.5 → equity/2). Senza la chiave → comportamento vecchio + (cap fisso `max_notional_per_asset_usd`). Il call site in `book_report()` passa + `equity`, `real_equity`, `eq_fallback` a `_cap()`. +- **`config/live.json`**: aggiunto `max_notional_per_asset_frac: 0.5` (+ nota). `max_notional_per_asset_usd` + resta come **cap fisso di fallback**. +- **`tests/test_book_live.py`**: +4 test (scaling con equity fidata, fallback su eq_fallback/real=None/ + equity 0, no-frac, integrazione online). Suite: **172 passed**. + +## Il guardrail di sicurezza (il punto delicato) + +Il cap dinamico si usa **SOLO con equity reale fidata** (letta dal conto). Se l'equity reale non è +leggibile — `eq_fallback` attivo, `real_equity=None`, offline, o equity ≤ 0 — si **torna al cap fisso +$300**. Motivo: in fallback `equity` è il paper-cap ($2000 ipotetico); calcolare `equity/2 = $1000` su +un conto di importo reale **ignoto** sizerebbe troppo. Preserva esattamente la protezione downside che +il test `test_book_execute_eq_fallback_warns_but_proceeds` verificava (l'hard-cap $/asset protegge +quando E è ignota). Regola: **equity/2 solo quando ci fidiamo di E; altrimenti $300 fisso.** + +## Verifica + +- **Live dry-run reale** (nessun `--execute`, nessun ordine): `cap/asset **$299**` = $598.06/2 → + **INERTE** oggi (≡ vecchio $300). Book flat, HOLD. +- Offline / `equity_override`: cap resta **$300 fisso** (nessuna equity reale da fidarsi) → il test + di parità pre-esistente regge invariato. +- A $5.000 di equity il cap diventerebbe automaticamente **$2.500** (equity/2), rimuovendo il + throttling che la frontiera aveva quantificato. + +## Impatto e limiti + +- **Già attivo**: il cron orario (`cron_book.sh`) esegue dal working tree → il prossimo run legge già + `frac=0.5`. Nessun deploy separato. **Inerte a $600**, si "accende" da sé quando l'equity cresce. +- **NON avvicina €50/g**: la frontiera lo aveva già detto — è una miglioria marginale (+€0.2/g @2k, + +€0.5/g @5k), il cui unico scopo è non lasciare capitale futuro strozzato. €50/g resta un problema + di **capitale + tempo** (~130k @ CAGR onesto), non di questo cap. +- Nessun cambio di **leva** (TP01 `target_vol=20%` invariato, la vol nativa ~10% emerge da sola — + coerente col verdetto frontier "gira alla vol nativa, non levare"). diff --git a/src/live/book.py b/src/live/book.py index ac3255b..e82772c 100644 --- a/src/live/book.py +++ b/src/live/book.py @@ -13,7 +13,11 @@ deribit_book_sleeves = 0.75*TP01 + 0.25*SKH01): tp_frac = target TP01 (causale, >=0, long-flat) da TrendPortfolio.current_target skh_sign = +1 long / -1 short / 0 flat da _skyhook_positions (book 230m) E = equity reale del conto (fallback paper se offline) - CAP = max_notional_per_asset_usd (config/live.json, $300) + CAP = cap notional per-asset. Fisso = max_notional_per_asset_usd ($300), OPPURE + DINAMICO = equity * max_notional_per_asset_frac (es. 0.5 -> equity/2) se la + frazione e' presente in config -> il cap cresce col capitale e non strozza il + book quando depositi (frontiera 2026-07-03). Dinamico SOLO con equity reale + fidata; su fallback/offline si torna al cap fisso (protezione downside). GLI EXIT DI SKH01 SONO IMPLICITI: _skyhook_positions() replica il book (ingressi + SL/TP/max_bars + non-overlap) sulla feed certificata fresca -> quando il trade va chiuso ritorna 'flat' -> skh_sign=0 @@ -42,9 +46,21 @@ W_SKH = 0.25 FLAT_USD = 1.0 -def _cap() -> float: +def _cap(equity: float | None = None, real_equity: float | None = None, + eq_fallback: str | None = None) -> float: + """Cap notional per-asset ($). Se in config c'e' `max_notional_per_asset_frac` (opt-in), il cap + diventa DINAMICO = equity * frac (es. equity/2) -> cresce col capitale, cosi' un deposito non + resta strozzato dal $300 fisso (vedi frontiera 2026-07-03). GUARDRAIL: il cap dinamico si usa + SOLO con equity reale fidata (letta dal conto); se l'equity reale non e' leggibile + (eq_fallback/offline, real_equity=None) si torna al cap FISSO `max_notional_per_asset_usd`, che + protegge il downside quando l'equity e' ignota. Senza `..._frac` in config -> sempre cap fisso.""" cfg = json.loads(CONFIG.read_text()) if CONFIG.exists() else {} - return float(cfg.get("max_notional_per_asset_usd", 300.0)) + fixed = float(cfg.get("max_notional_per_asset_usd", 300.0)) + frac = cfg.get("max_notional_per_asset_frac") + if frac is None: + return fixed + trusted = (real_equity is not None) and (not eq_fallback) and (equity is not None) and (equity > 0) + return float(equity) * float(frac) if trusted else fixed def book_net_target(tp_frac: float, skh_sign: int, equity: float, cap: float, @@ -99,8 +115,8 @@ def book_report(offline: bool = False, equity_override: float | None = None, segnale 230m all'ultima barra chiusa, per l'esecuzione reale. Default False (feed certificato, deterministico: dashboard/test). TP01 resta sul certificato (è giornaliero).""" sh = shadow_report(offline=offline, equity_override=equity_override) - cap = _cap() equity = sh["equity"] + cap = _cap(equity=equity, real_equity=sh.get("real_equity"), eq_fallback=sh.get("eq_fallback")) load5m = None if live_feed: from src.live.livefeed import fresh_5m diff --git a/tests/test_book_live.py b/tests/test_book_live.py index 558c20e..c0aaf4c 100644 --- a/tests/test_book_live.py +++ b/tests/test_book_live.py @@ -36,6 +36,55 @@ def test_net_target_clamps_negative(): assert book_net_target(-5.0, 1, 600.0, 300.0) == 75.0 +# --------------------------------------------------------------------------- +# CAP DINAMICO (frontiera 2026-07-03): con max_notional_per_asset_frac in config il +# cap = equity*frac (cresce col capitale). GUARDRAIL: dinamico SOLO con equity reale +# fidata; su fallback/offline/no-frac -> cap FISSO (protezione downside). +# --------------------------------------------------------------------------- +import json as _json + + +def _write_cfg(monkeypatch, tmp_path, **keys): + import src.live.book as book + p = tmp_path / "live.json" + p.write_text(_json.dumps(keys)) + monkeypatch.setattr(book, "CONFIG", p) + return book + + +def test_cap_dynamic_scales_with_trusted_equity(monkeypatch, tmp_path): + book = _write_cfg(monkeypatch, tmp_path, max_notional_per_asset_usd=300, max_notional_per_asset_frac=0.5) + # equity reale fidata -> cap = equity/2 (cresce col capitale) + assert book._cap(equity=5000.0, real_equity=5000.0, eq_fallback=None) == 2500.0 + assert book._cap(equity=598.0, real_equity=598.0, eq_fallback=None) == 299.0 # ~inerte a $600 + + +def test_cap_falls_back_to_fixed_on_eq_fallback(monkeypatch, tmp_path): + book = _write_cfg(monkeypatch, tmp_path, max_notional_per_asset_usd=300, max_notional_per_asset_frac=0.5) + # equity paper (real non leggibile) -> NON usare equity/2 ($1000 su conto ignoto = pericoloso) + assert book._cap(equity=2000.0, real_equity=None, eq_fallback="paper_cap") == 300.0 + assert book._cap(equity=2000.0, real_equity=None, eq_fallback=None) == 300.0 # real None -> fisso + assert book._cap(equity=0.0, real_equity=0.0, eq_fallback=None) == 300.0 # equity 0 -> fisso + + +def test_cap_fixed_when_no_frac_in_config(monkeypatch, tmp_path): + book = _write_cfg(monkeypatch, tmp_path, max_notional_per_asset_usd=300) # niente frac + assert book._cap(equity=5000.0, real_equity=5000.0, eq_fallback=None) == 300.0 + + +def test_book_report_dynamic_cap_online(monkeypatch, tmp_path): + """Integrazione: online con equity reale -> cap_per_asset = equity/2 e la formula lo rispetta.""" + book = _write_cfg(monkeypatch, tmp_path, max_notional_per_asset_usd=300, max_notional_per_asset_frac=0.5) + base = book.shadow_report(offline=True, equity_override=5000.0) + monkeypatch.setattr(book, "shadow_report", + lambda **k: {**base, "real_equity": 5000.0, "equity": 5000.0, "eq_fallback": None}) + r = book.book_report() + assert r["cap_per_asset"] == 2500.0 + for a in r["assets"]: + expect = book_net_target(a["tp_frac"], a["skh_sign"], 5000.0, 2500.0) + assert abs(a["net_target"] - expect) < 1e-6 + + def test_weights_match_backtest_sleeves(): """I pesi del book live DEVONO coincidere con quelli del backtest (deribit_book_sleeves).""" from src.portfolio.sleeves import deribit_book_sleeves