feat(live): cap/asset dinamico = equity/2 — operazionalizza la decisione frontier (inerte a $600)

Il cap notional per-asset non e' piu' fisso a $300: con max_notional_per_asset_frac
in config diventa equity/2, cosi' cresce col capitale e un deposito futuro non resta
strozzato (frontiera 2026-07-03). Guardrail: dinamico SOLO con equity reale fidata;
su fallback/offline si torna al cap fisso (protezione downside quando E e' ignota).
Live dry-run: cap $299 a equity $598 -> inerte oggi. Suite 172 passed (+4 test).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Adriano Dal Pastro
2026-07-03 09:42:13 +00:00
parent eef200cf14
commit cf7de40dc0
4 changed files with 119 additions and 5 deletions
+3 -1
View File
@@ -1,7 +1,9 @@
{ {
"_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). cap/asset $300 split 75/25, disaster-SL on-book -30% sulla posizione netta. Tutto flat all'arming -> nessun ordine finche' un segnale non arma.", "_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.",
"execution_enabled": true, "execution_enabled": true,
"max_notional_per_asset_usd": 300, "max_notional_per_asset_usd": 300,
"max_notional_per_asset_frac": 0.5,
"min_order_usd": 5, "min_order_usd": 5,
"disaster_sl_pct": 0.30 "disaster_sl_pct": 0.30
} }
+47
View File
@@ -0,0 +1,47 @@
# 2026-07-03 — Cap notional per-asset DINAMICO (= equity/2): operazionalizza la decisione frontier
**Contesto.** La frontiera del 2026-07-03 (`docs/diary/2026-07-03-book-frontier.md`) aveva identificato
**una sola** azione config con impatto reale: portare `max_notional_per_asset_usd` da $300 fisso a
**equity/2**, perché il cap fisso strozza il book man mano che si deposita (paradosso: più capitale =
book più freddo — a $5k il cap $300 fa girare il libro al ~49% del target). Il diario la lasciava
"da applicare AL deposito". Qui la **operazionalizzo in codice** rendendo il cap dinamico, così è
sempre corretta e non richiede un edit manuale al deposito.
## Cosa è cambiato
- **`src/live/book.py``_cap()`**: se in config c'è `max_notional_per_asset_frac` (opt-in), il cap
per-asset diventa **`equity × frac`** (0.5 → equity/2). Senza la chiave → comportamento vecchio
(cap fisso `max_notional_per_asset_usd`). Il call site in `book_report()` passa
`equity`, `real_equity`, `eq_fallback` a `_cap()`.
- **`config/live.json`**: aggiunto `max_notional_per_asset_frac: 0.5` (+ nota). `max_notional_per_asset_usd`
resta come **cap fisso di fallback**.
- **`tests/test_book_live.py`**: +4 test (scaling con equity fidata, fallback su eq_fallback/real=None/
equity 0, no-frac, integrazione online). Suite: **172 passed**.
## Il guardrail di sicurezza (il punto delicato)
Il cap dinamico si usa **SOLO con equity reale fidata** (letta dal conto). Se l'equity reale non è
leggibile — `eq_fallback` attivo, `real_equity=None`, offline, o equity ≤ 0 — si **torna al cap fisso
$300**. Motivo: in fallback `equity` è il paper-cap ($2000 ipotetico); calcolare `equity/2 = $1000` su
un conto di importo reale **ignoto** sizerebbe troppo. Preserva esattamente la protezione downside che
il test `test_book_execute_eq_fallback_warns_but_proceeds` verificava (l'hard-cap $/asset protegge
quando E è ignota). Regola: **equity/2 solo quando ci fidiamo di E; altrimenti $300 fisso.**
## Verifica
- **Live dry-run reale** (nessun `--execute`, nessun ordine): `cap/asset **$299**` = $598.06/2 →
**INERTE** oggi (≡ vecchio $300). Book flat, HOLD.
- Offline / `equity_override`: cap resta **$300 fisso** (nessuna equity reale da fidarsi) → il test
di parità pre-esistente regge invariato.
- A $5.000 di equity il cap diventerebbe automaticamente **$2.500** (equity/2), rimuovendo il
throttling che la frontiera aveva quantificato.
## Impatto e limiti
- **Già attivo**: il cron orario (`cron_book.sh`) esegue dal working tree → il prossimo run legge già
`frac=0.5`. Nessun deploy separato. **Inerte a $600**, si "accende" da sé quando l'equity cresce.
- **NON avvicina €50/g**: la frontiera lo aveva già detto — è una miglioria marginale (+€0.2/g @2k,
+€0.5/g @5k), il cui unico scopo è non lasciare capitale futuro strozzato. €50/g resta un problema
di **capitale + tempo** (~130k @ CAGR onesto), non di questo cap.
- Nessun cambio di **leva** (TP01 `target_vol=20%` invariato, la vol nativa ~10% emerge da sola —
coerente col verdetto frontier "gira alla vol nativa, non levare").
+20 -4
View File
@@ -13,7 +13,11 @@ deribit_book_sleeves = 0.75*TP01 + 0.25*SKH01):
tp_frac = target TP01 (causale, >=0, long-flat) da TrendPortfolio.current_target tp_frac = target TP01 (causale, >=0, long-flat) da TrendPortfolio.current_target
skh_sign = +1 long / -1 short / 0 flat da _skyhook_positions (book 230m) skh_sign = +1 long / -1 short / 0 flat da _skyhook_positions (book 230m)
E = equity reale del conto (fallback paper se offline) E = equity reale del conto (fallback paper se offline)
CAP = max_notional_per_asset_usd (config/live.json, $300) CAP = cap notional per-asset. Fisso = max_notional_per_asset_usd ($300), OPPURE
DINAMICO = equity * max_notional_per_asset_frac (es. 0.5 -> equity/2) se la
frazione e' presente in config -> il cap cresce col capitale e non strozza il
book quando depositi (frontiera 2026-07-03). Dinamico SOLO con equity reale
fidata; su fallback/offline si torna al cap fisso (protezione downside).
GLI EXIT DI SKH01 SONO IMPLICITI: _skyhook_positions() replica il book (ingressi + SL/TP/max_bars + GLI EXIT DI SKH01 SONO IMPLICITI: _skyhook_positions() replica il book (ingressi + SL/TP/max_bars +
non-overlap) sulla feed certificata fresca -> quando il trade va chiuso ritorna 'flat' -> skh_sign=0 non-overlap) sulla feed certificata fresca -> quando il trade va chiuso ritorna 'flat' -> skh_sign=0
@@ -42,9 +46,21 @@ W_SKH = 0.25
FLAT_USD = 1.0 FLAT_USD = 1.0
def _cap() -> float: 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."""
cfg = json.loads(CONFIG.read_text()) if CONFIG.exists() else {} cfg = json.loads(CONFIG.read_text()) if CONFIG.exists() else {}
return float(cfg.get("max_notional_per_asset_usd", 300.0)) 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
def book_net_target(tp_frac: float, skh_sign: int, equity: float, cap: float, def book_net_target(tp_frac: float, skh_sign: int, equity: float, cap: float,
@@ -99,8 +115,8 @@ def book_report(offline: bool = False, equity_override: float | None = None,
segnale 230m all'ultima barra chiusa, per l'esecuzione reale. Default False (feed certificato, segnale 230m all'ultima barra chiusa, per l'esecuzione reale. Default False (feed certificato,
deterministico: dashboard/test). TP01 resta sul certificato (è giornaliero).""" deterministico: dashboard/test). TP01 resta sul certificato (è giornaliero)."""
sh = shadow_report(offline=offline, equity_override=equity_override) sh = shadow_report(offline=offline, equity_override=equity_override)
cap = _cap()
equity = sh["equity"] equity = sh["equity"]
cap = _cap(equity=equity, real_equity=sh.get("real_equity"), eq_fallback=sh.get("eq_fallback"))
load5m = None load5m = None
if live_feed: if live_feed:
from src.live.livefeed import fresh_5m from src.live.livefeed import fresh_5m
+49
View File
@@ -36,6 +36,55 @@ def test_net_target_clamps_negative():
assert book_net_target(-5.0, 1, 600.0, 300.0) == 75.0 assert book_net_target(-5.0, 1, 600.0, 300.0) == 75.0
# ---------------------------------------------------------------------------
# CAP DINAMICO (frontiera 2026-07-03): con max_notional_per_asset_frac in config il
# cap = equity*frac (cresce col capitale). GUARDRAIL: dinamico SOLO con equity reale
# fidata; su fallback/offline/no-frac -> cap FISSO (protezione downside).
# ---------------------------------------------------------------------------
import json as _json
def _write_cfg(monkeypatch, tmp_path, **keys):
import src.live.book as book
p = tmp_path / "live.json"
p.write_text(_json.dumps(keys))
monkeypatch.setattr(book, "CONFIG", p)
return book
def test_cap_dynamic_scales_with_trusted_equity(monkeypatch, tmp_path):
book = _write_cfg(monkeypatch, tmp_path, max_notional_per_asset_usd=300, max_notional_per_asset_frac=0.5)
# equity reale fidata -> cap = equity/2 (cresce col capitale)
assert book._cap(equity=5000.0, real_equity=5000.0, eq_fallback=None) == 2500.0
assert book._cap(equity=598.0, real_equity=598.0, eq_fallback=None) == 299.0 # ~inerte a $600
def test_cap_falls_back_to_fixed_on_eq_fallback(monkeypatch, tmp_path):
book = _write_cfg(monkeypatch, tmp_path, max_notional_per_asset_usd=300, max_notional_per_asset_frac=0.5)
# equity paper (real non leggibile) -> NON usare equity/2 ($1000 su conto ignoto = pericoloso)
assert book._cap(equity=2000.0, real_equity=None, eq_fallback="paper_cap") == 300.0
assert book._cap(equity=2000.0, real_equity=None, eq_fallback=None) == 300.0 # real None -> fisso
assert book._cap(equity=0.0, real_equity=0.0, eq_fallback=None) == 300.0 # equity 0 -> fisso
def test_cap_fixed_when_no_frac_in_config(monkeypatch, tmp_path):
book = _write_cfg(monkeypatch, tmp_path, max_notional_per_asset_usd=300) # niente frac
assert book._cap(equity=5000.0, real_equity=5000.0, eq_fallback=None) == 300.0
def test_book_report_dynamic_cap_online(monkeypatch, tmp_path):
"""Integrazione: online con equity reale -> cap_per_asset = equity/2 e la formula lo rispetta."""
book = _write_cfg(monkeypatch, tmp_path, max_notional_per_asset_usd=300, max_notional_per_asset_frac=0.5)
base = book.shadow_report(offline=True, equity_override=5000.0)
monkeypatch.setattr(book, "shadow_report",
lambda **k: {**base, "real_equity": 5000.0, "equity": 5000.0, "eq_fallback": None})
r = book.book_report()
assert r["cap_per_asset"] == 2500.0
for a in r["assets"]:
expect = book_net_target(a["tp_frac"], a["skh_sign"], 5000.0, 2500.0)
assert abs(a["net_target"] - expect) < 1e-6
def test_weights_match_backtest_sleeves(): def test_weights_match_backtest_sleeves():
"""I pesi del book live DEVONO coincidere con quelli del backtest (deribit_book_sleeves).""" """I pesi del book live DEVONO coincidere con quelli del backtest (deribit_book_sleeves)."""
from src.portfolio.sleeves import deribit_book_sleeves from src.portfolio.sleeves import deribit_book_sleeves