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:
@@ -30,7 +30,7 @@ import pandas as pd
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
|
||||
from src.live.book import book_report
|
||||
from src.live.book import ScalaNonAutorizzata, book_report
|
||||
from src.live.execution import DeribitTrader
|
||||
from src.live.notifier import notify
|
||||
from src.live.venue_probe import diagnose, errori_dal_report
|
||||
@@ -78,7 +78,20 @@ def _run():
|
||||
min_order = float(cfg["min_order_usd"])
|
||||
sl_pct = float(cfg["disaster_sl_pct"])
|
||||
|
||||
r = book_report(live_feed=True) # target NETTO + conto/posizioni reali (feed SKH fresco)
|
||||
try:
|
||||
r = book_report(live_feed=True) # target NETTO + conto/posizioni reali (feed SKH fresco)
|
||||
except ScalaNonAutorizzata as e:
|
||||
# G1(c): NON si taglia al tetto. Un clamp silenzioso farebbe girare una config che dichiara
|
||||
# un numero e un libro che ne esegue un altro. Si ferma, non invia, e allerta.
|
||||
msg = (f"🚨 SCALA NON AUTORIZZATA — book_execute FERMO, nessun ordine inviato.\n{e}\n"
|
||||
f"Ripara: riporta `book_scale_k` a un gradino di SCALA_LADDER dentro il tetto, "
|
||||
f"oppure rimuovi la chiave (assente = 1,00 = libro di oggi).")
|
||||
print(msg)
|
||||
try:
|
||||
notify(msg)
|
||||
except Exception as ne: # P3: l'errore si registra dove lo si ingoia
|
||||
print(f" ⚠️ allerta NON inviata: {type(ne).__name__}: {ne}")
|
||||
return
|
||||
equity = r["equity"]
|
||||
|
||||
print("=" * 88)
|
||||
@@ -89,7 +102,15 @@ def _run():
|
||||
print(f" modo : {mode}")
|
||||
print(f" gate : execution_enabled={enabled} | --execute={want_execute}")
|
||||
print(f" conto reale : ${r['real_equity']:,.2f}" if r["real_equity"] else f" conto: {r['eq_basis']}")
|
||||
print(f" sizing base : ${equity:,.2f} | cap/asset ${r['cap_per_asset']:.0f} | min ${min_order:.0f} | disaster-SL -{sl_pct*100:.0f}%")
|
||||
_sc = r.get("scala", 1.0)
|
||||
_lordo = len(r["assets"]) * float(cfg.get("max_notional_per_asset_frac") or 0.0) * _sc
|
||||
print(f" sizing base : ${equity:,.2f} | cap/asset ${r['cap_per_asset']:.0f} | "
|
||||
f"min ${min_order:.0f} | disaster-SL -{sl_pct*100:.0f}%")
|
||||
# La scala e' stampata SEMPRE, anche a 1,00: un parametro che muove il nozionale e non compare
|
||||
# nel log e' invisibile proprio nel giro in cui e' cambiato (SPEC-scale-key §2.2).
|
||||
print(f" scala libro : {_sc:.2f}x (tetto {r.get('leva_lorda_max', 1.25):.2f}x) | "
|
||||
f"leva lorda max {_lordo:.3f}x dell'equity"
|
||||
+ ("" if _sc != 1.0 else " [chiave assente o 1,00 = libro invariato]"))
|
||||
print(f" ultima barra : {r['last_data']}\n")
|
||||
|
||||
if r.get("skh_error"): # SKH feed fallito -> book.py ha forzato flat IN SILENZIO
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Sorveglianza giornaliera della CHIAVE DI SCALA del libro (SPEC-scale-key §6.4).
|
||||
|
||||
uv run python scripts/live/scale_watch.py # osserva e stampa, invia se ALLARME
|
||||
uv run python scripts/live/scale_watch.py --secco # osserva e stampa, NON invia nulla
|
||||
uv run python scripts/live/scale_watch.py --quiet # una riga sola (per il cron)
|
||||
|
||||
Tre domande, tre azioni diverse, tre stati (OK / ALLARME / NON MISURABILE).
|
||||
Non impedisce la modifica: la rende visibile entro 24 ore e attribuibile.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
|
||||
|
||||
from src.live.notifier import notify # noqa: E402
|
||||
from src.live.scale_watch import ALLARME, OK, run_once # noqa: E402
|
||||
|
||||
|
||||
def _sender(testo: str):
|
||||
try:
|
||||
notify(testo)
|
||||
return True, "telegram"
|
||||
except Exception as e: # P3: si registra dove lo si ingoia
|
||||
return False, f"{type(e).__name__}: {e}"
|
||||
|
||||
|
||||
def main() -> None:
|
||||
secco = "--secco" in sys.argv
|
||||
quiet = "--quiet" in sys.argv
|
||||
rep = run_once(sender=None if secco else _sender)
|
||||
icona = {OK: "✅", ALLARME: "🚨"}.get(rep["stato"], "⚠️")
|
||||
if quiet:
|
||||
fb = rep["fallback"]
|
||||
q = f"{100*fb['quota']:.2f}%" if "quota" in fb else "n/d"
|
||||
print(f" scale_watch: {icona} {rep['stato']} · scala {rep['scala_dichiarata']:.2f}x · "
|
||||
f"fallback {q} · {rep['invio']}")
|
||||
return
|
||||
print("=" * 88)
|
||||
print(f" SCALE WATCH — la chiave di scala del libro {icona} {rep['stato']}")
|
||||
print("=" * 88)
|
||||
print(f" scala dichiarata : {rep['scala_dichiarata']:.2f}x "
|
||||
f"(scaletta {rep['ladder']}, tetto {rep['tetto']:.2f}x — costanti di CODICE)")
|
||||
for nome, d in rep["domande"].items():
|
||||
print(f"\n {nome} [{d['stato']}] {d['motivo']}")
|
||||
if d.get("azione") and d["azione"] != "nessuna":
|
||||
print(f" → azione: {d['azione']}")
|
||||
fb = rep["fallback"]
|
||||
if fb.get("stato") == "NON MISURABILE":
|
||||
print(f"\n fallback [{fb['stato']}] {fb.get('motivo')}")
|
||||
else:
|
||||
print(f"\n fallback [{fb['stato']}] equity illeggibile in {fb['fallback_veri']}/{fb['giri']} "
|
||||
f"giri ({100*fb['quota']:.2f}%, soglia {100*fb['soglia']:.0f}%) — "
|
||||
f"{fb['paper']} giri pre-finanziamento esclusi, li' il libro non invia affatto")
|
||||
print(f"\n lettore di produzione [{rep['lettore']['stato']}] {rep['lettore']['motivo'][:150]}")
|
||||
print(f"\n invio: {rep['invio']}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user