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
+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] = {}