monitor: advance() consuma solo barre CHIUSE — serie rigenerate, guardia PREMATURO, 4 debiti chiusi
S5.1 RIPARATO E RIGENERATO. Filtro condiviso src/live/paper_guard.py (barra open-labeled chiusa = ts + cadenza <= adesso) importato da tutti e 6 i monitor; serie rigenerate dallo stesso start_ts con scripts/live/paper_regen.py (evidenza in *.pre_regen_20260826.*): statarb +1,95 -> -1,61 (il ribaltamento del gate 27/09 previsto dall'audit), dvolspread -14,73 -> -4,41, xsr -4,98 -> -2,72, prevday invariato. Nessuna data di gate si sposta. Guardia cablata in monitor_health: stato PREMATURO (ultima barra che chiude dopo l'mtime, grazia 5 min, open_labeled=False per collect_chain) — sul dato vivo segnala i 5 rotti e tace sui 2 sani; dopo la rigenerazione 7/7 OK. paper_portfolio non rigenerato (GTAA su ADJUSTED_LAST: replay != serie registrata, P12), tolta la coda non chiusa. D6 pagata di nuovo nel fix: asi8 in pandas 3 e' in us, non ns — blindata con test su tre risoluzioni. S5.12 ESTESO: conftest devia anche trades.db (wrapper su connect: il default e' catturato alla definizione) e docs/journal/; book_executions.jsonl sorvegliato con impronta inizio/fine suite. S5.5 FATTO: test_leva_massima cancellato con nota (misurava frac*n_asset: con una chiave di scala avrebbe continuato a passare smettendo di controllare). S5.9 INDAGATO E RIPARATO (r0826_skh_band_drift): il dato regge (taglio 02/07 riproduce l'audit 1,6376, in-sample identico su ogni taglio); la deriva era la finestra hold-out — e la sola settimana 15-22/08 vale +0,35 di Sharpe hold-out. Il test ora taglia il feed al 02/07 e verifica la riproduzione stretta. Suite: 751 passati, 0 falliti. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B9UyJLHzR7EJzxR3iQ3RN1
This commit is contained in:
@@ -18,6 +18,7 @@ sys.path.insert(0, str(PROJECT_ROOT))
|
||||
import numpy as np, pandas as pd
|
||||
from src.portfolio.sleeves import _tp01_returns, _tp01_positions
|
||||
from src.portfolio.gtaa import gtaa_returns, gtaa_weights
|
||||
from src.live import paper_guard as PG
|
||||
|
||||
STATE_DIR = PROJECT_ROOT / "data" / "paper_combo"
|
||||
STATE = STATE_DIR / "state.json"
|
||||
@@ -77,6 +78,10 @@ def advance():
|
||||
return st
|
||||
last = pd.Timestamp(st["last"])
|
||||
nn = naked[naked.index > last]; gg = guard[guard.index > last]
|
||||
# SOLO giorni CHIUSI. L'audit 22/08 ha trovato questo monitor sano PER CASO (aspetta la
|
||||
# chiusura di una borsa, non per progetto): il filtro lo rende sano PER COSTRUZIONE.
|
||||
m = PG.indice_chiuso(nn.index, PG.MS_1D)
|
||||
nn, gg = nn[m], gg[PG.indice_chiuso(gg.index, PG.MS_1D)]
|
||||
if len(nn):
|
||||
e = st["equity"]; pk = st["peak"]; dd = st["max_dd"]
|
||||
eg = st["equity_g"]; pkg = st["peak_g"]; ddg = st["max_dd_g"]; lines = []
|
||||
|
||||
@@ -85,6 +85,7 @@ sys.path.insert(0, str(PROJECT_ROOT / "scripts" / "research" / "alt"))
|
||||
sys.path.insert(0, str(PROJECT_ROOT / "scripts" / "research" / "ortho"))
|
||||
|
||||
import altlib as al # noqa: E402
|
||||
from src.live import paper_guard as PG # noqa: E402
|
||||
import ortholib as ol # noqa: E402
|
||||
from r0726_dvolspread_gate import make_book # noqa: E402 (fabbrica ESATTA del T2)
|
||||
|
||||
@@ -152,7 +153,9 @@ def init_state(P: dict) -> dict:
|
||||
|
||||
def advance(st: dict, P: dict) -> dict:
|
||||
ts, dt = P["ts"], P["dt"]
|
||||
new = [i for i in range(len(ts)) if ts[i] > st["last_ts"]]
|
||||
# SOLO barre CHIUSE: il pannello arriva da `resample_tf` col giorno in corso dentro —
|
||||
# su uno spread BTC-ETH la barra troncata e' quasi solo rumore (~2 min/g, r0822_monitor_audit)
|
||||
new = PG.nuove_chiuse(ts, st["last_ts"], PG.MS_1D)
|
||||
if not new:
|
||||
return st
|
||||
hm, hr = st["wb_modeled"], st["wb_real"] # peso BTC TENUTO (w_eth = -w_btc)
|
||||
|
||||
@@ -17,6 +17,7 @@ sys.path.insert(0, str(PROJECT_ROOT))
|
||||
import numpy as np, pandas as pd
|
||||
from src.portfolio.portfolio import StrategyPortfolio
|
||||
from src.portfolio.sleeves import active_sleeves
|
||||
from src.live import paper_guard as PG
|
||||
|
||||
STATE_DIR = PROJECT_ROOT / "data" / "paper_portfolio"
|
||||
STATE = STATE_DIR / "state.json"
|
||||
@@ -51,6 +52,10 @@ def advance():
|
||||
return st
|
||||
last = pd.Timestamp(st["last"])
|
||||
new = r[r.index > last]
|
||||
# SOLO giorni CHIUSI: l'ultima riga dei rendimenti giornalieri e' il giorno in corso
|
||||
# (~19 min/giorno registrati, r0822_monitor_audit). E' un monitor da dashboard, ma la
|
||||
# regola D2 vale anche qui.
|
||||
new = new[PG.indice_chiuso(new.index, PG.MS_1D)]
|
||||
if len(new):
|
||||
eq = st["equity"]; peak = st["peak"]; dd = st["max_dd"]
|
||||
lines = []
|
||||
|
||||
@@ -38,6 +38,7 @@ PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
|
||||
from src.backtest.harness import load # noqa: E402
|
||||
from src.live import paper_guard as PG # noqa: E402
|
||||
from src.strategies.prevday_breakout import target as prevday_target # noqa: E402
|
||||
from src.strategies import prevday_breakout as pb # noqa: E402
|
||||
|
||||
@@ -93,7 +94,10 @@ def advance(st: dict, dfs: dict) -> dict:
|
||||
dt=pd.to_datetime(df["datetime"]).values, r=r,
|
||||
tgt=prevday_target(df))
|
||||
common = sorted(set(data["BTC"]["ts"]).intersection(data["ETH"]["ts"]))
|
||||
new_ts = [t for t in common if t > st["last_ts"]]
|
||||
# SOLO barre 1h CHIUSE: il parquet 1h puo' contenere l'ora in corso (1 barra su 24 —
|
||||
# r0822_monitor_audit); la si lascia al giro successivo
|
||||
new_ts = [t for t in common
|
||||
if t > st["last_ts"] and PG.chiusa(int(t), PG.MS_1H)]
|
||||
if not new_ts:
|
||||
return st
|
||||
idx = {a: {int(t): i for i, t in enumerate(data[a]["ts"])} for a in ASSETS}
|
||||
|
||||
@@ -0,0 +1,228 @@
|
||||
"""paper_regen.py — RIGENERA le serie dei forward-monitor rotti da `advance()` (§5.1).
|
||||
|
||||
PERCHE' RIGENERARE E NON AZZERARE. L'audit r0822_monitor_audit ha provato due volte, in modo
|
||||
indipendente, che il replay sul pannello di oggi E' la serie corretta sulla stessa finestra:
|
||||
(a) `paper_prevday` ha 1427/1488 barre identiche al bit dopo 62 notti di riscrittura del feed;
|
||||
(b) le 6 barre chiuse di `paper_statarb` recuperate dopo il guasto EPERM coincidono al bit.
|
||||
Quindi la finestra forward NON va persa: si rigenera dallo STESSO `start_ts` pre-registrato,
|
||||
con la config congelata e il codice di produzione riparato — e nessuna data di gate si sposta.
|
||||
|
||||
COSA FA, per ciascun monitor (statarb · dvolspread · xsr · prevday):
|
||||
1. archivia i file correnti come *.pre_regen_<data>.* (NON li cancella: sono l'evidenza
|
||||
del difetto, e stanno nel perimetro di backup);
|
||||
2. ricostruisce lo stato iniziale all'ORIGINALE `start_ts` (stessa costruzione dell'audit);
|
||||
3. riesegue `advance()` di produzione — che ora consuma SOLO barre chiuse (paper_guard);
|
||||
4. salva lo stato e stampa il verdetto a runtime (N11): barre, Sharpe vecchio vs nuovo,
|
||||
e la prova che l'ultima barra registrata e' chiusa.
|
||||
|
||||
`paper_portfolio` e `paper_combo` NON si rigenerano qui, e la scelta e' dichiarata: la gamba
|
||||
GTAA legge `ADJUSTED_LAST` di IB ri-aggiustato all'indietro, quindi il replay NON sarebbe la
|
||||
serie che un monitor sano avrebbe registrato (P12: meglio una storia corrotta e dichiarata di
|
||||
una riscritta e spacciata per registrata). Sono dashboard senza gate; dal fix in poi scrivono
|
||||
solo barre chiuse.
|
||||
|
||||
uv run python scripts/live/paper_regen.py # tutti e 4
|
||||
uv run python scripts/live/paper_regen.py --solo paper_statarb
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import json
|
||||
import shutil
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
ANN = float(np.sqrt(365.0))
|
||||
SUFFISSO = datetime.now(timezone.utc).strftime("pre_regen_%Y%m%d")
|
||||
|
||||
|
||||
def live(name: str):
|
||||
spec = importlib.util.spec_from_file_location(f"_regen_{name}",
|
||||
ROOT / "scripts" / "live" / f"{name}.py")
|
||||
m = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(m)
|
||||
return m
|
||||
|
||||
|
||||
def sharpe_jsonl(path: Path, campo: str = "net_modeled") -> tuple[float, int]:
|
||||
if not path.exists():
|
||||
return float("nan"), 0
|
||||
r = []
|
||||
for ln in path.read_text().splitlines():
|
||||
ln = ln.strip()
|
||||
if ln:
|
||||
r.append(float(json.loads(ln)[campo]))
|
||||
r = np.asarray(r)
|
||||
if len(r) < 2 or r.std() == 0:
|
||||
return float("nan"), len(r)
|
||||
return float(r.mean() / r.std() * ANN), len(r)
|
||||
|
||||
|
||||
def archivia(d: Path, nomi: tuple[str, ...]) -> None:
|
||||
for n in nomi:
|
||||
f = d / n
|
||||
if f.exists():
|
||||
dest = d / f"{f.stem}.{SUFFISSO}{f.suffix}"
|
||||
if dest.exists():
|
||||
raise SystemExit(f" {dest} esiste gia': rigenerazione doppia nello stesso "
|
||||
"giorno? Verificare a mano prima di riprovare.")
|
||||
shutil.move(str(f), str(dest))
|
||||
print(f" archiviato {f.name} -> {dest.name}")
|
||||
|
||||
|
||||
def verdetto(nome: str, d: Path, mod, st: dict, sh_prima: float, n_prima: int,
|
||||
cadenza_ms: int) -> None:
|
||||
from src.live.paper_guard import chiusa
|
||||
sh_dopo, n_dopo = sharpe_jsonl(d / "returns.jsonl")
|
||||
ult = st["last_ts"] if "last_ts" in st else None
|
||||
ok_chiusa = chiusa(int(ult), cadenza_ms) if ult else False
|
||||
print(f" barre: {n_prima} registrate (rotte) -> {n_dopo} rigenerate (chiuse)")
|
||||
print(f" Sharpe ann. net_modeled: {sh_prima:+.2f} (serie rotta) -> {sh_dopo:+.2f} "
|
||||
f"(serie vera)")
|
||||
print(f" ultima barra {pd.Timestamp(int(ult), unit='ms', tz='UTC').date() if ult else '?'} "
|
||||
f"chiusa: {'SI' if ok_chiusa else 'NO — QUALCOSA NON VA'}")
|
||||
if not ok_chiusa:
|
||||
raise SystemExit(f" {nome}: la rigenerazione ha consumato una barra NON chiusa")
|
||||
|
||||
|
||||
def regen_statarb() -> None:
|
||||
print("\n== paper_statarb (gate 2026-09-27) " + "=" * 50)
|
||||
d = ROOT / "data" / "paper_statarb"
|
||||
ps = live("paper_statarb")
|
||||
st_old = json.loads((d / "state.json").read_text())
|
||||
sh0, n0 = sharpe_jsonl(d / "returns.jsonl")
|
||||
j = ps.build_joint("1d")
|
||||
ts, _, pos, _ = ps._signal(j)
|
||||
i0 = int(np.where(ts == st_old["start_ts"])[0][0])
|
||||
archivia(d, ("returns.jsonl", "trades.jsonl", "state.json"))
|
||||
st = dict(start_ts=st_old["start_ts"], last_ts=st_old["start_ts"], n_bars=0,
|
||||
pos_modeled=float(pos[i0]), pos_real=float(pos[i0]),
|
||||
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)
|
||||
st = ps.advance(st, j)
|
||||
ps._state_io(st)
|
||||
verdetto("paper_statarb", d, ps, st, sh0, n0, 86_400_000)
|
||||
|
||||
|
||||
def regen_dvolspread() -> None:
|
||||
print("\n== paper_dvolspread (kill 2026-10-24 / decisione 2027-01-24) " + "=" * 24)
|
||||
d = ROOT / "data" / "paper_dvolspread"
|
||||
pv = live("paper_dvolspread")
|
||||
st_old = json.loads((d / "state.json").read_text())
|
||||
sh0, n0 = sharpe_jsonl(d / "returns.jsonl")
|
||||
P = pv._panel()
|
||||
i0 = int(np.where(P["ts"] == st_old["start_ts"])[0][0])
|
||||
archivia(d, ("returns.jsonl", "state.json"))
|
||||
st = dict(start_ts=st_old["start_ts"], last_ts=st_old["start_ts"], n_bars=0, n_active=0,
|
||||
n_flat_signal=0, n_flat_nodata=0,
|
||||
wb_modeled=float(P["wb"][i0]), wb_real=float(P["wb"][i0]),
|
||||
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)
|
||||
st = pv.advance(st, P)
|
||||
pv._state_io(st)
|
||||
verdetto("paper_dvolspread", d, pv, st, sh0, n0, 86_400_000)
|
||||
|
||||
|
||||
def regen_xsr() -> None:
|
||||
print("\n== paper_xsr (gate 2026-10-23) " + "=" * 54)
|
||||
d = ROOT / "data" / "paper_xsr"
|
||||
px = live("paper_xsr")
|
||||
st_old = json.loads((d / "state.json").read_text())
|
||||
sh0, n0 = sharpe_jsonl(d / "returns.jsonl")
|
||||
archivia(d, ("returns.jsonl", "state.json"))
|
||||
st = dict(start_ts=st_old["start_ts"], last_ts=st_old["start_ts"], n_bars=0,
|
||||
syms=st_old["syms"],
|
||||
modeled=px._book(px.MODELED_CAPITAL, len(st_old["syms"])),
|
||||
reals={n: px._book(c, len(st_old["syms"])) for c, n in px.REAL_BOOKS})
|
||||
st = px.advance(st)
|
||||
px._state_io(st)
|
||||
verdetto("paper_xsr", d, px, st, sh0, n0, 86_400_000)
|
||||
|
||||
|
||||
def regen_prevday() -> None:
|
||||
print("\n== paper_prevday (griglia ORARIA, nessun gate a data fissa) " + "=" * 24)
|
||||
d = ROOT / "data" / "paper_prevday"
|
||||
pp = live("paper_prevday")
|
||||
st_old = json.loads((d / "state.json").read_text())
|
||||
sh0, n0 = sharpe_jsonl(d / "returns.jsonl")
|
||||
dfs = pp.build_bars()
|
||||
S = st_old["start_ts"]
|
||||
pos = {a: pp.pb.current_target(dfs[a][dfs[a]["timestamp"] <= S]) for a in pp.ASSETS}
|
||||
archivia(d, ("returns.jsonl", "trades.jsonl", "state.json"))
|
||||
st = 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)
|
||||
st = pp.advance(st, dfs)
|
||||
pp._state_io(st)
|
||||
verdetto("paper_prevday", d, pp, st, sh0, n0, 3_600_000)
|
||||
|
||||
|
||||
def trim_portfolio() -> None:
|
||||
"""`paper_portfolio` NON si rigenera (la gamba GTAA legge ADJUSTED_LAST ri-aggiustato:
|
||||
il replay non sarebbe la serie registrata — P12). Ma la sua CODA contiene la riga del
|
||||
giorno in corso, scritta dal codice vecchio: senza toglierla la guardia PREMATURO
|
||||
resterebbe accesa per sempre su un difetto gia' riparato (P14). Il taglio e' onesto:
|
||||
la storia registrata resta com'e' (troncata e DICHIARATA in memoria), va via solo la
|
||||
riga che dice di essere un giorno e non lo e' — la versione chiusa la riscrive il
|
||||
prossimo giro del cron col codice riparato."""
|
||||
from src.live.paper_guard import MS_1D, indice_chiuso
|
||||
print("\n== paper_portfolio (dashboard — TRIM della coda non chiusa, non rigenerazione) ==")
|
||||
d = ROOT / "data" / "paper_portfolio"
|
||||
righe = (d / "equity.csv").read_text().strip().splitlines()
|
||||
testa, dati = righe[0], righe[1:]
|
||||
date = pd.DatetimeIndex([r.split(",")[0] for r in dati])
|
||||
m = indice_chiuso(date, MS_1D)
|
||||
tolte = [r for r, k in zip(dati, m) if not k]
|
||||
if not tolte:
|
||||
print(" coda gia' pulita: niente da fare")
|
||||
return
|
||||
archivia(d, ("equity.csv", "state.json"))
|
||||
dati_ok = [r for r, k in zip(dati, m) if k]
|
||||
(d / "equity.csv").write_text("\n".join([testa] + dati_ok) + "\n")
|
||||
eq = [float(r.split(",")[1]) for r in dati_ok]
|
||||
peak, dd = 0.0, 0.0
|
||||
for e in eq:
|
||||
peak = max(peak, e)
|
||||
dd = max(dd, (peak - e) / peak if peak > 0 else 0.0)
|
||||
st_old = json.loads((d / f"state.{SUFFISSO}.json").read_text())
|
||||
st = dict(st_old, last=dati_ok[-1].split(",")[0], equity=eq[-1], peak=peak, max_dd=dd,
|
||||
n_days=st_old["n_days"] - len(tolte))
|
||||
(d / "state.json").write_text(json.dumps(st, indent=2))
|
||||
for r in tolte:
|
||||
print(f" tolta riga non chiusa: {r}")
|
||||
print(f" ultima riga ora: {dati_ok[-1]} (peak/max_dd ricalcolati dalla serie)")
|
||||
|
||||
|
||||
REGEN = dict(paper_statarb=regen_statarb, paper_dvolspread=regen_dvolspread,
|
||||
paper_xsr=regen_xsr, paper_prevday=regen_prevday,
|
||||
paper_portfolio=trim_portfolio)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
print("=" * 90)
|
||||
print(" RIGENERAZIONE forward-monitor — stesso start_ts, config congelata, solo barre chiuse")
|
||||
print("=" * 90)
|
||||
solo = None
|
||||
if "--solo" in sys.argv:
|
||||
solo = sys.argv[sys.argv.index("--solo") + 1]
|
||||
if solo not in REGEN:
|
||||
raise SystemExit(f"monitor sconosciuto: {solo} (validi: {', '.join(REGEN)})")
|
||||
for nome, fn in REGEN.items():
|
||||
if solo and nome != solo:
|
||||
continue
|
||||
fn()
|
||||
print("\n fatto. I file *.%s.* sono l'evidenza del difetto e restano nel backup." % SUFFISSO)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -51,6 +51,7 @@ sys.path.insert(0, str(PROJECT_ROOT / "scripts" / "research"))
|
||||
|
||||
# Segnale ESATTO dello sweep (nessuna reimplementazione → identità garantita col backtest).
|
||||
from orthogonal_signals import build_joint, f_statarb_resid, spread_ret # noqa: E402
|
||||
from src.live import paper_guard as PG # noqa: E402
|
||||
|
||||
STATE_DIR = PROJECT_ROOT / "data" / "paper_statarb"
|
||||
STATE_FILE = STATE_DIR / "state.json"
|
||||
@@ -99,7 +100,9 @@ def init_state(j: pd.DataFrame) -> dict:
|
||||
|
||||
def advance(st: dict, j: pd.DataFrame) -> dict:
|
||||
ts, dt, pos, sr = _signal(j)
|
||||
new = [i for i in range(len(ts)) if ts[i] > st["last_ts"]]
|
||||
# SOLO barre CHIUSE: `build_joint('1d')` passa da `resample_tf`, che NON scarta il giorno
|
||||
# in corso (1 barra 1h su 24) — consumarlo faceva registrare ~4 min/giorno (r0822_monitor_audit)
|
||||
new = PG.nuove_chiuse(ts, st["last_ts"], PG.MS_1D)
|
||||
if not new:
|
||||
return st
|
||||
pm, pr = st["pos_modeled"], st["pos_real"] # posizioni TENUTE (decise alla barra precedente)
|
||||
|
||||
@@ -65,6 +65,7 @@ sys.path.insert(0, str(PROJECT_ROOT / "scripts" / "research"))
|
||||
|
||||
# Segnale ESATTO dello studio (nessuna reimplementazione -> niente drift col backtest).
|
||||
from r0725_statarb_multi import BASE, MIN_BARS, load_hl, signal, universe # noqa: E402
|
||||
from src.live import paper_guard as PG # noqa: E402
|
||||
|
||||
STATE_DIR = PROJECT_ROOT / "data" / "paper_xsr"
|
||||
STATE_FILE = STATE_DIR / "state.json"
|
||||
@@ -158,7 +159,9 @@ def advance(st: dict) -> dict:
|
||||
print(f" [XSR01] universo cambiato ({len(st['syms'])} -> {len(syms)}): "
|
||||
"la finestra forward richiede universo costante. Usa --reset per ripartire.")
|
||||
return st
|
||||
new = [i for i in range(len(ts)) if ts[i] > st["last_ts"]]
|
||||
# SOLO barre CHIUSE: l'ultima riga del parquet HL e' il giorno IN CORSO, e consumarla
|
||||
# e' il difetto che faceva registrare ~41 minuti di mercato al giorno (r0822_monitor_audit)
|
||||
new = PG.nuove_chiuse(ts, st["last_ts"], PG.MS_1D)
|
||||
if not new:
|
||||
return st
|
||||
for i in new:
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
"""r0826_skh_band_drift.py — si guarda SOTTO il test rosso §5.9 prima di toccare la banda.
|
||||
|
||||
IL FATTO. `test_t1_canonical_riproduce_il_backtest_ufficiale` fallisce dal ~24-25/08:
|
||||
Sharpe hold-out SKH01 `canonical` = 1,9223 contro la banda cablata `1.3 < h < 1.9`
|
||||
(scritta il 25/07 sull'audit del 02/07: ~1,64). Il codice non e' cambiato; `data/raw/`
|
||||
e' gitignored e il cron lo ricostruisce ogni notte -> la finestra si allunga e il numero
|
||||
deriva. Lezione 07/08: un test che fallisce senza che il codice sia cambiato sta
|
||||
segnalando QUALCOSA — si misura cosa, non si allarga la banda a occhio.
|
||||
|
||||
LA MISURA. Stesso codice di produzione (`r0726_skh_onbook.skh_series`), stesso `sh3`,
|
||||
ma col feed TAGLIATO a date crescenti: se il numero alla data dell'audit riproduce
|
||||
~1,64 e la curva sale in modo regolare con le barre aggiunte, la deriva e' CRESCITA
|
||||
DELLA FINESTRA (hold-out 2025+ che si allunga con settimane favorevoli), non un dato
|
||||
corrotto ne' un codice cambiato. Se invece il taglio all'epoca NON riproduce il numero
|
||||
dell'epoca, il problema e' nel DATO RICOSTRUITO e la banda e' l'ultima delle questioni.
|
||||
|
||||
COSA MI ASPETTO PRIMA DI MISURARE (M12): riproduzione al taglio 02/07 dentro ±0,05;
|
||||
curva monotona o quasi verso 1,92 (ago 2026 e' stato un rally forte, BTC +21%/30g,
|
||||
e SKH01 era long ETH da meta' agosto).
|
||||
|
||||
uv run python scripts/research/r0826_skh_band_drift.py
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pandas as pd
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
for p in (ROOT, ROOT / "scripts" / "research", ROOT / "scripts" / "research" / "alt"):
|
||||
sys.path.insert(0, str(p))
|
||||
|
||||
import r0702_anchor_skh01 as r02 # noqa: E402
|
||||
import r0726_skh_onbook as ob # noqa: E402
|
||||
|
||||
TAGLI = ("2026-07-02", # la data dell'audit che ha scritto ~1,64
|
||||
"2026-07-25", # il giorno in cui la banda e' stata cablata nel test
|
||||
"2026-08-01", "2026-08-08", "2026-08-15", "2026-08-22",
|
||||
None) # oggi: deve riprodurre l'1,9223 del test rosso
|
||||
|
||||
_GET5M_VERO = r02.get5m.__wrapped__ # la funzione nuda, senza lru_cache
|
||||
|
||||
|
||||
def _taglia(cut_ms: int | None):
|
||||
"""Rimpiazza get5m con una versione tagliata e svuota TUTTE le cache a valle."""
|
||||
if cut_ms is None:
|
||||
def g(asset: str):
|
||||
return _GET5M_VERO(asset)
|
||||
else:
|
||||
def g(asset: str):
|
||||
df = _GET5M_VERO(asset)
|
||||
return df[df["timestamp"] < cut_ms].reset_index(drop=True)
|
||||
r02.get5m = g
|
||||
# ⚠️ run_asset non usa lru_cache: memoizza in un DICT di modulo (`_CACHE`) — svuotare
|
||||
# solo le lru_cache serviva a niente e la prima stesura di questo script ha misurato
|
||||
# SETTE volte la stessa finestra (tutte le righe identiche). Si svuota il dict.
|
||||
r02._CACHE.clear()
|
||||
r02.skh_port.cache_clear()
|
||||
|
||||
|
||||
def main() -> None:
|
||||
print("=" * 96)
|
||||
print(" SKH01 canonical — Sharpe (full / in-sample / HOLD-OUT) in funzione della finestra dati")
|
||||
print(" hold-out = barre >= 2025-01-01: si ALLUNGA ogni notte col cron")
|
||||
print("=" * 96)
|
||||
print(f" {'taglio':>12} {'barre HO (g)':>13} {'full':>7} {'in-s':>7} {'HOLD-OUT':>9} "
|
||||
f"{'maxDD':>8} nota")
|
||||
print(" " + "-" * 90)
|
||||
esiti = {}
|
||||
for cut in TAGLI:
|
||||
cut_ms = None if cut is None else int(pd.Timestamp(cut, tz="UTC").value // 10**6)
|
||||
_taglia(cut_ms)
|
||||
s, _ = ob.skh_series(0, "canonical")
|
||||
full, is_, hold, dd = ob.sh3(s)
|
||||
n_ho = int((s.index >= r02.HOLDOUT).sum())
|
||||
nota = ""
|
||||
if cut == "2026-07-02":
|
||||
nota = "<- deve riprodurre ~1,64 (audit 02/07)"
|
||||
elif cut == "2026-07-25":
|
||||
nota = "<- il giorno della banda 1.3<h<1.9"
|
||||
elif cut is None:
|
||||
nota = "<- deve riprodurre l'1,9223 del test rosso"
|
||||
lab = cut or "oggi"
|
||||
esiti[lab] = hold
|
||||
print(f" {lab:>12} {n_ho:>13} {full:>7.3f} {is_:>7.3f} {hold:>9.4f} {dd:>7.1%} {nota}")
|
||||
|
||||
print("\n" + "=" * 96)
|
||||
print(" VERDETTO (runtime)")
|
||||
print("=" * 96)
|
||||
rip_audit = abs(esiti["2026-07-02"] - 1.64) < 0.05
|
||||
rip_oggi = abs(esiti["oggi"] - 1.9223) < 0.01
|
||||
serie = [esiti[k] for k in esiti]
|
||||
mono = all(b >= a - 0.03 for a, b in zip(serie, serie[1:]))
|
||||
print(f" taglio 02/07 riproduce l'audit (~1,64)? {'SI' if rip_audit else 'NO'} "
|
||||
f"({esiti['2026-07-02']:.4f})")
|
||||
print(f" nessun taglio riproduce -> dato cambiato: "
|
||||
f"{'no, il dato regge' if rip_audit else 'SI - INDAGARE IL FEED'}")
|
||||
print(f" oggi riproduce il numero del test rosso? {'SI' if rip_oggi else 'NO'} "
|
||||
f"({esiti['oggi']:.4f})")
|
||||
print(f" la curva sale con la finestra (quasi-monot.)? {'SI' if mono else 'NO'}")
|
||||
if rip_audit and rip_oggi:
|
||||
print("\n => La deriva e' CRESCITA DELLA FINESTRA, non corruzione ne' codice: il test")
|
||||
print(" confronta un numero che si muove col feed contro una banda scritta su una")
|
||||
print(" finestra congelata. La riparazione giusta e' ancorare il test alla finestra")
|
||||
print(" su cui la banda fu scritta (taglio fisso), non allargare la banda — una")
|
||||
print(" banda che insegue il feed non riproduce niente (N11).")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user