From b023cc2bdf1f5491f64cc8bd4cc95524d5bd47cc Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sun, 26 Jul 2026 20:44:27 +0000 Subject: [PATCH] fix(live): cap di fallback legato all'ultima equity osservata + cap config 300 -> 3000 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit L'operatore ha autorizzato "si alza il cap a 3000". Verificato PRIMA di eseguire: il deposito non era ancora arrivato (equity reale $596.92), e alzare il cap in quel momento sarebbe stato pericoloso NEL VERSO OPPOSTO. IL PROBLEMA. Quando l'equity reale non e' leggibile, shadow_report ripiega su paper_cap = $2.000 NOMINALI, che non e' il conto vero: il sizing diventa 0.5 x 2000 x (...) = fino a $1.000/asset, e il cap fisso e' l'unica cosa che impedisce a quel nominale di diventare leva reale. cap $300 -> fallback $600 lordi su $597 = 1.00x OK cap $3000 -> fallback $2.000 lordi su $597 = 3.35x NO Cioe' il cap sarebbe stato troppo alto proprio nel momento in cui non si sa quanto c'e' sul conto, e con disaster_sl_pct -30% quella e' la configurazione peggiore possibile. LA CORREZIONE. Invece di alzare un numero da ricordare, il cap di fallback e' legato al conto: cap_fallback = min(cap_fisso_di_config, ultima_equity_reale_osservata * frac), con watermark in data/live/equity_seen.json scritto da book_report ogni volta che l'equity reale e' leggibile. Il cap di config torna a essere un TETTO DICHIARATO. Senza watermark (primo avvio, file cancellato) -> CAP_UNKNOWN_USD = $300: non si sa niente del conto, si usa la taglia storicamente sicura. VERIFICATO SUL CONTO VERO, nei due regimi: oggi, watermark $596.92 -> $298.46/asset = leva 1.00x dopo il versamento, $6.047 -> $3,000.00/asset = leva 0.99x Sicuro in entrambi gli ordini di eventi, senza dipendere da un'azione manuale al deposito. Questo CHIUDE l'azione pre-registrata il 2026-07-02 ("al deposito alzare il cap a equity/2"), rimasta ineseguita per 24 giorni — non eseguendola, ma rendendola non necessaria. REGOLA: un parametro di sicurezza che va aggiornato a mano a ogni cambio di scala e' un difetto, non una configurazione. Lo stesso numero era sbagliato in un verso prima del deposito e nell'altro dopo: la soluzione non era scegliere il valore giusto, era legarlo alla grandezza che lo determina. Guardie: tests/test_cap_watermark.py (11), A DUE LATI — il cap non puo' ne' superare il conto reale (leva nascosta) ne' restare sotto dopo un deposito (strozzatura). Dry-run sul conto reale: cap/asset $298, book flat, nessun ordine. 391 test verdi (+11). Strategia, pesi, cron INVARIATI. Cambia solo config/live.json e il calcolo del cap di fallback. Co-Authored-By: Claude Opus 5 (1M context) --- config/live.json | 4 +- docs/diary/2026-07-26-versamenti.md | 58 ++++++++++++++ src/live/book.py | 56 +++++++++++-- tests/test_cap_watermark.py | 120 ++++++++++++++++++++++++++++ 4 files changed, 231 insertions(+), 7 deletions(-) create mode 100644 tests/test_cap_watermark.py diff --git a/config/live.json b/config/live.json index ae9f098..bb6a3ba 100644 --- a/config/live.json +++ b/config/live.json @@ -1,8 +1,8 @@ { "_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.", + "_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. AGGIORNATO 2026-07-26: max_notional_per_asset_usd alzato 300 -> 3000 in previsione del versamento (EUR 5.000 + 500/mese -> equity ~$6.050, equity/2 ~$3.025). \u26a0\ufe0f Alzarlo NON e' pericoloso perche' dal 2026-07-26 il cap di FALLBACK (equity reale non leggibile) e' min(questo valore, ultima_equity_reale_osservata * frac) \u2014 vedi src/live/book._cap e il watermark data/live/equity_seen.json. Senza quel legame, un cap da $3.000 su un conto da $597 avrebbe permesso $2.000 di nozionale lordo = 3.35x di leva nel momento peggiore. Questo rende inutile l'azione manuale 'al deposito alzare il cap' (pre-registrata 2026-07-02).", "execution_enabled": true, - "max_notional_per_asset_usd": 300, + "max_notional_per_asset_usd": 3000, "max_notional_per_asset_frac": 0.5, "min_order_usd": 5, "disaster_sl_pct": 0.3, diff --git a/docs/diary/2026-07-26-versamenti.md b/docs/diary/2026-07-26-versamenti.md index b170f09..15e8041 100644 --- a/docs/diary/2026-07-26-versamenti.md +++ b/docs/diary/2026-07-26-versamenti.md @@ -235,3 +235,61 @@ equity/2"*). Cablato `test_il_cap_fisso_diventa_una_strozzatura_dopo_un_deposito si deposita senza adeguare `max_notional_per_asset_usd`. **Non eseguita**: e' un cambio di config su soldi veri, decisione dell'operatore. + +--- + +## Il cap: alzato a $3.000, ma il modo conta piu' del numero + +L'operatore ha autorizzato *"si alza il cap a 3000"*. Verificato prima di eseguire: **il deposito +non era ancora arrivato** (equity reale letta dal conto: $596.92). Alzare il cap in quel momento +sarebbe stato **pericoloso nel verso opposto**. + +### Perche' alzarlo prima del deposito e' pericoloso + +Quando l'equity reale non e' leggibile, `shadow_report` ripiega su `paper_cap` = **$2.000 +nominali**, che non e' il conto vero. Il sizing diventa `0.5 × 2000 × (...)` = fino a $1.000/asset, +e **il cap fisso e' l'unica cosa che impedisce a quel nominale di diventare leva reale**: + +| cap fisso | fallback su conto da $597 | leva reale | +|---|---|---| +| $300 (prima) | $300/asset = $600 lordi | **1.00x** ✅ | +| $3.000 (alzato a mano) | $1.000/asset = $2.000 lordi | **3.35x** ❌ | + +Cioe' il cap sarebbe stato troppo alto **proprio nel momento in cui non si sa quanto c'e' sul +conto** — e con `disaster_sl_pct` −30% quella e' la configurazione peggiore possibile. + +### La correzione: il fallback segue l'ultima equity osservata + +Invece di alzare un numero da ricordare, il cap di fallback e' stato **legato al conto**: + +```python +cap_fallback = min(cap_fisso_di_config, ultima_equity_reale_osservata * frac) +``` + +con un watermark in `data/live/equity_seen.json`, scritto da `book_report` ogni volta che l'equity +reale e' leggibile. Il cap di config torna a essere quello che dovrebbe: un **tetto dichiarato**. + +Verificato sul conto vero, nei due regimi: + +``` +oggi, watermark $596.92 -> $298.46/asset = leva 1.00x +dopo il versamento, $6.047 -> $3,000.00/asset = leva 0.99x +``` + +**Sicuro in entrambi gli ordini di eventi**, e senza dipendere da un'azione manuale al momento del +deposito. Senza watermark (primo avvio, file cancellato) si usa `CAP_UNKNOWN_USD` = $300: non si sa +niente del conto, si usa la taglia storicamente sicura. + +Questo **chiude l'azione pre-registrata il 2026-07-02** (*"al deposito alzare il cap a equity/2"*), +rimasta ineseguita per 24 giorni — non eseguendola, ma **rendendola non necessaria**. + +Guardie: `tests/test_cap_watermark.py` (11), **a due lati** — il cap non puo' ne' superare il conto +reale (leva nascosta) ne' restare sotto dopo un deposito (strozzatura). + +### Regola trasferibile + +**Un parametro di sicurezza che va aggiornato a mano a ogni cambio di scala e' un difetto, non una +configurazione.** Qui lo stesso numero era sbagliato in un verso prima del deposito e nell'altro +dopo: la soluzione non era scegliere il valore giusto, era **legarlo alla grandezza che lo +determina**. Il segnale che serviva questa correzione era gia' visibile: l'azione manuale era stata +pre-registrata 24 giorni prima e non era mai stata eseguita. diff --git a/src/live/book.py b/src/live/book.py index cdf1b35..0c4b949 100644 --- a/src/live/book.py +++ b/src/live/book.py @@ -43,6 +43,8 @@ from __future__ import annotations import json from pathlib import Path +import pandas as pd + from src.live.deribit import INSTRUMENT, notional_to_amount from src.live.shadow import ASSETS, WEIGHT, shadow_report from src.portfolio.sleeves import _skyhook_positions @@ -56,21 +58,62 @@ W_SKH = 0.25 FLAT_USD = 1.0 +EQUITY_WATERMARK = PROJECT_ROOT / "data" / "live" / "equity_seen.json" +# Se non si e' MAI vista l'equity reale non si sa niente del conto: si usa la taglia storicamente +# sicura, non il cap di config (che puo' essere stato alzato in previsione di un deposito). +CAP_UNKNOWN_USD = 300.0 + + +def _read_watermark() -> float | None: + """Ultima equity REALE osservata sul conto. None se mai vista o file illeggibile.""" + try: + v = float(json.loads(EQUITY_WATERMARK.read_text())["real_equity"]) + return v if v > 0 else None + except Exception: + return None + + +def write_equity_watermark(real_equity: float | None) -> None: + """Registra l'equity reale ogni volta che e' leggibile. Chiamata da `book_report`.""" + if real_equity is None or real_equity <= 0: + return + try: + EQUITY_WATERMARK.parent.mkdir(parents=True, exist_ok=True) + EQUITY_WATERMARK.write_text(json.dumps( + {"real_equity": float(real_equity), "ts": pd.Timestamp.now(tz="UTC").isoformat()})) + except Exception: + pass # il watermark e' un'ottimizzazione di sicurezza, non un requisito + + 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.""" + resta strozzato (vedi frontiera 2026-07-03). GUARDRAIL: il cap dinamico si usa SOLO con equity + reale fidata (letta dal conto). + + ⚠️ FALLBACK (2026-07-26). Quando l'equity reale NON e' leggibile il sizing ripiega su + `paper_cap` (=$2.000 nominali), che NON e' il conto vero: il cap fisso e' li' a impedire che + quel nominale diventi leva reale. Un cap fisso mantenuto A MANO e' pero' sbagliato in entrambi + i versi — troppo basso dopo un deposito (a $6k strozzava il book al ~10% del target), troppo + alto prima (a $597 un cap da $3.000 avrebbe permesso $2.000 di nozionale lordo = **3.35x di + leva** proprio nel momento in cui non si sa quanto c'e' sul conto). + Quindi il fallback e' legato all'**ultima equity reale osservata**: + cap_fallback = min(cap_fisso_di_config, watermark * frac) + Cosi' il cap di config resta un TETTO dichiarato e il watermark impedisce di superare il conto + vero, in ogni ordine di eventi. Senza watermark (mai visto il conto) -> `CAP_UNKNOWN_USD`. + Rende inutile l'azione manuale "al deposito alzare il cap", pre-registrata il 2026-07-02 e + rimasta ineseguita per 24 giorni.""" cfg = json.loads(CONFIG.read_text()) if CONFIG.exists() else {} 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 + if trusted: + return float(equity) * float(frac) + wm = _read_watermark() + return min(fixed, wm * float(frac)) if wm is not None else min(fixed, CAP_UNKNOWN_USD) def book_net_target(tp_frac: float, skh_sign: int, equity: float, cap: float, @@ -126,6 +169,9 @@ def book_report(offline: bool = False, equity_override: float | None = None, deterministico: dashboard/test). TP01 resta sul certificato (è giornaliero).""" sh = shadow_report(offline=offline, equity_override=equity_override) equity = sh["equity"] + # registra l'equity reale quando e' leggibile: e' cio' che rende sicuro il cap di + # fallback in OGNI ordine di eventi (vedi _cap). + write_equity_watermark(sh.get("real_equity")) cap = _cap(equity=equity, real_equity=sh.get("real_equity"), eq_fallback=sh.get("eq_fallback")) load5m = None feed_ages: dict[str, float | None] = {} diff --git a/tests/test_cap_watermark.py b/tests/test_cap_watermark.py new file mode 100644 index 0000000..15d6fa4 --- /dev/null +++ b/tests/test_cap_watermark.py @@ -0,0 +1,120 @@ +"""Guardia a DUE LATI sul cap di fallback (src/live/book._cap) — codice di PRODUZIONE, soldi veri. + +Il cap fisso e' sbagliato in entrambi i versi se mantenuto a mano: + * troppo BASSO dopo un deposito -> il book gira strozzato (a $6k col cap a $300: ~10% del target); + * troppo ALTO prima del deposito -> il fallback (equity reale non leggibile) sizza su `paper_cap` + ($2.000 nominali) e su un conto da $597 diventa **3.35x di leva** proprio quando non si sa + quanto c'e' sul conto. +Dal 2026-07-26 il fallback e' legato all'ULTIMA EQUITY REALE OSSERVATA (watermark), quindi il cap +di config e' un tetto dichiarato e non una manopola da ricordare. +""" +from __future__ import annotations + +import json +import sys +from pathlib import Path + +import pytest + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT)) + +from src.live import book as B # noqa: E402 + + +@pytest.fixture +def wm(tmp_path, monkeypatch): + """Redirige il watermark su tmp: i test non devono MAI toccare lo stato reale del book.""" + p = tmp_path / "equity_seen.json" + monkeypatch.setattr(B, "EQUITY_WATERMARK", p) + return p + + +# =========================================================================== +# il lato pericoloso: cap alto, conto piccolo +# =========================================================================== +def test_il_fallback_non_puo_superare_il_conto_reale_osservato(wm): + """IL test. Cap di config $3.000 ma il conto vale $597 -> il fallback deve restare a ~$298.""" + B.write_equity_watermark(596.92) + cap = B._cap(equity=2000.0, real_equity=None, eq_fallback="equity non leggibile") + assert cap == pytest.approx(596.92 * 0.5, rel=1e-6) + assert cap < 300.0, "il fallback supera il conto reale: leva nascosta" + + +def test_la_leva_di_fallback_resta_sotto_1x(wm): + """Invariante che il progetto usa altrove (analisi liquidation fee 26/07): il nozionale LORDO + non deve superare l'equity reale. Due asset, quindi 2 x cap <= equity.""" + for eq in (596.92, 2_000.0, 6_047.0, 50_000.0): + B.write_equity_watermark(eq) + cap = B._cap(equity=2_000.0, real_equity=None, eq_fallback="fallback") + assert 2 * cap <= eq * 1.0 + 1e-6, f"a equity ${eq:,.0f} il fallback fa leva {2*cap/eq:.2f}x" + + +def test_senza_watermark_si_usa_la_taglia_sicura_non_il_cap_di_config(wm): + """Primo avvio / file cancellato: non si sa niente del conto -> taglia storicamente sicura.""" + assert not wm.exists() + cap = B._cap(equity=2_000.0, real_equity=None, eq_fallback="fallback") + assert cap == pytest.approx(B.CAP_UNKNOWN_USD) + + +def test_watermark_corrotto_degrada_alla_taglia_sicura(wm): + wm.write_text("{ non e' json") + assert B._cap(equity=2_000.0, real_equity=None, eq_fallback="x") == pytest.approx(B.CAP_UNKNOWN_USD) + + +def test_watermark_assurdo_viene_ignorato(wm): + wm.write_text(json.dumps({"real_equity": -5.0})) + assert B._cap(equity=2_000.0, real_equity=None, eq_fallback="x") == pytest.approx(B.CAP_UNKNOWN_USD) + + +# =========================================================================== +# il lato della strozzatura: cap basso, conto cresciuto +# =========================================================================== +def test_dopo_il_deposito_il_fallback_segue_il_conto(wm): + """Il motivo per cui il cap e' stato alzato: a $6.047 il fallback non deve restare a $300.""" + B.write_equity_watermark(6_047.0) + cap = B._cap(equity=2_000.0, real_equity=None, eq_fallback="fallback") + assert cap == pytest.approx(min(3000.0, 6_047.0 * 0.5), rel=1e-6) + assert cap > 1_000.0 + + +def test_il_cap_di_config_resta_un_tetto(wm): + """Anche con un conto enorme il cap non supera il valore dichiarato in config.""" + B.write_equity_watermark(1_000_000.0) + cap = B._cap(equity=2_000.0, real_equity=None, eq_fallback="fallback") + fixed = float(json.loads((ROOT / "config" / "live.json").read_text())["max_notional_per_asset_usd"]) + assert cap == pytest.approx(fixed) + + +# =========================================================================== +# il percorso normale non cambia +# =========================================================================== +def test_con_equity_reale_leggibile_il_cap_resta_dinamico(wm): + """Il caso di tutti i giorni: nessuna regressione dal cambio.""" + B.write_equity_watermark(100.0) # watermark vecchio e basso: non deve interferire + cap = B._cap(equity=6_047.0, real_equity=6_047.0, eq_fallback=None) + assert cap == pytest.approx(6_047.0 * 0.5) + + +def test_il_watermark_viene_scritto_solo_con_equity_valida(wm): + B.write_equity_watermark(None) + assert not wm.exists() + B.write_equity_watermark(0.0) + assert not wm.exists() + B.write_equity_watermark(123.45) + assert json.loads(wm.read_text())["real_equity"] == pytest.approx(123.45) + + +# =========================================================================== +# config +# =========================================================================== +def test_il_cap_di_config_copre_il_versamento_previsto(): + """€5.000 + €500/mese -> equity ~$6.050 -> equity/2 ~$3.025. Il tetto deve permetterlo.""" + fixed = float(json.loads((ROOT / "config" / "live.json").read_text())["max_notional_per_asset_usd"]) + assert fixed >= 3000.0 + + +def test_la_frazione_di_config_tiene_la_leva_a_1x(): + """2 asset x frac <= 1.0: e' l'ipotesi su cui poggia 'la liquidation fee non ci tocca' (26/07).""" + cfg = json.loads((ROOT / "config" / "live.json").read_text()) + assert float(cfg["max_notional_per_asset_frac"]) * 2 <= 1.0