cinque punti della revisione 09/09: usde_watch a 1000 trade + finestra 24h, cuscino_watch (equity USDC, riconverte da solo), PREVDAY-01 kill/veto cablati, SCALA-01 al 2027-02-28, versamento 04/09 dichiarato
Decisi dall'operatore il 2026-09-10, verificati da revisione fable (15 segnalazioni, 12 applicate). - usde_watch: TRADE_LIMIT 1000 (count max Deribit) e trade_copertura(): lista troncata o ultima lettura oltre la finestra di 24h del gateway => somma NON leggibile, reward non attribuito (P12). Riga del 07/09 corretta nel log con campo `correzione` (400 USDE erano acquisti, non reward). - cuscino_watch.py (cron :53, monitor_health): equity USDC contro cuscino derivato da usde.cuscino_richiesto_usd (formula spostata in src/live/usde.py, usde_convert la importa); OK/PREAVVISO/SCOPERTO/BLIND; sotto zero lancia usde_convert --quota quota_ripristino(0.20)=0.64 --esegui con guardie (execution_enabled, depeg_warn, 1 tentativo/6h). Primo giro: PREAVVISO, +$26. - usde_convert: il tetto del venue vale solo in ACQUISTO (bloccava la vendita). - paper_prevday: GATE PREVDAY-01 cablato (2027-06-21, kill Sharpe giornaliero < -0,50 su >=180 g attivi, veto >=80% barre ricostruibili + divergenze non crescenti con soglia materiale). Oggi: +0,95 su 81 g, 1942/1943 ricostruibili, kill NON MATURO. - CLAUDE.md: arming 20/06 (TP01) / 23/06 (BOOK); piano EUR 5.000 chiuso col versamento 04/09 ($2.414,68 dal balance, dichiarato); SCALA-01 non prima del 2027-02-28 (A2 dal 01/09); PREVDAY-01 con data, kill, veto; §5.17 riparato con i limiti dichiarati (ratchet, slack zero a 0,70). - test: 1062 (+31): test_cuscino_watch (16), test_paper_prevday_gate (8), test_usde_watch (+5). Fixes #2 Fixes #3 Fixes #4 Fixes #5 Fixes #6 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016kqvff47UBGeYfj1QeN4zE
This commit is contained in:
@@ -53,6 +53,21 @@ MODELED_CAPITAL = 2000.0 # nominale, ribilanciamento continuo
|
||||
REAL_CAPITAL = 600.0 # capitale mainnet reale
|
||||
MIN_ORDER = 5.0 # min order Deribit -> sotto, il conto vero NON ribilancia
|
||||
|
||||
# --- GATE PRE-REGISTRATO `GATE PREVDAY-01` (2026-08-23, RESULTS-0822 §54) -----------------------
|
||||
# Cablato qui il 2026-09-10 (issue #4): esisteva solo nel testo, e la revisione del 09/09 lo ha
|
||||
# letto come "gate senza data". Decisione dell'operatore: tenere la data, cablare kill e veto.
|
||||
GATE_DATE = "2027-06-21" # decisione piena: 7 condizioni (a)-(g), tutte necessarie
|
||||
KILL_SHARPE = -0.50 # kill: Sharpe forward < -0,50 ...
|
||||
KILL_MIN_ACTIVE_DAYS = 180 # ... su >= 180 giorni di barre ATTIVE (posizione != 0)
|
||||
MIN_RECON_FRAC = 0.80 # veto d'integrita': >= 80% di barre ricostruibili dal feed
|
||||
RECON_TOL = 1.5e-6 # net_modeled e' registrato a 6 decimali: mezzo ulp di tolleranza
|
||||
RECENTI_GIORNI = 30 # "i minuti non registrati non devono crescere": ultimi 30g vs tutto
|
||||
MIN_DIV_CRESCITA = 5 # ... ma UNA barra divergente su 1943 non e' una crescita (P14): la
|
||||
# crescita richiede >= 5 divergenti recenti E tasso doppio del totale
|
||||
# LENTE: il kill si misura sullo Sharpe GIORNALIERO (somma delle barre orarie per giorno UTC),
|
||||
# la lente con cui e' misurato ogni altro sleeve del progetto — §54: sulla lente ORARIA lo stesso
|
||||
# forward vale +2,04 contro +1,56 giornaliero. Al kill la famiglia NON si ri-ottimizza.
|
||||
|
||||
|
||||
def build_bars() -> dict[str, pd.DataFrame]:
|
||||
return {a: load(a, "1h").reset_index(drop=True) for a in ASSETS}
|
||||
@@ -145,6 +160,151 @@ def advance(st: dict, dfs: dict) -> dict:
|
||||
return st
|
||||
|
||||
|
||||
def _returns_df() -> pd.DataFrame | None:
|
||||
if not RETURNS_FILE.exists():
|
||||
return None
|
||||
r = pd.read_json(RETURNS_FILE, lines=True)
|
||||
if r.empty:
|
||||
return None
|
||||
r["dt"] = pd.to_datetime(r["ts"], unit="ms", utc=True)
|
||||
return r
|
||||
|
||||
|
||||
def sharpe_giornaliero(r: pd.DataFrame, col: str = "net_modeled") -> tuple[float, int]:
|
||||
"""PURA. Sharpe annualizzato sui rendimenti GIORNALIERI (somma delle barre orarie per giorno
|
||||
UTC) -> (sharpe, n_giorni). nan sotto 30 giorni o a varianza nulla."""
|
||||
d = r.groupby(r["dt"].dt.floor("D"))[col].sum()
|
||||
if len(d) < 30 or d.std() == 0:
|
||||
return float("nan"), int(len(d))
|
||||
return float(d.mean() / d.std() * np.sqrt(365.25)), int(len(d))
|
||||
|
||||
|
||||
def giorni_attivi(r: pd.DataFrame) -> int:
|
||||
"""PURA. Giorni UTC con almeno una barra a posizione != 0 (il kill conta le barre ATTIVE).
|
||||
⚠️ le posizioni registrate sono quelle del libro REAL-$600 (advance scrive `pr`), lo Sharpe
|
||||
del kill e' MODELED: divergono solo se un ingresso da 0 vale < $5 di nozionale, cosa che a
|
||||
vol-target 20% non succede (10/09: 81/81 giorni). Dichiarato, non riparato (D5)."""
|
||||
att = r[(r["pos_btc"] != 0) | (r["pos_eth"] != 0)]
|
||||
return int(att["dt"].dt.floor("D").nunique())
|
||||
|
||||
|
||||
def kill_check(sharpe: float, n_attivi: int, today: pd.Timestamp) -> tuple[str, str]:
|
||||
"""PURA. -> (stato, motivo). KILL solo con >= 180 giorni attivi E Sharpe < -0,50 (§54):
|
||||
a quell'orizzonte P(uccidere un edge vivo a Sharpe 1,2) = 9,7%."""
|
||||
if n_attivi < KILL_MIN_ACTIVE_DAYS:
|
||||
return "NON_MATURO", (f"{n_attivi} giorni attivi < {KILL_MIN_ACTIVE_DAYS}: il kill non e' "
|
||||
f"leggibile (mancano {KILL_MIN_ACTIVE_DAYS - n_attivi} giorni)")
|
||||
if not np.isfinite(sharpe):
|
||||
return "NON_MISURABILE", "Sharpe forward non misurabile"
|
||||
if sharpe < KILL_SHARPE:
|
||||
return "KILL", f"Sharpe {sharpe:+.2f} < {KILL_SHARPE:+.2f} su {n_attivi} giorni attivi -> RITIRARE, senza ri-ottimizzare"
|
||||
return "SUPERATO", f"Sharpe {sharpe:+.2f} >= {KILL_SHARPE:+.2f} su {n_attivi} giorni attivi"
|
||||
|
||||
|
||||
def ricostruzione(r: pd.DataFrame, dfs: dict, start_ts: int) -> dict:
|
||||
"""Rigioca la strategia CONGELATA sul feed di OGGI dallo `start_ts` del monitor e confronta
|
||||
barra per barra `net_modeled` registrato con quello ricalcolato (ribilanciamento continuo,
|
||||
la stessa aritmetica di `advance`). Una barra e' RICOSTRUIBILE se coincide entro RECON_TOL.
|
||||
|
||||
Cosa misura: quante barre registrate sono riproducibili dal dato certificato. Le divergenti
|
||||
sono barre calcolate su un feed che il rebuild ha poi rivisto (§54: 67 su 1512, tutte
|
||||
all'ora del cron) — e' la misura del veto d'integrita' (>= 80%, e i minuti persi non devono
|
||||
crescere: qui, divergenti/giorno negli ultimi 30g contro l'intera finestra)."""
|
||||
data = {}
|
||||
for a in ASSETS:
|
||||
df = dfs[a]
|
||||
c = df["close"].values.astype(float)
|
||||
rr = np.zeros(len(c)); rr[1:] = c[1:] / c[:-1] - 1.0
|
||||
data[a] = dict(ts=df["timestamp"].values.astype("int64"), r=rr, tgt=prevday_target(df))
|
||||
idx = {a: {int(t): i for i, t in enumerate(data[a]["ts"])} for a in ASSETS}
|
||||
# posizione iniziale come init_state: target all'ultima barra <= start_ts
|
||||
pos = {}
|
||||
for a in ASSETS:
|
||||
i0 = max(i for t, i in idx[a].items() if t <= start_ts)
|
||||
pos[a] = float(data[a]["tgt"][i0])
|
||||
ricalc, presenti = [], []
|
||||
for t in r["ts"].astype("int64"):
|
||||
t = int(t)
|
||||
if any(t not in idx[a] for a in ASSETS):
|
||||
ricalc.append(np.nan); presenti.append(False); continue
|
||||
net = 0.0
|
||||
for a in ASSETS:
|
||||
i = idx[a][t]
|
||||
tgt = float(data[a]["tgt"][i])
|
||||
net += WEIGHT * (pos[a] * float(data[a]["r"][i]) - FEE_SIDE * abs(tgt - pos[a]))
|
||||
pos[a] = tgt
|
||||
ricalc.append(net); presenti.append(True)
|
||||
ricalc = np.asarray(ricalc, dtype=float)
|
||||
reg = r["net_modeled"].values.astype(float)
|
||||
ok = np.isfinite(ricalc) & (np.abs(ricalc - reg) <= RECON_TOL)
|
||||
n = len(reg)
|
||||
giorni = r["dt"].dt.floor("D")
|
||||
span_g = max(1, int(giorni.nunique()))
|
||||
recenti = r["dt"] >= (r["dt"].max() - pd.Timedelta(days=RECENTI_GIORNI))
|
||||
div_rec = int((~ok[recenti.values]).sum())
|
||||
g_rec = max(1, int(giorni[recenti].nunique()))
|
||||
return dict(n=n, ricostruibili=int(ok.sum()), frac=float(ok.sum() / n) if n else float("nan"),
|
||||
assenti_nel_feed=int((~np.asarray(presenti)).sum()),
|
||||
div_per_giorno=float((n - ok.sum()) / span_g),
|
||||
div_recenti=div_rec, div_per_giorno_recenti=float(div_rec / g_rec),
|
||||
divergenti_ts=[int(t) for t in r["ts"].values[~ok]][:20])
|
||||
|
||||
|
||||
def veto_check(frac: float, div_g_tot: float, div_g_rec: float, div_rec: int) -> tuple[bool, str]:
|
||||
"""PURA. Veto d'integrita' (blocca, non decide): sotto l'80% di barre ricostruibili, o con le
|
||||
divergenze che CRESCONO negli ultimi 30 giorni, la finestra si ESTENDE invece di decidere."""
|
||||
motivi = []
|
||||
if not np.isfinite(frac) or frac < MIN_RECON_FRAC:
|
||||
motivi.append(f"barre ricostruibili {frac:.1%} < {MIN_RECON_FRAC:.0%}")
|
||||
if div_rec >= MIN_DIV_CRESCITA and div_g_rec > 2.0 * div_g_tot + 1e-12:
|
||||
motivi.append(f"divergenze in crescita: {div_rec} negli ultimi {RECENTI_GIORNI}g "
|
||||
f"({div_g_rec:.2f}/g contro {div_g_tot:.2f}/g sull'intera finestra)")
|
||||
return (not motivi), ("; ".join(motivi) if motivi else
|
||||
f"ricostruibili {frac:.1%}, divergenti {div_rec} negli ultimi {RECENTI_GIORNI}g "
|
||||
f"({div_g_rec:.2f}/g contro {div_g_tot:.2f}/g totale)")
|
||||
|
||||
|
||||
def print_gate(st: dict, dfs: dict, today: pd.Timestamp | None = None) -> dict:
|
||||
"""Stampa lo stato del gate pre-registrato e lo RITORNA (per i test e per chi legge il log).
|
||||
Non decide niente: al kill e alla data stampa cosa la regola dice, in maiuscolo."""
|
||||
r = _returns_df()
|
||||
out = dict(gate_date=GATE_DATE, kill="NON_MISURABILE", veto_ok=None)
|
||||
print(f"\n GATE PRE-REGISTRATO `PREVDAY-01` (23/08, RESULTS-0822 §54) — decisione {GATE_DATE}")
|
||||
if r is None or len(r) < 2:
|
||||
print(" serie forward assente o troppo corta: niente da misurare")
|
||||
return out
|
||||
today = today or pd.Timestamp.now(tz="UTC").normalize()
|
||||
sh, n_g = sharpe_giornaliero(r)
|
||||
n_att = giorni_attivi(r)
|
||||
stato, motivo = kill_check(sh, n_att, today)
|
||||
out.update(sharpe_giornaliero=sh, n_giorni=n_g, n_attivi=n_att, kill=stato, kill_motivo=motivo)
|
||||
print(f" Sharpe forward GIORNALIERO (MODELED): {sh:+.2f} su {n_g} giorni, {n_att} attivi"
|
||||
f" [lente oraria {_sharpe_orario(r):+.2f}: non si cita, §54]")
|
||||
if stato == "KILL":
|
||||
print(f" *** KILL: {motivo} ***")
|
||||
else:
|
||||
print(f" kill (Sharpe < {KILL_SHARPE:+.2f} su >= {KILL_MIN_ACTIVE_DAYS}g attivi): {stato} — {motivo}")
|
||||
rc = ricostruzione(r, dfs, int(st["start_ts"]))
|
||||
ok, vm = veto_check(rc["frac"], rc["div_per_giorno"], rc["div_per_giorno_recenti"], rc["div_recenti"])
|
||||
out.update(ricostruzione=rc, veto_ok=ok, veto_motivo=vm)
|
||||
print(f" veto d'integrita': {'ok' if ok else '*** VETO — la finestra si ESTENDE ***'} — {vm}"
|
||||
f" ({rc['ricostruibili']}/{rc['n']} barre, {rc['assenti_nel_feed']} assenti nel feed)")
|
||||
if today >= pd.Timestamp(GATE_DATE, tz="UTC"):
|
||||
print(f" *** DECISIONE DOVUTA ({GATE_DATE}): (a) DSR >= 0,95 sulla famiglia dichiarata "
|
||||
f"(b) cella al buio == congelata (c) delta libro > +0,05 col funding (d) weights_tilt_null "
|
||||
f"(e) ADDS+robust_oos+non-hedge sulla cella al buio (f) day_boundary_robust "
|
||||
f"(g) Sharpe forward > 0 [{sh:+.2f}] — TUTTE necessarie ***")
|
||||
else:
|
||||
print(f" decisione fra {(pd.Timestamp(GATE_DATE, tz='UTC') - today).days} giorni; "
|
||||
f"le condizioni (a)-(b) sono STRUTTURALI e il forward non le cambia (§54)")
|
||||
return out
|
||||
|
||||
|
||||
def _sharpe_orario(r: pd.DataFrame) -> float:
|
||||
x = r["net_modeled"].astype(float)
|
||||
return float(x.mean() / x.std() * np.sqrt(24 * 365.25)) if x.std() > 0 else float("nan")
|
||||
|
||||
|
||||
def print_status(st: dict, dfs: dict):
|
||||
days = (max(int(dfs[a]["timestamp"].iloc[-1]) for a in ASSETS) - st["start_ts"]) / 86400_000
|
||||
rm = st["cap_modeled"] / MODELED_CAPITAL - 1
|
||||
@@ -156,7 +316,9 @@ def print_status(st: dict, dfs: dict):
|
||||
print(f" MODELED ($2000 nominale): {rm*100:+6.2f}% eq ${st['cap_modeled']:.2f} maxDD {st['dd_modeled']*100:.1f}%")
|
||||
print(f" REAL-$600 (min-order $5) : {rr*100:+6.2f}% eq ${st['cap_real']:.2f} maxDD {st['dd_real']*100:.1f}%")
|
||||
print(f" -> fill-haircut MODELED-REAL: {(rm-rr)*100:+.2f} pp (lo scettico l'aveva segnalato)")
|
||||
print(f" log: {RETURNS_FILE}\n")
|
||||
print(f" log: {RETURNS_FILE}")
|
||||
print_gate(st, dfs)
|
||||
print()
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
Reference in New Issue
Block a user