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:
+2
-2
@@ -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,
|
||||
|
||||
@@ -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
@@ -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] = {}
|
||||
|
||||
@@ -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