fix(live): cap di fallback legato all'ultima equity osservata + cap config 300 -> 3000

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) <noreply@anthropic.com>
This commit is contained in:
Adriano Dal Pastro
2026-07-26 20:44:27 +00:00
parent eb7c3af636
commit b023cc2bdf
4 changed files with 231 additions and 7 deletions
+120
View File
@@ -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