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:
Adriano Dal Pastro
2026-09-10 13:24:28 +00:00
parent 79afe41ec2
commit f82f685528
13 changed files with 1110 additions and 36 deletions
+126
View File
@@ -0,0 +1,126 @@
"""GATE PREVDAY-01 cablato nel monitor (issue #4): kill, veto d'integrita', data.
Prima del 10/09 il gate esisteva solo nel testo (RESULTS-0822 §54) e la revisione settimanale lo
ha letto come «senza data». Qui si blinda che: (a) le costanti sono quelle scritte il 23/08;
(b) il kill non e' leggibile sotto 180 giorni attivi e scatta solo sotto -0,50; (c) il veto
blocca sotto l'80% di barre ricostruibili e su una crescita MATERIALE delle divergenze — non su
una barra sola (P14); (d) la ricostruzione riproduce una serie generata con la stessa aritmetica
di `advance`; (e) la lente del kill e' GIORNALIERA."""
from __future__ import annotations
import importlib.util
import sys
from pathlib import Path
import numpy as np
import pandas as pd
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
sp = importlib.util.spec_from_file_location("paper_prevday", ROOT / "scripts" / "live" / "paper_prevday.py")
PP = importlib.util.module_from_spec(sp)
sp.loader.exec_module(PP)
def test_le_costanti_sono_quelle_del_gate_scritto_il_23_08():
assert PP.GATE_DATE == "2027-06-21"
assert PP.KILL_SHARPE == -0.50 and PP.KILL_MIN_ACTIVE_DAYS == 180
assert PP.MIN_RECON_FRAC == 0.80
# e CLAUDE.md §4 cita la stessa data (era assente fino al 10/09)
assert "2027-06-21" in (ROOT / "CLAUDE.md").read_text()
def _serie(n_giorni: int, mu_h: float, sd_h: float, seed: int = 0, pos: float = 0.3) -> pd.DataFrame:
rng = np.random.default_rng(seed)
n = n_giorni * 24
ts = 1_782_000_000_000 + np.arange(n) * 3_600_000
x = rng.normal(mu_h, sd_h, n)
return pd.DataFrame(dict(ts=ts, dt=pd.to_datetime(ts, unit="ms", utc=True), net_modeled=x,
net_real=x, pos_btc=pos, pos_eth=pos))
def test_kill_non_leggibile_sotto_180_giorni_attivi():
r = _serie(81, -0.001, 0.004)
sh, n = PP.sharpe_giornaliero(r)
assert n == 81 and sh < PP.KILL_SHARPE # Sharpe pessimo...
stato, motivo = PP.kill_check(sh, PP.giorni_attivi(r), pd.Timestamp("2026-09-10", tz="UTC"))
assert stato == "NON_MATURO" and "99" in motivo # ...ma il kill non e' leggibile
def test_kill_scatta_sopra_180_giorni_solo_sotto_la_soglia():
r_male = _serie(200, -0.001, 0.004)
sh, _ = PP.sharpe_giornaliero(r_male)
assert PP.kill_check(sh, PP.giorni_attivi(r_male), pd.Timestamp("2027-01-01", tz="UTC"))[0] == "KILL"
oggi = pd.Timestamp("2027-01-01", tz="UTC")
assert PP.kill_check(-0.49, 200, oggi)[0] == "SUPERATO" # la soglia e' STRETTA: -0,49 passa
assert PP.kill_check(-0.51, 200, oggi)[0] == "KILL"
assert PP.kill_check(float("nan"), 200, oggi)[0] == "NON_MISURABILE"
def test_i_giorni_flat_non_contano_come_attivi():
r = _serie(200, 0.0, 0.004)
r.loc[r.index[: 50 * 24], ["pos_btc", "pos_eth"]] = 0.0
assert PP.giorni_attivi(r) == 150
def test_la_lente_del_kill_e_giornaliera_non_oraria():
"""§54: sulla lente oraria lo stesso forward vale +2,04 contro +1,56. Con autocorrelazione
positiva intragiornaliera la lente oraria SOVRASTIMA: il kill usa la giornaliera."""
rng = np.random.default_rng(3)
n = 120 * 24
base = np.repeat(rng.normal(0.0004, 0.004, 120), 24) # shock comune al giorno
x = base + rng.normal(0, 0.0005, n)
ts = 1_782_000_000_000 + np.arange(n) * 3_600_000
r = pd.DataFrame(dict(ts=ts, dt=pd.to_datetime(ts, unit="ms", utc=True), net_modeled=x,
net_real=x, pos_btc=0.3, pos_eth=0.3))
sh_g, _ = PP.sharpe_giornaliero(r)
assert PP._sharpe_orario(r) > sh_g * 2 # la lente oraria gonfia; il kill non la usa
def test_veto_sotto_80_pct_ricostruibili():
ok, m = PP.veto_check(0.79, 0.5, 0.5, 10)
assert not ok and "79.0%" in m
assert PP.veto_check(0.80, 0.5, 0.5, 10)[0]
def test_veto_su_crescita_materiale_non_su_una_barra(monkeypatch):
"""Il 10/09 la serie reale aveva UNA barra divergente su 1943 (26/08 00:00, il giorno del fix
di advance()) e la prima stesura del veto la leggeva come 'crescita' (0,03/g contro 0,01/g)."""
assert PP.veto_check(0.999, 0.012, 0.032, 1)[0] # 1 barra: non e' crescita
ok, m = PP.veto_check(0.95, 0.10, 0.50, 12) # 12 recenti, tasso 5x: crescita
assert not ok and "crescita" in m
assert PP.veto_check(0.95, 0.30, 0.50, 12)[0] # tasso < 2x: no
def test_ricostruzione_riproduce_una_serie_generata_come_advance(monkeypatch):
"""Feed sintetico, target congelato deterministico (monkeypatch su prevday_target): la serie
registrata con l'aritmetica di advance si ricostruisce al 100%; una barra manomessa no."""
n = 24 * 10
ts = 1_782_000_000_000 + np.arange(n) * 3_600_000
rng = np.random.default_rng(7)
dfs = {}
for a, p0 in (("BTC", 60000.0), ("ETH", 2000.0)):
close = p0 * np.cumprod(1 + rng.normal(0, 0.003, n))
dfs[a] = pd.DataFrame(dict(timestamp=ts, datetime=pd.to_datetime(ts, unit="ms", utc=True), close=close))
tgt = {a: np.where((np.arange(n) // 24) % 2 == 0, 0.3, -0.3) for a in dfs}
monkeypatch.setattr(PP, "prevday_target",
lambda df: tgt["BTC"] if float(df["close"].iloc[0]) > 10000 else tgt["ETH"])
start = int(ts[23])
# registrazione con la stessa aritmetica di advance (ribilanciamento continuo)
pos = {a: float(tgt[a][23]) for a in dfs}
rows = []
for i in range(24, n):
net = 0.0
for a in dfs:
c = dfs[a]["close"].values
rr = c[i] / c[i - 1] - 1
net += PP.WEIGHT * (pos[a] * rr - PP.FEE_SIDE * abs(tgt[a][i] - pos[a]))
pos[a] = float(tgt[a][i])
rows.append(dict(ts=int(ts[i]), net_modeled=round(net, 6), pos_btc=pos["BTC"], pos_eth=pos["ETH"]))
r = pd.DataFrame(rows); r["dt"] = pd.to_datetime(r["ts"], unit="ms", utc=True)
rc = PP.ricostruzione(r, dfs, start)
assert rc["ricostruibili"] == rc["n"] == n - 24 and rc["assenti_nel_feed"] == 0
r2 = r.copy(); r2.loc[r2.index[-1], "net_modeled"] += 0.01
rc2 = PP.ricostruzione(r2, dfs, start)
assert rc2["ricostruibili"] == rc2["n"] - 1 and rc2["div_recenti"] == 1