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
+2 -2
View File
@@ -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,
+58
View File
@@ -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.
+51 -5
View File
@@ -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] = {}
+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