research(wave-0822): GATE-RECON — il difetto ribalta STATARB (+1,96 -> -2,02), la premessa della rigenerazione e' confermata per via diretta
This commit is contained in:
@@ -0,0 +1,719 @@
|
||||
"""GATE-RECON — cosa direbbero i tre gate pre-registrati se le serie forward fossero giuste.
|
||||
|
||||
IL FATTO DA CUI NASCE (filone MONITOR-AUDIT, 22/08, gia' misurato e NON riparato per scelta).
|
||||
`fetch_hyperliquid` e `resample_tf` scrivono la barra del giorno IN CORSO; `advance()` la consuma
|
||||
e porta `last_ts` su di essa -> le ore restanti non entrano in nessun rendimento. Quattro monitor
|
||||
su sei registrano una FRAZIONE del giorno (2, 4, 19 e 41 minuti su 1440). Tre decisioni
|
||||
pre-registrate leggono serie costruite cosi':
|
||||
STATARB 2026-09-27 r0724_statarb_deploy_gate (Sh>=0.5, maxDD<10%, haircut<0.5pp)
|
||||
XSR01 2026-10-23 r0725_xsr_deploy_gate (Sh>=1.0 E haircut$5000<=40%)
|
||||
DVOLSPREAD 2026-10-24 kill / 2027-01-24 decisione (soglie dentro paper_dvolspread)
|
||||
|
||||
QUESTO SCRIPT NON RIPARA E NON DECIDE. La riparazione e' una decisione dell'operatore e i gate
|
||||
hanno una data: anticiparli sarebbe selezione-sul-forward, il bias che questo progetto ha gia'
|
||||
pagato due volte. La consegna e' un DELTA, non un verdetto:
|
||||
"alla data di oggi, con la serie riparata il criterio direbbe Y; con la serie com'e', Z"
|
||||
e la distanza fra Y e Z e' la misura del danno del difetto — l'unica cosa che serve per decidere
|
||||
SE e QUANDO riparare. Sola lettura su tutto cio' che e' produzione: i moduli di `scripts/live/`
|
||||
si importano senza chiamarne `main()`, e `_append` viene sostituito con un raccoglitore in memoria
|
||||
PRIMA di qualunque chiamata ad `advance()` -> nessun file di `data/` viene aperto in scrittura.
|
||||
|
||||
METODO. La serie RIPARATA non e' una reimplementazione: e' il codice di produzione rieseguito
|
||||
dall'inception sui dati di oggi, TOLTA l'ultima barra (l'unica ancora aperta). Tutto il resto del
|
||||
pannello e' fatto di barre chiuse, quindi il replay E' la serie che il monitor avrebbe registrato
|
||||
se avesse aspettato la chiusura. Questo poggia su due condizioni, entrambe verificate qui:
|
||||
(P1) il feed non riscrive le barre gia' chiuse -> sezione 0;
|
||||
(P2) il pannello che leggo ora e' quello che il cron ha letto stanotte, e troncare l'ULTIMA
|
||||
barra non cambia nessuna barra precedente -> sezione 1 (tre prove d'identita').
|
||||
Senza (P2) starei ricostruendo una strategia diversa invece della stessa strategia riparata, ed e'
|
||||
il modo piu' facile di sbagliare questo lavoro.
|
||||
|
||||
COSA MI ASPETTAVO PRIMA DI MISURARE (registrato qui perche' sia falsificabile):
|
||||
* la premessa (P1) vera ma verificabile solo di traverso -> **sbagliato, ed e' la sorpresa
|
||||
utile**: `data/_feed_backup/*.prebuild.bak` conserva il vintage PRE-riscrittura di stanotte,
|
||||
quindi la premessa si verifica DIRETTAMENTE su ~914.000 barre, senza passare da una strategia;
|
||||
* i tre gate tutti ribaltati -> **sbagliato, e la ragione conta**: solo STATARB si ribalta.
|
||||
XSR01 e DVOLSPREAD dicono "ritiro/kill" su ENTRAMBE le lenti, ma con numeri che differiscono
|
||||
di 3-7x e che a 28 barre non sono letture di gate su nessuna delle due;
|
||||
* il veto d'integrita' di DVOLSPREAD (l'unico dei tre che ha una guardia) capace di accorgersene
|
||||
-> **confermato che NON se ne accorge**, e per una ragione strutturale: conta se il DVOL C'ERA,
|
||||
non quanto dura la barra su cui c'era.
|
||||
|
||||
nice -n 19 timeout 900 uv run python scripts/research/r0822c_gate_recon.py
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
import importlib.util
|
||||
import json
|
||||
import sys
|
||||
from datetime import date
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
RAW = ROOT / "data" / "raw"
|
||||
BAK = ROOT / "data" / "_feed_backup"
|
||||
for _p in (ROOT, ROOT / "scripts" / "research", ROOT / "scripts" / "research" / "alt",
|
||||
ROOT / "scripts" / "research" / "ortho"):
|
||||
sys.path.insert(0, str(_p))
|
||||
|
||||
ANN = np.sqrt(365.0)
|
||||
TODAY = date.today()
|
||||
H = "=" * 100
|
||||
# Incidente del feed 2026-07-09 -> 2026-07-15 (`rebuild_history` abortito 7 giri, EPERM sul backup,
|
||||
# gia' riparato): col feed fermo i monitor non trovavano barre nuove e al ripristino ne hanno
|
||||
# recuperate in blocco, GIA' CHIUSE. Sono le uniche barre corrette di `paper_statarb` e vanno
|
||||
# escluse da ogni stima del difetto, o gli si attribuisce un'attenuazione che viene da altro.
|
||||
FREEZE = (pd.Timestamp("2026-07-09", tz="UTC"), pd.Timestamp("2026-07-16", tz="UTC"))
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------------------------
|
||||
# infrastruttura di sola lettura (stessa di r0822_monitor_audit: nessuna reimplementazione)
|
||||
# --------------------------------------------------------------------------------------------
|
||||
_MODS: dict[str, object] = {}
|
||||
|
||||
|
||||
def live(name: str):
|
||||
"""Esegue un modulo di `scripts/live/` per averne le DEFINIZIONI. Mai `main()`."""
|
||||
if name in _MODS:
|
||||
return _MODS[name]
|
||||
spec = importlib.util.spec_from_file_location(f"_ro_{name}", ROOT / "scripts" / "live" / f"{name}.py")
|
||||
m = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(m)
|
||||
_MODS[name] = m
|
||||
return m
|
||||
|
||||
|
||||
class Sink:
|
||||
"""Sostituisce `_append`: raccoglie in memoria invece di scrivere. E' l'unica ragione per cui
|
||||
si puo' rieseguire il codice di produzione TALE E QUALE restando in sola lettura."""
|
||||
|
||||
def __init__(self):
|
||||
self.rows: list[tuple[str, dict]] = []
|
||||
|
||||
def __call__(self, path, rec):
|
||||
self.rows.append((Path(path).name, dict(rec)))
|
||||
|
||||
def of(self, fname: str) -> list[dict]:
|
||||
return [r for n, r in self.rows if n == fname]
|
||||
|
||||
|
||||
def read_jsonl(p: Path) -> list[dict]:
|
||||
return [json.loads(x) for x in p.read_text().splitlines() if x.strip()] if p.exists() else []
|
||||
|
||||
|
||||
def mets(a) -> dict:
|
||||
"""Sharpe (365), maxDD e ritorno cumulato di una serie di rendimenti netti per barra."""
|
||||
a = np.asarray(a, float)
|
||||
a = a[np.isfinite(a)]
|
||||
if len(a) < 2 or a.std() == 0:
|
||||
return dict(n=len(a), sh=float("nan"), dd=float("nan"), tot=float("nan"),
|
||||
mu=float("nan"), sd=float("nan"))
|
||||
eq = np.cumprod(1.0 + a)
|
||||
pk = np.maximum.accumulate(eq)
|
||||
return dict(n=len(a), sh=float(a.mean() / a.std() * ANN),
|
||||
dd=float(np.max((pk - eq) / pk)), tot=float(eq[-1] - 1.0),
|
||||
mu=float(a.mean() * 365.0), sd=float(a.std() * ANN))
|
||||
|
||||
|
||||
def se_sharpe(n: int) -> float:
|
||||
"""Errore standard grezzo di uno Sharpe annualizzato su n barre giornaliere (~sqrt(365/n))."""
|
||||
return float(np.sqrt(365.0 / max(n, 1)))
|
||||
|
||||
|
||||
# ============================================================================================
|
||||
# 0 — LA PREMESSA: rigenerare non perde la finestra SOLO se le barre chiuse non vengono riscritte
|
||||
# ============================================================================================
|
||||
def sezione_0() -> dict:
|
||||
print(H)
|
||||
print(" 0 — PREMESSA: il feed riscrive le barre gia' CHIUSE?")
|
||||
print(H)
|
||||
print("""
|
||||
Perche' e' la premessa di tutto: la raccomandazione del filone MONITOR-AUDIT e' "riparare
|
||||
`advance()` + RIGENERARE", cioe' ricalcolare la finestra forward dall'inception sui dati di
|
||||
oggi. Se il feed riscrivesse le barre chiuse, quella rigenerazione produrrebbe una serie
|
||||
DIVERSA da quella che si sarebbe registrata giorno per giorno -> la finestra andrebbe persa e
|
||||
la conclusione cambierebbe. E' stata verificata due volte di traverso (via strategie); qui si
|
||||
verifica una terza volta e in modo DIRETTO.""")
|
||||
|
||||
out = {}
|
||||
print("\n 0a. VERIFICA DIRETTA sul dato grezzo — vintage PRE-riscrittura contro oggi")
|
||||
print(" `rebuild_history` copia il parquet in data/_feed_backup/*.prebuild.bak PRIMA di")
|
||||
print(" sovrascriverlo: sul disco c'e' il file com'era ieri sera. Nessuna strategia in mezzo.")
|
||||
tot_ch, tot_div = 0, 0
|
||||
for a in ("btc", "eth"):
|
||||
for tf in ("1h", "5m"):
|
||||
new_p, old_p = RAW / f"{a}_{tf}.parquet", BAK / f"{a}_{tf}.parquet.prebuild.bak"
|
||||
if not (new_p.exists() and old_p.exists()):
|
||||
print(f" {a}_{tf}: vintage assente -> NON MISURATO")
|
||||
continue
|
||||
new = pd.read_parquet(new_p)
|
||||
old = pd.read_parquet(old_p)
|
||||
m = old.merge(new, on="timestamp", suffixes=("_o", "_n"))
|
||||
cols = ["open", "high", "low", "close", "volume"]
|
||||
diff = np.zeros(len(m), bool)
|
||||
for c in cols:
|
||||
diff |= (m[f"{c}_o"].values != m[f"{c}_n"].values)
|
||||
last_old = int(old["timestamp"].max())
|
||||
chiuse = m["timestamp"].values < last_old # nel vecchio file l'ultima riga era APERTA
|
||||
nch, nd = int(chiuse.sum()), int((diff & chiuse).sum())
|
||||
tot_ch += nch
|
||||
tot_div += nd
|
||||
print(f" {a}_{tf}: {nch:,} barre gia' chiuse nel vintage -> DIVERSE OGGI: {nd}"
|
||||
f" (righe cambiate in tutto: {int(diff.sum())})")
|
||||
if diff.sum() == 1:
|
||||
i = int(np.where(diff)[0][0])
|
||||
ts = pd.Timestamp(int(m["timestamp"].iloc[i]), unit="ms", tz="UTC")
|
||||
print(f" l'unica riga cambiata e' {ts} = l'ULTIMA del vintage, che era "
|
||||
f"aperta: close {m['close_o'].iloc[i]:g} -> {m['close_n'].iloc[i]:g}, "
|
||||
f"volume {m['volume_o'].iloc[i]:,.0f} -> {m['volume_n'].iloc[i]:,.0f}")
|
||||
out["chiuse"], out["div"] = tot_ch, tot_div
|
||||
print(f"\n TOTALE: {tot_div} barre chiuse cambiate su {tot_ch:,} "
|
||||
f"-> la riscrittura notturna e' un NO-OP sulle barre chiuse.")
|
||||
print(" ⚠ questo e' UN ciclo di riscrittura (ieri->oggi). La stessa riga mostra anche il")
|
||||
print(" DIFETTO: l'ultima riga del vintage era una barra parziale, ed e' esattamente")
|
||||
print(" quella che i monitor consumano.")
|
||||
|
||||
print("\n 0b. VERIFICA su 62 CICLI — tasso di divergenza per ETA' della barra (paper_prevday)")
|
||||
print(" Statistica diversa da quella del 22/08 (che contava le identiche): se il feed")
|
||||
print(" riscrivesse le barre chiuse, il tasso CRESCEREBBE con l'eta' (piu' riscritture")
|
||||
print(" subite). Se il difetto e' solo la barra parziale al momento della registrazione,")
|
||||
print(" il tasso e' PIATTO nell'eta' e vale ~1/24.")
|
||||
pp = live("paper_prevday")
|
||||
st = json.loads((ROOT / "data" / "paper_prevday" / "state.json").read_text())
|
||||
rec = read_jsonl(ROOT / "data" / "paper_prevday" / "returns.jsonl")
|
||||
dfs = pp.build_bars()
|
||||
S = st["start_ts"]
|
||||
pos = {a: pp.pb.current_target(dfs[a][dfs[a]["timestamp"] <= S]) for a in pp.ASSETS}
|
||||
sink = Sink()
|
||||
pp._append = sink
|
||||
st0 = dict(start_ts=S, last_ts=S, n_bars=0, pos_modeled=pos, pos_real=dict(pos),
|
||||
cap_modeled=pp.MODELED_CAPITAL, cap_real=pp.REAL_CAPITAL,
|
||||
peak_modeled=pp.MODELED_CAPITAL, peak_real=pp.REAL_CAPITAL,
|
||||
dd_modeled=0.0, dd_real=0.0, n_trades=0)
|
||||
pp.advance(st0, dfs)
|
||||
rep = {int(r["ts"]): float(r["net_modeled"]) for r in sink.of("returns.jsonl")}
|
||||
pairs = [(int(r["ts"]), float(r["net_modeled"]), rep[int(r["ts"])])
|
||||
for r in rec if int(r["ts"]) in rep]
|
||||
T = pd.DatetimeIndex(pd.to_datetime([p[0] for p in pairs], unit="ms", utc=True))
|
||||
A = np.array([p[1] for p in pairs])
|
||||
B = np.array([p[2] for p in pairs])
|
||||
dv = np.abs(A - B) > 1e-6
|
||||
now = pd.Timestamp.now(tz="UTC")
|
||||
age = (now - T).days
|
||||
d = pd.DataFrame(dict(age=age, div=dv))
|
||||
d["b"] = pd.cut(d.age, [-1, 7, 14, 21, 30, 45, 60, 999],
|
||||
labels=["0-7g", "8-14g", "15-21g", "22-30g", "31-45g", "46-60g", ">60g"])
|
||||
g = d.groupby("b", observed=True).agg(n=("div", "size"), div=("div", "sum"))
|
||||
g["tasso"] = g["div"] / g["n"]
|
||||
print(f" {len(A)} barre orarie appaiate, {int(dv.sum())} divergenti "
|
||||
f"({dv.mean() * 100:.2f}%; identiche {len(A) - int(dv.sum())}/{len(A)})")
|
||||
print(" " + " ".join(f"{ix}:{r.tasso * 100:4.1f}%" for ix, r in g.iterrows()))
|
||||
hh = pd.Series(T.hour[dv]).value_counts().sort_index()
|
||||
print(" ora UTC delle divergenti: " + ", ".join(f"{int(h):02d}:00 x{int(n)}" for h, n in hh.items())
|
||||
+ " <- il cron gira alle 00:30")
|
||||
out["prevday_rate_flat"] = (float(g["tasso"].min()), float(g["tasso"].max()))
|
||||
|
||||
print("\n 0c. LIMITE DICHIARATO — cosa NON e' verificato direttamente")
|
||||
print(" Il vintage prebuild esiste solo per BTC/ETH (lo scrive `rebuild_history`).")
|
||||
print(" `hl_*_1d.parquet` (che alimenta XSR01) e `dvol_*.parquet` NON hanno vintage su")
|
||||
print(" disco: per loro l'evidenza resta indiretta (le barre che i monitor hanno letto")
|
||||
print(" gia' chiuse coincidono al bit dopo settimane di riscritture — sezione 1).")
|
||||
print("\n -> PREMESSA CONFERMATA sul feed BTC/ETH, per via diretta e su 62 cicli.")
|
||||
print(" Rigenerare NON perde la finestra forward; nessuna data di gate si sposta.")
|
||||
return out
|
||||
|
||||
|
||||
# ============================================================================================
|
||||
# 1 — RICOSTRUZIONE DELLE TRE SERIE + PROVE D'IDENTITA'
|
||||
# ============================================================================================
|
||||
def _ident_pannello(nome: str, salvato, calcolato) -> float:
|
||||
d = float(np.max(np.abs(np.asarray(salvato, float) - np.asarray(calcolato, float))))
|
||||
print(f" I1 pannello == quello del cron di stanotte ({nome}): max|d| = {d:g}"
|
||||
f" {'OK' if d == 0.0 else '*** DIVERSO ***'}")
|
||||
return d
|
||||
|
||||
|
||||
def ric_statarb() -> dict:
|
||||
print("\n 1a. paper_statarb — gate STATARB 2026-09-27")
|
||||
ps = live("paper_statarb")
|
||||
st = json.loads((ROOT / "data" / "paper_statarb" / "state.json").read_text())
|
||||
rec = read_jsonl(ROOT / "data" / "paper_statarb" / "returns.jsonl")
|
||||
j = ps.build_joint("1d")
|
||||
ts, dt, pos, sr = ps._signal(j)
|
||||
_ident_pannello("pos_modeled", [st["pos_modeled"]], [pos[-1]])
|
||||
i0 = int(np.where(ts == st["start_ts"])[0][0])
|
||||
|
||||
def replay(frame):
|
||||
sink = Sink()
|
||||
ps._append = sink
|
||||
t2, _, p2, _ = ps._signal(frame)
|
||||
k = int(np.where(t2 == st["start_ts"])[0][0])
|
||||
s0 = dict(start_ts=st["start_ts"], last_ts=st["start_ts"], n_bars=0,
|
||||
pos_modeled=float(p2[k]), pos_real=float(p2[k]),
|
||||
cap_modeled=ps.MODELED_CAPITAL, cap_real=ps.REAL_CAPITAL,
|
||||
peak_modeled=ps.MODELED_CAPITAL, peak_real=ps.REAL_CAPITAL,
|
||||
dd_modeled=0.0, dd_real=0.0, n_trades=0)
|
||||
ps.advance(s0, frame)
|
||||
return sink.of("returns.jsonl")
|
||||
|
||||
full = replay(j)
|
||||
trunc = replay(j.iloc[:-1].reset_index(drop=True))
|
||||
a = np.array([float(r["net_modeled"]) for r in full[:-1]])
|
||||
b = np.array([float(r["net_modeled"]) for r in trunc])
|
||||
print(f" I3 troncare l'INPUT == affettare l'OUTPUT: n {len(a)}/{len(b)}, "
|
||||
f"max|d| = {float(np.max(np.abs(a - b))):g} (l'ultima barra non retroagisce)")
|
||||
return dict(mod=ps, rec=rec, rep=full, panel=j, extra=dict(pos=pos, ts=ts))
|
||||
|
||||
|
||||
def ric_xsr() -> dict:
|
||||
print("\n 1b. paper_xsr — gate XSR01 2026-10-23")
|
||||
px = live("paper_xsr")
|
||||
st = json.loads((ROOT / "data" / "paper_xsr" / "state.json").read_text())
|
||||
rec = read_jsonl(ROOT / "data" / "paper_xsr" / "returns.jsonl")
|
||||
sink = Sink()
|
||||
px._append = sink
|
||||
ts, dt, W, R, rb, syms = px.build_panel()
|
||||
_ident_pannello("w[modeled]", st["modeled"]["w"], W[-1])
|
||||
s0 = dict(start_ts=st["start_ts"], last_ts=st["start_ts"], n_bars=0, syms=st["syms"],
|
||||
modeled=px._book(px.MODELED_CAPITAL, len(st["syms"])),
|
||||
reals={n: px._book(c, len(st["syms"])) for c, n in px.REAL_BOOKS})
|
||||
px.advance(s0)
|
||||
full = sink.of("returns.jsonl")
|
||||
|
||||
# I3: troncare l'INPUT (l'ultima riga di OGNI parquet HL) e ricostruire da capo
|
||||
orig = px.load_hl
|
||||
|
||||
def load_trunc(sym):
|
||||
return orig(sym).iloc[:-1]
|
||||
|
||||
px.load_hl = load_trunc
|
||||
sink2 = Sink()
|
||||
px._append = sink2
|
||||
try:
|
||||
ts2, _, W2, _, _, syms2 = px.build_panel()
|
||||
s2 = dict(start_ts=st["start_ts"], last_ts=st["start_ts"], n_bars=0, syms=st["syms"],
|
||||
modeled=px._book(px.MODELED_CAPITAL, len(st["syms"])),
|
||||
reals={n: px._book(c, len(st["syms"])) for c, n in px.REAL_BOOKS})
|
||||
px.advance(s2)
|
||||
tr = sink2.of("returns.jsonl")
|
||||
a = np.array([float(r["net_modeled"]) for r in full[:-1]])
|
||||
b = np.array([float(r["net_modeled"]) for r in tr])
|
||||
dmax = float(np.max(np.abs(a - b))) if len(a) == len(b) else float("nan")
|
||||
print(f" I3 troncare l'INPUT == affettare l'OUTPUT: n {len(a)}/{len(b)}, max|d| = {dmax:g}")
|
||||
finally:
|
||||
px.load_hl = orig
|
||||
return dict(mod=px, rec=rec, rep=full)
|
||||
|
||||
|
||||
def ric_dvolspread() -> dict:
|
||||
print("\n 1c. paper_dvolspread — kill 2026-10-24 / decisione 2027-01-24")
|
||||
pv = live("paper_dvolspread")
|
||||
st = json.loads((ROOT / "data" / "paper_dvolspread" / "state.json").read_text())
|
||||
rec = read_jsonl(ROOT / "data" / "paper_dvolspread" / "returns.jsonl")
|
||||
P = pv._panel()
|
||||
_ident_pannello("wb_modeled", [st["wb_modeled"]], [P["wb"][-1]])
|
||||
i0 = int(np.where(P["ts"] == st["start_ts"])[0][0])
|
||||
|
||||
def replay(Q):
|
||||
sink = Sink()
|
||||
pv._append = sink
|
||||
k = int(np.where(Q["ts"] == st["start_ts"])[0][0])
|
||||
s0 = dict(start_ts=st["start_ts"], last_ts=st["start_ts"], n_bars=0, n_active=0,
|
||||
n_flat_signal=0, n_flat_nodata=0,
|
||||
wb_modeled=float(Q["wb"][k]), wb_real=float(Q["wb"][k]),
|
||||
cap_modeled=pv.MODELED_CAPITAL, cap_real=pv.REAL_CAPITAL,
|
||||
peak_modeled=pv.MODELED_CAPITAL, peak_real=pv.REAL_CAPITAL,
|
||||
dd_modeled=0.0, dd_real=0.0, n_flips=0, frozen=pv.FROZEN)
|
||||
out = pv.advance(s0, Q)
|
||||
return sink.of("returns.jsonl"), out
|
||||
|
||||
full, st_full = replay(P)
|
||||
Q = {k: (v[:-1] if hasattr(v, "__len__") else v) for k, v in P.items()}
|
||||
trunc, st_tr = replay(Q)
|
||||
a = np.array([float(r["net_modeled"]) for r in full[:-1]])
|
||||
b = np.array([float(r["net_modeled"]) for r in trunc])
|
||||
print(f" I3 troncare l'INPUT == affettare l'OUTPUT: n {len(a)}/{len(b)}, "
|
||||
f"max|d| = {float(np.max(np.abs(a - b))):g}")
|
||||
return dict(mod=pv, rec=rec, rep=full, st_rep=st_tr, st_rec=st)
|
||||
|
||||
|
||||
def sezione_1() -> dict:
|
||||
print("\n" + H)
|
||||
print(" 1 — RICOSTRUZIONE + PROVE D'IDENTITA' (senza queste, il numero e' un'altra strategia)")
|
||||
print(H)
|
||||
print("""
|
||||
Tre prove, ognuna chiude un modo diverso di sbagliare:
|
||||
I1 il pannello che leggo ORA e' quello che il cron ha letto stanotte (i parquet hanno mtime
|
||||
00:30-00:35 e nulla li riscrive fino al prossimo giro): si verifica contro lo `state.json`
|
||||
del monitor, che e' stato scritto DA LUI alle 00:35.
|
||||
I2 sulle barre che il monitor ha letto GIA' CHIUSE la ricostruzione coincide AL BIT: e' la
|
||||
parte non affetta dal difetto, ed e' l'unico posto dove i due numeri devono coincidere.
|
||||
I3 troncare l'ULTIMA barra dell'INPUT da' lo stesso risultato che affettare l'output: nessuna
|
||||
barra precedente dipende dall'ultima (se dipendesse, la 'riparazione' sarebbe un altro
|
||||
segnale, non lo stesso).""")
|
||||
S = dict(statarb=ric_statarb(), xsr=ric_xsr(), dvol=ric_dvolspread())
|
||||
|
||||
print("\n I2 — barre che il monitor ha letto CHIUSE, e coincidenza al bit:")
|
||||
for k, lab in (("statarb", "paper_statarb"), ("xsr", "paper_xsr"), ("dvol", "paper_dvolspread")):
|
||||
rec, rep = S[k]["rec"], S[k]["rep"]
|
||||
d = {int(r["ts"]): float(r["net_modeled"]) for r in rep}
|
||||
pr = [(int(r["ts"]), float(r["net_modeled"]), d[int(r["ts"])]) for r in rec if int(r["ts"]) in d]
|
||||
A = np.array([p[1] for p in pr])
|
||||
B = np.array([p[2] for p in pr])
|
||||
T = pd.DatetimeIndex(pd.to_datetime([p[0] for p in pr], unit="ms", utc=True))
|
||||
eq = np.abs(A - B) <= 1e-6
|
||||
S[k]["pairs"] = (T, A, B)
|
||||
gg = sorted({str(x.date()) for x in T[eq]})
|
||||
note = f" ({gg[0]} -> {gg[-1]})" if gg else ""
|
||||
print(f" {lab:<18} {int(eq.sum())}/{len(A)} identiche{note}")
|
||||
if k == "statarb":
|
||||
print(" quelle 6 sono le barre recuperate dopo l'incidente del feed 09-15/07:")
|
||||
print(" registrate il 16/07 e riprodotte oggi al bit dopo ~37 riscritture")
|
||||
print(" notturne -> conferma indipendente della premessa della sezione 0.")
|
||||
if k == "dvol":
|
||||
print(" *** ZERO barre non affette: per questo monitor la prova I2 NON ESISTE.")
|
||||
print(" Restano I1 (pannello identico, max|d|=0), I3 (troncamento innocuo) e il")
|
||||
print(" fatto strutturale che il replay chiama `advance()` di produzione senza")
|
||||
print(" una riga riscritta. Va detto: e' una prova piu' debole delle altre due.")
|
||||
return S
|
||||
|
||||
|
||||
# ============================================================================================
|
||||
# 2 — I TRE GATE, ALLA LETTERA
|
||||
# ============================================================================================
|
||||
def _riga(lab, m, extra=""):
|
||||
print(f" {lab:<22} n={m['n']:>3} Sharpe {m['sh']:+7.2f} maxDD {m['dd'] * 100:6.2f}% "
|
||||
f"ritorno {m['tot'] * 100:+6.2f}% vol {m['sd'] * 100:5.2f}%{extra}")
|
||||
|
||||
|
||||
def gate_dvolspread(S: dict) -> tuple[str, str]:
|
||||
print("\n" + H)
|
||||
print(" 2a — DVOLSPREAD (kill 2026-10-24, decisione 2027-01-24) — PER PRIMO: e' l'unico dei")
|
||||
print(" tre che ha gia' un VETO D'INTEGRITA', e la domanda e' se il difetto lo fa scattare")
|
||||
print(H)
|
||||
pv, rec, rep = S["dvol"]["mod"], S["dvol"]["rec"], S["dvol"]["rep"]
|
||||
A = np.array([float(r["net_modeled"]) for r in rec])
|
||||
B = np.array([float(r["net_modeled"]) for r in rep])[:-1] # RIPARATA = solo barre chiuse
|
||||
mA, mB = mets(A), mets(B)
|
||||
print(f"\n criterio kill (pre-registrato 26/07): Sharpe forward MODELED < {pv.KILL_SHARPE:+.2f}"
|
||||
f" -> RITIRO")
|
||||
_riga("serie ATTUALE", mA)
|
||||
_riga("serie RIPARATA", mB)
|
||||
vA = "KILL" if mA["sh"] < pv.KILL_SHARPE else "vivo"
|
||||
vB = "KILL" if mB["sh"] < pv.KILL_SHARPE else "vivo"
|
||||
print(f" -> criterio kill: serie attuale {vA} · serie riparata {vB} "
|
||||
f"{'STESSO VERDETTO' if vA == vB else '*** VERDETTO RIBALTATO ***'}")
|
||||
print(f" ⚠ ma a n={mA['n']} barre SE(Sharpe) ~ {se_sharpe(mA['n']):.2f}: nessuna delle due")
|
||||
print(" e' una lettura di gate, e la data del kill e' fra "
|
||||
f"{(date(2026, 10, 24) - TODAY).days} giorni.")
|
||||
|
||||
print("\n IL VETO D'INTEGRITA' (`n_active + n_flat_signal >= 80% delle barre`):")
|
||||
stA, stB = S["dvol"]["st_rec"], S["dvol"]["st_rep"]
|
||||
fA = (stA["n_active"] + stA["n_flat_signal"]) / max(stA["n_bars"], 1)
|
||||
fB = (stB["n_active"] + stB["n_flat_signal"]) / max(stB["n_bars"], 1)
|
||||
print(f" serie ATTUALE : attive {stA['n_active']} · flat-da-segnale {stA['n_flat_signal']}"
|
||||
f" · flat-SENZA-DATO {stA['n_flat_nodata']} -> valide {fA:.0%} "
|
||||
f"(soglia {pv.MIN_ACTIVE_FRAC:.0%}) -> {'VETO' if fA < pv.MIN_ACTIVE_FRAC else 'PASSA'}")
|
||||
print(f" serie RIPARATA: attive {stB['n_active']} · flat-da-segnale {stB['n_flat_signal']}"
|
||||
f" · flat-SENZA-DATO {stB['n_flat_nodata']} -> valide {fB:.0%} -> "
|
||||
f"{'VETO' if fB < pv.MIN_ACTIVE_FRAC else 'PASSA'}")
|
||||
print("""
|
||||
📌 IL RISULTATO E' SUL VETO, NON SUL MONITOR. Una serie che misura DUE MINUTI di mercato al
|
||||
giorno passa al 100% un veto d'integrita' progettato apposta per non far decidere su dati
|
||||
mancanti. Non e' un difetto di taratura: e' che il veto e' costruito sulla domanda giusta
|
||||
per un altro guasto. Conta *se il DVOL c'era* (e c'era, 28/28) e non puo' vedere *quanto
|
||||
dura la barra su cui c'era* — sono due assi ortogonali. La stessa soglia 0.80 applicata alla
|
||||
quota di barre CHIUSE leggerebbe 0/28 = 0% e vieterebbe la decisione.
|
||||
E' la stessa forma gia' pagata dal progetto tre volte: 'una riga presente non e' un dato
|
||||
presente'. Qui la variante nuova e': *una barra presente non e' una GIORNATA presente*.""")
|
||||
|
||||
print("\n criteri (b) marginale ADDS / (c) deflated-Sharpe / (d) weights_tilt_null: NON GIRATI,")
|
||||
print(" e il motivo e' una misura, non pigrizia: si valutano sul campione ESTESO (in-sample")
|
||||
print(f" attivo 1949 barre + forward). Oggi il forward pesa {mA['n']}/{1949 + mA['n']} = "
|
||||
f"{100 * mA['n'] / (1949 + mA['n']):.1f}% del campione; alla decisione del 24/01/2027 ne")
|
||||
print(" pesera' ~8%. -> il difetto NON puo' ribaltare (c): puo' ribaltare solo (a), che e'")
|
||||
print(" calcolato sulla sola finestra forward ed e' interamente determinato da lui.")
|
||||
return vA, vB
|
||||
|
||||
|
||||
def gate_statarb(S: dict) -> tuple[str, str]:
|
||||
print("\n" + H)
|
||||
print(" 2b — STATARB (decisione 2026-09-27) — il gate su cui il difetto MORDE")
|
||||
print(H)
|
||||
rec, rep = S["statarb"]["rec"], S["statarb"]["rep"]
|
||||
gate = importlib.import_module("r0724_statarb_deploy_gate")
|
||||
A = np.array([float(r["net_modeled"]) for r in rec])
|
||||
Ar = np.array([float(r["net_real"]) for r in rec])
|
||||
B = np.array([float(r["net_modeled"]) for r in rep])[:-1]
|
||||
Br = np.array([float(r["net_real"]) for r in rep])[:-1]
|
||||
mA, mB = mets(A), mets(B)
|
||||
hc = lambda m, r: abs(float((np.prod(1 + m) - np.prod(1 + r)) * 100))
|
||||
hA, hB = hc(A, Ar), hc(B, Br)
|
||||
print(f"\n soglie pre-registrate 24/07: Sharpe >= {gate.SH_DEPLOY} · maxDD < {gate.DD_MAX:.0%}"
|
||||
f" · fill-haircut < {gate.HAIRCUT_MAX_PP} pp")
|
||||
_riga("serie ATTUALE", mA, f" haircut {hA:.3f} pp")
|
||||
_riga("serie RIPARATA", mB, f" haircut {hB:.3f} pp")
|
||||
|
||||
def verdetto(sh, dd, h):
|
||||
if sh >= gate.SH_DEPLOY and dd < gate.DD_MAX and h < gate.HAIRCUT_MAX_PP:
|
||||
return "CANDIDATO AL DEPLOY"
|
||||
if sh >= 0:
|
||||
return "ESTENSIONE (unica, al 2026-12-26)"
|
||||
return "RITIRO"
|
||||
vA, vB = verdetto(mA["sh"], mA["dd"], hA), verdetto(mB["sh"], mB["dd"], hB)
|
||||
for lab, sh, dd, h, v in (("ATTUALE", mA["sh"], mA["dd"], hA, vA),
|
||||
("RIPARATA", mB["sh"], mB["dd"], hB, vB)):
|
||||
print(f" {lab:<9} Sharpe>={gate.SH_DEPLOY}: {'SI' if sh >= gate.SH_DEPLOY else 'NO':<3}"
|
||||
f" | maxDD<{gate.DD_MAX:.0%}: {'SI' if dd < gate.DD_MAX else 'NO':<3}"
|
||||
f" | haircut<{gate.HAIRCUT_MAX_PP}pp: {'SI' if h < gate.HAIRCUT_MAX_PP else 'NO':<3}"
|
||||
f" -> {v}")
|
||||
print(f"\n -> DUE CRITERI SU TRE SI RIBALTANO. Sharpe {mA['sh']:+.2f} -> {mB['sh']:+.2f} "
|
||||
f"(attraversa 0 e la soglia 0.5); maxDD {mA['dd']:.1%} -> {mB['dd']:.1%} (attraversa la")
|
||||
print(" guardia del 10%). Il terzo (haircut) non si ribalta e non poteva: e' un")
|
||||
print(" rapporto fra due libri che subiscono lo STESSO troncamento.")
|
||||
|
||||
print("\n DIAGNOSTICA PRE-REGISTRATA IL 25/07 (statica sempre-short a pari vol-target):")
|
||||
bA = gate._bench_static_short(rec)
|
||||
bB = gate._bench_static_short(rep[:-1])
|
||||
if bA and bB:
|
||||
print(f" benchmark sulla finestra ATTUALE : {bA[0]:+.2f} su {bA[1]} barre")
|
||||
print(f" benchmark sulla finestra RIPARATA: {bB[0]:+.2f} su {bB[1]} barre")
|
||||
print(f" STATARB - benchmark: attuale {mA['sh'] - bA[0]:+.2f} "
|
||||
f"riparata {mB['sh'] - bB[0]:+.2f}")
|
||||
print("""
|
||||
📌 ASIMMETRIA CHE VALE PIU' DEI NUMERI: il benchmark NON e' affetto dal difetto — e'
|
||||
ricalcolato dal pannello, non letto dalla serie registrata (varia di %+.2f fra le due
|
||||
finestre, contro %+.2f della strategia). Alla data del gate l'operatore metterebbe quindi
|
||||
a confronto un benchmark GIUSTO con un numero di strategia SBAGLIATO, e la diagnostica
|
||||
sembrerebbe piu' favorevole di quanto sia: +%.2f invece di +%.2f di margine.
|
||||
Il segno del margine sopravvive comunque (STATARB batte la statica in entrambe le lenti):
|
||||
cio' che il difetto ribalta e' il gate, non questa diagnostica.""" %
|
||||
(bB[0] - bA[0], mB["sh"] - mA["sh"], mA["sh"] - bA[0], mB["sh"] - bB[0]))
|
||||
else:
|
||||
print(" benchmark NON calcolabile -> non girato.")
|
||||
return vA, vB
|
||||
|
||||
|
||||
def gate_xsr(S: dict) -> tuple[str, str]:
|
||||
print("\n" + H)
|
||||
print(" 2c — XSR01 (decisione 2026-10-23)")
|
||||
print(H)
|
||||
rec, rep = S["xsr"]["rec"], S["xsr"]["rep"]
|
||||
gate = importlib.import_module("r0725_xsr_deploy_gate")
|
||||
k5 = "net_REAL-$5000"
|
||||
A = np.array([float(r["net_modeled"]) for r in rec])
|
||||
A5 = np.array([float(r[k5]) for r in rec])
|
||||
B = np.array([float(r["net_modeled"]) for r in rep])[:-1]
|
||||
B5 = np.array([float(r[k5]) for r in rep])[:-1]
|
||||
mA, mB = mets(A), mets(B)
|
||||
|
||||
def haircut(m, r5):
|
||||
tm = float(np.prod(1 + m) - 1)
|
||||
t5 = float(np.prod(1 + r5) - 1)
|
||||
return (tm - t5) / abs(tm) if tm != 0 else float("nan")
|
||||
hA, hB = haircut(A, A5), haircut(B, B5)
|
||||
print(f"\n soglie pre-registrate 25/07: Sharpe >= {gate.SH_DEPLOY} E haircut$5000 <= "
|
||||
f"{gate.HAIRCUT_MAX:.0%} · ritiro sotto Sharpe {gate.SH_RETIRE}")
|
||||
print(f" ⚠ se l'haircut sfonda -> RITIRO a prescindere dallo Sharpe (regola esplicita)")
|
||||
_riga("serie ATTUALE", mA, f" haircut$5000 {hA * 100:+.1f}%")
|
||||
_riga("serie RIPARATA", mB, f" haircut$5000 {hB * 100:+.1f}%")
|
||||
|
||||
def verdetto(sh, h):
|
||||
if np.isfinite(h) and h > gate.HAIRCUT_MAX:
|
||||
return "RITIRO (haircut)"
|
||||
if sh >= gate.SH_DEPLOY:
|
||||
return "CANDIDATO SLEEVE"
|
||||
if sh >= gate.SH_RETIRE:
|
||||
return "ESTENSIONE (unica, al 2027-01-21)"
|
||||
return "RITIRO"
|
||||
vA, vB = verdetto(mA["sh"], hA), verdetto(mB["sh"], hB)
|
||||
print(f" ATTUALE -> {vA}\n RIPARATA -> {vB} "
|
||||
f"{'STESSO VERDETTO' if vA == vB else '*** VERDETTO RIBALTATO ***'}")
|
||||
print(f"\n -> il verdetto NON si ribalta, ma per un caso: entrambe stanno sotto "
|
||||
f"{gate.SH_RETIRE}. Il")
|
||||
print(f" numero pero' cambia di {abs(mA['sh'] / mB['sh']):.1f}x ({mA['sh']:+.2f} contro "
|
||||
f"{mB['sh']:+.2f}), e con SE ~ {se_sharpe(mA['n']):.1f} nessuno dei due decide niente.")
|
||||
print(" L'haircut e' l'unico criterio robusto al difetto (rapporto fra libri troncati")
|
||||
print(" allo stesso modo) ed e' lontanissimo dalla guardia in entrambe le lenti.")
|
||||
return vA, vB
|
||||
|
||||
|
||||
# ============================================================================================
|
||||
# 3 — IL MECCANISMO: perche' il troncamento non e' rumore
|
||||
# ============================================================================================
|
||||
def sezione_3(S: dict) -> None:
|
||||
print("\n" + H)
|
||||
print(" 3 — PERCHE' IL DIFETTO NON E' RUMORE: comprime la VOL piu' della MEDIA")
|
||||
print(H)
|
||||
print("""
|
||||
Un troncamento a k minuti su 1440 scala la media del rendimento circa come k e la deviazione
|
||||
standard come sqrt(k) (random walk). Quindi il rapporto media/sd — cioe' lo Sharpe — viene
|
||||
MOLTIPLICATO per ~sqrt(k/1440)... in valore assoluto no: il numeratore e' la media del pezzo
|
||||
troncato, che non ha lo stesso SEGNO della media della giornata intera. Risultato: la serie
|
||||
registrata produce letture di Sharpe piu' ESTREME e in una direzione che non e' quella vera.
|
||||
Si vede in tabella: la vol registrata e' una frazione di quella vera, ma il |Sharpe| non e' piu'
|
||||
piccolo — in due casi su tre e' molto piu' GRANDE.""")
|
||||
print(f"\n {'monitor':<20} {'vol REG':>9} {'vol RIP':>9} {'min/giorno':>11} "
|
||||
f"{'Sh REG':>8} {'Sh RIP':>8} nota")
|
||||
for k, lab in (("statarb", "paper_statarb"), ("xsr", "paper_xsr"), ("dvol", "paper_dvolspread")):
|
||||
T, A, B = S[k]["pairs"]
|
||||
Bc = B[:-1] if len(B) == len(A) else B
|
||||
# ⚠ l'INTERA finestra dell'incidente del feed (09-16/07) va esclusa dalla stima dei
|
||||
# minuti/giorno, non le sole barre risultate identiche: durante il blocco il monitor non
|
||||
# trovava barre nuove e al ripristino ne ha recuperate 7 in un colpo, alcune solo
|
||||
# PARZIALMENTE riallineate. Escludendo le sole identiche la stima esce 4x piu' alta
|
||||
# (16,7 invece di 3,9 min/giorno su statarb) — e' la stessa maschera del 22/08.
|
||||
keep = ~((T >= FREEZE[0]) & (T <= FREEZE[1]))
|
||||
mA, mB = mets(A), mets(Bc)
|
||||
kA, kB = mets(A[keep]), mets(B[:len(A)][keep])
|
||||
minuti = (kA["sd"] / kB["sd"]) ** 2 * 1440.0
|
||||
nota = ("SEGNO OPPOSTO" if np.sign(mA["sh"]) != np.sign(mB["sh"])
|
||||
else f"|Sh| x{abs(mA['sh']) / abs(mB['sh']):.1f}")
|
||||
print(f" {lab:<20} {mA['sd'] * 100:8.2f}% {mB['sd'] * 100:8.2f}% {minuti:10.1f} "
|
||||
f"{mA['sh']:+8.2f} {mB['sh']:+8.2f} {nota}"
|
||||
+ (f" ({int((~keep).sum())} barre dell'incidente escluse)" if (~keep).any() else ""))
|
||||
print("""
|
||||
Il caso peggiore NON e' quello col |Sharpe| piu' gonfiato: e' `paper_statarb`, dove il
|
||||
|Sharpe| e' quasi identico (1,96 contro 2,02) e cambia il SEGNO. Un controllo che cercasse
|
||||
"numeri troppo belli" lo lascerebbe passare senza una parola: e' proprio il monitor che
|
||||
alimenta il gate piu' vicino.
|
||||
-> la firma da cercare non e' un |Sharpe| alto ma una VOL FORWARD molto sotto quella del
|
||||
backtest: 7,9% contro ~31%, 0,5% contro ~2,8%, 0,8% contro ~19%.
|
||||
⚠ i "min/giorno" sono una LETTURA del rapporto di varianza (assume random walk
|
||||
sull'intervallo troncato), non un cronometro, e sono sensibili alla maschera: su statarb
|
||||
3,9 escludendo l'incidente del feed, 16,7 escludendo le sole barre identiche, 92
|
||||
includendo tutto. Si cita la prima, che e' quella del 22/08; l'ordine di grandezza e'
|
||||
quello, la cifra no.
|
||||
✅ Replica indipendente del filone MONITOR-AUDIT: 3,9 / 41,2 / 2,5 contro i 4 / 41 / 2
|
||||
pubblicati stamattina, per un percorso scritto separatamente.""")
|
||||
|
||||
print("\n `implausible_sharpe` puntato sulle serie forward (mai fatto — nota del 22/08).")
|
||||
print(" ⚠ il gate chiede n>=30: due serie forward su tre NON sono giudicabili, e questo")
|
||||
print(" e' esso stesso un risultato — il gate esiste dal 26/07 ma non puo' sorvegliare")
|
||||
print(" una finestra forward giovane, che e' esattamente quando serve.")
|
||||
import altlib as al
|
||||
for k, lab in (("statarb", "paper_statarb"), ("xsr", "paper_xsr"), ("dvol", "paper_dvolspread")):
|
||||
T, A, B = S[k]["pairs"]
|
||||
for nome, v in (("REGISTRATA", A), ("RIPARATA", B[:-1] if len(B) == len(A) else B)):
|
||||
idx = T[:len(v)]
|
||||
try:
|
||||
r = al.implausible_sharpe(pd.Series(v, index=idx))
|
||||
corto = any("troppo corto" in x for x in r["reasons"])
|
||||
flag = ("SEGNALA" if r["implausible"]
|
||||
else "n/giudic." if corto else "ok")
|
||||
print(f" {lab:<18} {nome:<11} {flag:<10} {'; '.join(r['reasons'])[:66]}")
|
||||
except Exception as e: # noqa: BLE001
|
||||
print(f" {lab:<18} {nome:<11} non girato ({type(e).__name__})")
|
||||
|
||||
|
||||
# ============================================================================================
|
||||
# 4 — QUANTO COSTA ASPETTARE
|
||||
# ============================================================================================
|
||||
GATES = (
|
||||
("STATARB", "statarb", date(2026, 9, 27)),
|
||||
("XSR01", "xsr", date(2026, 10, 23)),
|
||||
("DVOLSPREAD", "dvol", date(2026, 10, 24)),
|
||||
)
|
||||
|
||||
|
||||
def sezione_4(S: dict) -> None:
|
||||
print("\n" + H)
|
||||
print(" 4 — QUANTO COSTA ASPETTARE (l'unico argomento quantitativo su QUANDO riparare)")
|
||||
print(H)
|
||||
print("""
|
||||
Due scenari, e la differenza fra loro e' tutto il contenuto della sezione:
|
||||
RIPARA+RIGENERA : si corregge `advance()` e si RICALCOLA la finestra dall'inception.
|
||||
Possibile perche' le barre chiuse non vengono riscritte (sezione 0).
|
||||
RIPARA IN AVANTI : si corregge `advance()` e basta. Le barre gia' scritte restano sbagliate.""")
|
||||
print(f"\n {'gate':<12} {'data':<12} {'barre al gate':>14} {'oggi':>6} "
|
||||
f"{'corrette se si ripara OGGI':>28} {'se si ripara alla vigilia':>27}")
|
||||
for nome, key, gd in GATES:
|
||||
rec = S[key]["rec"]
|
||||
first = pd.Timestamp(rec[0]["dt"][:10]).date()
|
||||
n_gate = (gd - first).days + 1
|
||||
n_oggi = len(rec)
|
||||
print(f" {nome:<12} {str(gd):<12} {n_gate:>14} {n_oggi:>6} "
|
||||
f"{f'{n_gate - n_oggi} ({(n_gate - n_oggi) / n_gate:.0%})':>28} "
|
||||
f"{f'1 ({1 / n_gate:.0%})':>27}")
|
||||
print("\n Con RIGENERAZIONE la colonna giusta e' un'altra: 100% a QUALUNQUE data di")
|
||||
print(" riparazione. Il costo dell'attesa e' ZERO barre — cio' che si paga aspettando non")
|
||||
print(" e' dato perso ma il RISCHIO che la riparazione non arrivi prima del gate.")
|
||||
print(" Senza rigenerazione, invece, riparare oggi lascia comunque sbagliata la maggioranza")
|
||||
print(" della finestra di STATARB, e riparare alla vigilia non serve a niente.")
|
||||
|
||||
print("\n SENSIBILITA' — quanto deve essere contaminata la finestra perche' il verdetto")
|
||||
print(" cambi. Misurata sui dati di OGGI, non estrapolata: si costruisce la serie mista")
|
||||
print(" `registrate[:k] + riparate[k:]` (= riparazione in avanti dal giorno k) e si legge")
|
||||
print(" il criterio. k=0 e' la rigenerazione completa, k=n e' la serie di oggi.")
|
||||
gate = importlib.import_module("r0724_statarb_deploy_gate")
|
||||
rec, rep = S["statarb"]["rec"], S["statarb"]["rep"]
|
||||
A = np.array([float(r["net_modeled"]) for r in rec])[:-1]
|
||||
B = np.array([float(r["net_modeled"]) for r in rep])[:-1]
|
||||
n = len(A)
|
||||
print(f"\n STATARB — {n} barre chiuse; soglia deploy {gate.SH_DEPLOY}, ritiro sotto 0, "
|
||||
f"guardia maxDD {gate.DD_MAX:.0%}")
|
||||
print(f" {'corrette (coda)':>16} {'contaminate':>12} {'Sharpe':>8} {'maxDD':>8} verdetto")
|
||||
verd = []
|
||||
for k in range(0, n + 1):
|
||||
m = mets(np.concatenate([A[:k], B[k:]]))
|
||||
v = ("CANDIDATO" if (m["sh"] >= gate.SH_DEPLOY and m["dd"] < gate.DD_MAX)
|
||||
else "ESTENSIONE" if m["sh"] >= 0 else "RITIRO")
|
||||
verd.append((k, m, v))
|
||||
# minimo numero di barre CORRETTE in coda che basta a leggere gia' il verdetto riparato
|
||||
v_rip = verd[0][2]
|
||||
min_ok = min((n - k for k, m, v in verd if v == v_rip), default=n)
|
||||
for k, m, v in verd:
|
||||
if k in (0, n // 4, n // 2, (3 * n) // 4, n - min_ok, n - min_ok + 1, n):
|
||||
print(f" {n - k:>16} {f'{k} ({k / n:.0%})':>12} {m['sh']:+8.2f} "
|
||||
f"{m['dd'] * 100:7.2f}% {v}")
|
||||
n_res = 90 - len(S["statarb"]["rec"]) # barre che restano da qui al gate del 27/09
|
||||
print(f"""
|
||||
📌 LA RIPARAZIONE PARZIALE NON E' PROPORZIONALE. Su questo campione bastano
|
||||
**{min_ok} barre corrette in coda su {n}** ({min_ok / n:.0%}) perche' il criterio legga gia' il
|
||||
verdetto della serie riparata. Il meccanismo e' quello della sezione 3: una barra troncata a
|
||||
~4 minuti su 1440 porta ~1/370 della varianza E ~1/370 del rendimento di una barra vera
|
||||
(entrambi scalano con la frazione di giornata), quindi pesa quasi nulla sia nella media sia
|
||||
nella deviazione della miscela. La maggioranza NUMERICA delle barre non comanda.
|
||||
Detto meglio, ed e' la formulazione onesta: riparare in avanti senza rigenerare non
|
||||
CONTAMINA la finestra, la ACCORCIA — il criterio finisce per leggere la strategia sulle sole
|
||||
barre corrette. Riparando oggi, al 27/09 STATARB avrebbe {n_res} barre vere su 90: una
|
||||
finestra piu' corta ma VERA, con SE(Sharpe) ~{se_sharpe(n_res):.1f} invece di ~{se_sharpe(90):.1f}.
|
||||
Riparare alla vigilia (1 barra) no: li' il numero resta quello di oggi.
|
||||
⚠ Il {min_ok}/{n} NON e' una costante — dipende da quali barre capitano in coda, e cambia col
|
||||
campione: si legge come "poche", non come una soglia. E rigenerare resta strettamente
|
||||
migliore, per due cose che l'accorciamento non da': un maxDD leggibile (la miscela ha un
|
||||
percorso di equity che non e' mai esistito) e una finestra rileggibile con altri criteri.""")
|
||||
|
||||
|
||||
# ============================================================================================
|
||||
def main() -> None:
|
||||
print(H)
|
||||
print(" GATE-RECON — cosa direbbero i tre gate pre-registrati con le serie riparate")
|
||||
print(f" {TODAY} SOLA LETTURA su produzione (nessun file di data/ aperto in scrittura)")
|
||||
print(H)
|
||||
sezione_0()
|
||||
S = sezione_1()
|
||||
v_dv = gate_dvolspread(S)
|
||||
v_st = gate_statarb(S)
|
||||
v_xs = gate_xsr(S)
|
||||
sezione_3(S)
|
||||
sezione_4(S)
|
||||
|
||||
print("\n" + H)
|
||||
print(" RIEPILOGO — il DELTA, non la decisione (i gate hanno una data e non si anticipano)")
|
||||
print(H)
|
||||
print(f" STATARB 27/09 — serie riparata: {v_st[1]:<36} · serie attuale: {v_st[0]}")
|
||||
print(f" XSR01 23/10 — serie riparata: {v_xs[1]:<36} · serie attuale: {v_xs[0]}")
|
||||
print(f" DVOLSPREAD 24/10 — serie riparata: {v_dv[1]:<36} · serie attuale: {v_dv[0]}")
|
||||
print("\n Nessuno di questi e' il verdetto del gate: sono i criteri letti OGGI, su una finestra")
|
||||
print(" ancora incompleta. Servono a una sola decisione, che e' dell'operatore: riparare")
|
||||
print(" `advance()` e RIGENERARE prima del 27/09, oppure sapere che quel giorno il criterio")
|
||||
print(" verra' letto su una serie che misura 4 minuti di mercato al giorno.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user