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:
@@ -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
|
||||
Reference in New Issue
Block a user