GATE SCALA-01: la chiave di scala esiste, ed e' INERTE (SPEC §8 punti 1-4)
Chiude il debito che CLAUDE.md dichiarava da settimane: la regola "ogni cambio di scala passa dal cap di config, non da target_vol" NON era implementabile perche' una chiave di scala non esisteva — qualunque cambio sarebbe finito su WEIGHT/W_TP01/W_SKH, cioe' codice su un percorso con soldi veri e per giunta nel posto sbagliato (W_TP01/W_SKH sono il RAPPORTO 75/25, non la taglia). Specifica gia' scritta in docs/research/SPEC-scale-key.md (618 righe, 9 condizioni di gate, prototipo). Non ho progettato: ho eseguito i punti 1-4. 🚨 config/live.json NON E' STATO TOCCATO. La chiave e' assente, vale 1,00, e T7 dimostra bit-exact che il libro e' quello di ieri (max|diff| = 0.0). Verificato anche a runtime: book_execute in dry-run da' gli stessi target del cron delle 15:47 (BTC $+355, ETH $+214). LE QUATTRO DECISIONI CHE NON SONO DI COMODO - La scala si applica DOPO il clamp. Prima, il cap se la mangerebbe proprio nei giorni di massima convinzione (a tp=1/sg=+1 il grezzo vale esattamente cap => k_eff tornerebbe a 1,00 a ogni k): sarebbe un cambio di FORMA travestito da cambio di taglia, e la curva g(k) con cui il gradino viene autorizzato non descriverebbe quel libro. Prezzo dichiarato: il cap diventa il tetto del libro UNITARIO, e la guardia sulla leva lorda va ricostruita. - Il tetto e' sul PRODOTTO e sta nel CODICE. Sulla sola chiave lascerebbe aperta la porta accanto (frac 0,625 x scala 1,25 = 1,562x); in config sarebbe un lucchetto con la chiave attaccata. LEVA_LORDA_MAX 1,25 in src/live/book.py => il gradino a 1,50 richiede codice, quindi review. - Fuori scaletta o fuori tetto = STOP, non clamp. book_execute si ferma, non invia, allerta (ScalaNonAutorizzata). E SCALA_LADDER (1,00 · 1,25) rende INESPRIMIBILE "solo un po'": 1,05 non e' prudente, e' fuori scaletta. - La scala vive solo sul percorso fidato (equity illeggibile => 1,00), cosi' "il fallback non e' piu' permissivo" e' vero per costruzione. Ma la VALIDAZIONE avviene sempre: una config rotta non si nasconde dietro un giro in cui l'equity non era leggibile. LA GUARDIA CHE MORDE PER PRIMA non e' il peggior giorno (k <= 3,49x) ma il COSTO di un disaster-SL (k <= 1,67x, 2,1x piu' stringente): l'invariante n_asset x frac x scala x disaster_sl_pct <= 0,50 scatta anche se qualcuno allarga lo stop invece di alzare la scala. TEST T1-T11 (tests/test_book_scale.py, 18 verdi). Il piu' importante e' T1b: a k=1 l'implementazione simmetrica e quella asimmetrica danno lo STESSO numero, quindi un test di simmetria scritto sul caso di default ha potenza ZERO. T1b verifica che le due coincidano a k=1 (il rischio e' reale) e che fuori da k=1 l'asserzione le SEPARI, con un'implementazione asimmetrica scritta nel test apposta perche' fallisca. SORVEGLIANTE scale_watch (cron_daily, 3 domande / 3 azioni / 3 stati, una allerta per streak, marcatore scritto solo dopo invio riuscito — debito #2). Riporta la frequenza del ramo di fallback, che sopra il 2% in 90 giorni invaliderebbe la regola: misurata 0/1.676, coi 19 giri "paper capital" (pre-finanziamento, dove il libro non invia) contati e dichiarati a parte. Non puo' impedire la modifica: la rende visibile entro 24h e attribuibile. CHIUDE il debito #5 di §5: T2/T3 sostituiscono il vecchio test_leva_massima_da_config (che misurava frac x n_asset mentre la grandezza vera e' frac x n_asset x scala), e T11 verifica che sia rimasto cancellato. NON FATTO, deliberato: la chiave in config (punto 2 lo vieta), GATE SCALA-01 (A2 richiede >=30 giorni a 1,00 col sorvegliante attivo — "l'unico modo di scoprire che il sorvegliante e' rotto mentre la leva e' ancora 1,00"), r0726_fee_sensitivity rifatto (A7: serve solo al gradino; a 1,25x una liquidazione costerebbe 1,25% non 1,00%, e ereditarlo sarebbe l'errore). Nessun ordine. Suite: 825 passati. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+93
-4
@@ -63,6 +63,79 @@ EQUITY_WATERMARK = PROJECT_ROOT / "data" / "live" / "equity_seen.json"
|
||||
# sicura, non il cap di config (che puo' essere stato alzato in previsione di un deposito).
|
||||
CAP_UNKNOWN_USD = 300.0
|
||||
|
||||
# ---- CHIAVE DI SCALA (GATE SCALA-01) — spec: docs/research/SPEC-scale-key.md ------------------
|
||||
# ⚠️ COSTANTI DI CODICE, NON DI CONFIG, e il motivo e' il meccanismo del cricchetto (SPEC §4.1b):
|
||||
# un tetto che vive nello stesso file della chiave sarebbe "un lucchetto con la chiave attaccata".
|
||||
# Portare il gradino a 1,50 richiede una modifica di CODICE -> una review -> un gate.
|
||||
# Il tetto e' basso APPOSTA: non e' un'opinione sulla leva massima sopportabile (che da §4.5
|
||||
# sarebbe 1,67x), e' il dispositivo che rende impossibile alzarla "solo un po'".
|
||||
LEVA_LORDA_MAX = 1.25
|
||||
# La scaletta esiste perche' una scala A GRADINI rende INESPRIMIBILE "solo un po'": 1,05 non e'
|
||||
# un valore prudente, e' un valore fuori scaletta, e viene RIFIUTATO.
|
||||
SCALA_LADDER = (1.00, 1.25)
|
||||
# Invariante del disaster-SL (SPEC §4.5, la guardia che morde per PRIMA): il costo in equity di un
|
||||
# episodio di disaster-SL e' `n_asset * frac * scala * disaster_sl_pct` e non deve superare mezza
|
||||
# equity. Con frac 0,50 e sl 30% da' scala <= 1,67x, e scatta se qualcuno allarga il disaster-SL.
|
||||
DISASTER_SL_BUDGET = 0.50
|
||||
|
||||
|
||||
class ScalaNonAutorizzata(RuntimeError):
|
||||
"""Config che dichiara una scala fuori scaletta o fuori tetto.
|
||||
|
||||
⚠️ E' un'ECCEZIONE e non un clamp, ed e' una scelta (SPEC §4.1c): tagliare silenziosamente al
|
||||
tetto farebbe girare una config che DICHIARA un numero e un libro che ne ESEGUE un altro —
|
||||
la stessa classe di difetto che questo progetto ha gia' pagato tre volte. Chi la incontra
|
||||
ferma l'esecuzione e allerta; non "rende robusto" il lettore con un min().
|
||||
"""
|
||||
|
||||
|
||||
def _scala(real_equity: float | None = None, eq_fallback: str | None = None,
|
||||
cfg: dict | None = None) -> float:
|
||||
"""Scala del libro (`book_scale_k` in config). 1.0 = il libro di oggi, bit-exact.
|
||||
|
||||
DUE COMPORTAMENTI, entrambi voluti:
|
||||
|
||||
(1) VALIDA SEMPRE, anche sul percorso degradato. Una config fuori scaletta o fuori tetto e'
|
||||
un errore di configurazione, non una condizione di mercato: dev'essere scoperta al primo
|
||||
giro, non nascosta dal fatto che oggi l'equity non era leggibile.
|
||||
(2) POI applica la regola G3/G4: **la scala vive SOLO sul percorso fidato**. Se l'equity reale
|
||||
non e' leggibile la scala e' 1,00 — *una scala e' una decisione di rischio presa conoscendo
|
||||
il conto; quando non si sa quanto vale il conto, non si prende*. Cosi' "il fallback non e'
|
||||
piu' permissivo" e' vero PER COSTRUZIONE, non per aritmetica.
|
||||
|
||||
Il tetto e' sul PRODOTTO `n_asset * frac * scala`, mai sulla sola chiave: con un tetto sulla
|
||||
sola chiave, alzare `frac` da 0,50 a 0,625 con scala 1,25 darebbe 1,562x passando il controllo.
|
||||
"""
|
||||
if cfg is None:
|
||||
cfg = json.loads(CONFIG.read_text()) if CONFIG.exists() else {}
|
||||
raw = cfg.get("book_scale_k")
|
||||
if raw is None:
|
||||
return 1.0 # chiave assente = il libro di oggi
|
||||
s = float(raw)
|
||||
if not any(abs(s - x) < 1e-9 for x in SCALA_LADDER):
|
||||
raise ScalaNonAutorizzata(
|
||||
f"book_scale_k={s!r} non e' un gradino di SCALA_LADDER {SCALA_LADDER}: "
|
||||
f"una scala a gradini rende inesprimibile 'solo un po''. Config: {CONFIG}")
|
||||
frac = cfg.get("max_notional_per_asset_frac")
|
||||
if frac is not None:
|
||||
leva = len(ASSETS) * float(frac) * s
|
||||
if leva > LEVA_LORDA_MAX + 1e-12:
|
||||
raise ScalaNonAutorizzata(
|
||||
f"leva lorda {leva:.4f}x = n_asset {len(ASSETS)} x frac {frac} x scala {s} "
|
||||
f"supera LEVA_LORDA_MAX {LEVA_LORDA_MAX}x (tetto sul PRODOTTO, src/live/book.py). "
|
||||
f"Alzarlo e' una modifica di CODICE, non di config.")
|
||||
sl = float(cfg.get("disaster_sl_pct", 0.0))
|
||||
costo = leva * sl
|
||||
if costo > DISASTER_SL_BUDGET + 1e-12:
|
||||
raise ScalaNonAutorizzata(
|
||||
f"un episodio di disaster-SL costerebbe {100*costo:.1f}% dell'equity "
|
||||
f"(n_asset x frac {frac} x scala {s} x sl {sl}) > {100*DISASTER_SL_BUDGET:.0f}%: "
|
||||
f"e' la guardia che morde per prima (SPEC-scale-key §4.5).")
|
||||
if real_equity is None or eq_fallback:
|
||||
return 1.0 # G3/G4: niente scala fuori dal percorso fidato
|
||||
return s
|
||||
|
||||
|
||||
|
||||
def _read_watermark() -> float | None:
|
||||
"""Ultima equity REALE osservata sul conto. None se mai vista o file illeggibile."""
|
||||
@@ -136,12 +209,22 @@ def _cap(equity: float | None = None, real_equity: float | None = None,
|
||||
|
||||
|
||||
def book_net_target(tp_frac: float, skh_sign: int, equity: float, cap: float,
|
||||
weight: float = WEIGHT) -> float:
|
||||
weight: float = WEIGHT, scala: float = 1.0) -> float:
|
||||
"""Target NETTO (USD notional, segno = direzione) di un asset del book. PURA, testabile.
|
||||
Combina la frazione long-flat di TP01 (peso 0.75) e il segno L/S di SKH01 (peso 0.25),
|
||||
clampata al cap per-asset. Vedi formula nel docstring del modulo."""
|
||||
clampata al cap per-asset. Vedi formula nel docstring del modulo.
|
||||
|
||||
⚠️ LA SCALA SI APPLICA **DOPO** IL CLAMP, e non e' una scelta di comodo (SPEC §3.2).
|
||||
Applicandola PRIMA, il clamp la mangerebbe **proprio nei giorni di massima convinzione** (a
|
||||
`tp=1, sg=+1` il grezzo vale esattamente `cap`, quindi k_eff tornerebbe a 1,00 a ogni k): il
|
||||
risultato non sarebbe "il libro a leva k" ma un libro che alza i giorni piccoli e lascia fermi
|
||||
i grandi — un cambio di FORMA travestito da cambio di taglia, che la curva g(k) con cui il
|
||||
gradino viene autorizzato **non descrive**. DOPO il clamp, `k_eff == k` in ogni giorno.
|
||||
Il prezzo, dichiarato: il cap smette di essere il tetto assoluto del nozionale e diventa il
|
||||
tetto del libro UNITARIO; la guardia sulla leva lorda e' ricostruita in `_scala` (SPEC §4).
|
||||
Con `scala=1.0` (chiave assente) questa funzione e' bit-exact quella di prima."""
|
||||
raw = weight * equity * (W_TP01 * max(tp_frac, 0.0) + W_SKH * float(skh_sign))
|
||||
return max(-cap, min(cap, raw))
|
||||
return scala * max(-cap, min(cap, raw))
|
||||
|
||||
|
||||
def _skh_sign(state) -> int:
|
||||
@@ -192,6 +275,9 @@ def book_report(offline: bool = False, equity_override: float | None = None,
|
||||
# fallback in OGNI ordine di eventi (vedi _cap).
|
||||
equity_jump = write_equity_watermark(sh.get("real_equity"))
|
||||
cap = _cap(equity=equity, real_equity=sh.get("real_equity"), eq_fallback=sh.get("eq_fallback"))
|
||||
# La scala VALIDA sempre e si applica solo sul percorso fidato: se la config e' fuori scaletta
|
||||
# o fuori tetto, `_scala` SOLLEVA e book_execute si ferma senza inviare (SPEC §4.1c).
|
||||
scala = _scala(real_equity=sh.get("real_equity"), eq_fallback=sh.get("eq_fallback"))
|
||||
load5m = None
|
||||
feed_ages: dict[str, float | None] = {}
|
||||
feed_errors: dict[str, str] = {}
|
||||
@@ -226,7 +312,7 @@ def book_report(offline: bool = False, equity_override: float | None = None,
|
||||
tp_frac = float(a_rec["target"])
|
||||
st = skh.get(a, "flat")
|
||||
sign = _skh_sign(st)
|
||||
net = book_net_target(tp_frac, sign, equity, cap)
|
||||
net = book_net_target(tp_frac, sign, equity, cap, scala=scala)
|
||||
cur = float(a_rec["position_usd"])
|
||||
mark = a_rec["mark"]
|
||||
order = build_book_order(inst, net, cur, mark, min_usd=5.0)
|
||||
@@ -243,6 +329,9 @@ def book_report(offline: bool = False, equity_override: float | None = None,
|
||||
last_data=sh["last_data"], online=sh["online"],
|
||||
real_equity=sh["real_equity"], equity=equity, eq_basis=sh["eq_basis"],
|
||||
cap_per_asset=cap, weights=dict(TP01=W_TP01, SKH01=W_SKH),
|
||||
# La scala EFFETTIVA del giro (1.0 = libro di oggi). Esposta perche' un parametro che muove
|
||||
# il nozionale e non compare nel report e' la forma esatta di un difetto gia' pagato.
|
||||
scala=scala, leva_lorda_max=LEVA_LORDA_MAX,
|
||||
assets=assets, orders=orders, skh_error=skh_error, pos_error=sh.get("pos_error"),
|
||||
eq_fallback=sh.get("eq_fallback"),
|
||||
# Salto di equity fra due letture consecutive oltre EQUITY_JUMP_ALERT: dict o None.
|
||||
|
||||
Reference in New Issue
Block a user