"""FORWARD-MONITOR — PREVDAY RANGE BREAKOUT (lead ortogonale a TP01), forward-only, PAPER. NON è esecuzione reale. È il monitoraggio forward-only del LEAD validato dall'onda intraday (src/strategies/prevday_breakout.py, parametri CONGELATI) per vedere se l'edge in-sample regge FUORI CAMPIONE VERO nei prossimi mesi. Stesso trattamento di XS01 STAT-MODE / STA05. DESIGN (onesto): - Legge i parquet certificati BTC/ETH 1h (data/raw). Segnale a 1h, libro 50/50. - Alla prima esecuzione parte dall'ultima barra 1h CHIUSA (forward-only: lo storico NON entra nel PnL di paper, si traccia solo da ora in avanti). - Ogni run processa le NUOVE barre 1h chiuse: applica il rendimento della posizione tenuta, addebita le fee sul turnover, registra i flip di segno, poi ricalcola la posizione-bersaglio. - Traccia DUE libri in parallelo per onestà sull'esecuzione (lo scettico ha segnalato che a $600 il micro-ribilanciamento del vol-target ha un haircut di fill): * MODELED : capitale nominale $2000, ribilanciamento continuo (fee proporzionale su ogni |Δ|). * REAL-$600: capitale reale $600, salta i ribilanciamenti di nozionale < min_order ($5) — cosa che il conto vero catturerebbe davvero. Il gap MODELED-REAL = l'haircut di fill reale. - Per barre fresche, aggiornare prima i dati: uv run python scripts/analysis/rebuild_history.py --asset BTC ETH Stato: data/paper_prevday/{state.json, trades.jsonl, returns.jsonl} (append-only). uv run python scripts/live/paper_prevday.py # avanza col dato disponibile uv run python scripts/live/paper_prevday.py --status # solo stato, non avanza uv run python scripts/live/paper_prevday.py --reset # azzera (riparte da ora) """ from __future__ import annotations import argparse import json import sys from pathlib import Path import numpy as np import pandas as pd PROJECT_ROOT = Path(__file__).resolve().parents[2] sys.path.insert(0, str(PROJECT_ROOT)) from src.backtest.harness import load # noqa: E402 from src.strategies.prevday_breakout import target as prevday_target # noqa: E402 from src.strategies import prevday_breakout as pb # noqa: E402 STATE_DIR = PROJECT_ROOT / "data" / "paper_prevday" STATE_FILE = STATE_DIR / "state.json" TRADES_FILE = STATE_DIR / "trades.jsonl" RETURNS_FILE = STATE_DIR / "returns.jsonl" ASSETS = ["BTC", "ETH"] WEIGHT = 0.5 FEE_SIDE = 0.0005 # 0.05%/side = 0.10% round-trip (Deribit taker) 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 def build_bars() -> dict[str, pd.DataFrame]: return {a: load(a, "1h").reset_index(drop=True) for a in ASSETS} def _state_io(write: dict | None = None): if write is not None: STATE_DIR.mkdir(parents=True, exist_ok=True) STATE_FILE.write_text(json.dumps(write, indent=2)) return write return json.loads(STATE_FILE.read_text()) if STATE_FILE.exists() else None def _append(path: Path, rec: dict): STATE_DIR.mkdir(parents=True, exist_ok=True) with open(path, "a") as f: f.write(json.dumps(rec) + "\n") def init_state(dfs) -> dict: last_ts = min(int(dfs[a]["timestamp"].iloc[-1]) for a in ASSETS) pos = {a: pb.current_target(dfs[a][dfs[a]["timestamp"] <= last_ts]) for a in ASSETS} return dict( start_ts=last_ts, last_ts=last_ts, n_bars=0, pos_modeled=pos, pos_real=dict(pos), cap_modeled=MODELED_CAPITAL, cap_real=REAL_CAPITAL, peak_modeled=MODELED_CAPITAL, peak_real=REAL_CAPITAL, dd_modeled=0.0, dd_real=0.0, n_trades=0, ) def advance(st: dict, dfs: dict) -> dict: data = {} for a in ASSETS: df = dfs[a] c = df["close"].values.astype(float) r = np.zeros(len(c)); r[1:] = c[1:] / c[:-1] - 1.0 data[a] = dict(ts=df["timestamp"].values.astype("int64"), 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"]] if not new_ts: return st idx = {a: {int(t): i for i, t in enumerate(data[a]["ts"])} for a in ASSETS} pm, pr = dict(st["pos_modeled"]), dict(st["pos_real"]) cm, cr = st["cap_modeled"], st["cap_real"] pkm, pkr = st["peak_modeled"], st["peak_real"] ddm, ddr = st["dd_modeled"], st["dd_real"] ntr = st.get("n_trades", 0) for t in new_ts: net_m = net_r = 0.0 nm, nr = {}, {} for a in ASSETS: i = idx[a][int(t)] r = float(data[a]["r"][i]); tgt = float(data[a]["tgt"][i]) # MODELED: continuous rebalance hm = pm[a] net_m += WEIGHT * (hm * r - FEE_SIDE * abs(tgt - hm)) nm[a] = tgt if np.sign(tgt) != np.sign(hm): _append(TRADES_FILE, dict(ts=int(t), dt=str(pd.Timestamp(data[a]["dt"][i])), asset=a, action="ENTRY" if tgt != 0 else "EXIT", from_pos=round(hm, 4), to_pos=round(tgt, 4))) ntr += 1 # REAL-$600: skip sub-min_order rebalances hr = pr[a] leg_cap = cr * WEIGHT executed = abs(tgt - hr) * leg_cap >= MIN_ORDER new_hr = tgt if executed else hr net_r += WEIGHT * (hr * r - FEE_SIDE * abs(new_hr - hr)) nr[a] = new_hr cm *= (1.0 + max(net_m, -0.99)); cr *= (1.0 + max(net_r, -0.99)) pkm = max(pkm, cm); pkr = max(pkr, cr) ddm = max(ddm, (pkm - cm) / pkm if pkm > 0 else 0.0) ddr = max(ddr, (pkr - cr) / pkr if pkr > 0 else 0.0) pm, pr = nm, nr _append(RETURNS_FILE, dict(ts=int(t), dt=str(pd.Timestamp(data["BTC"]["dt"][idx["BTC"][int(t)]])), net_modeled=round(net_m, 6), net_real=round(net_r, 6), pos_btc=round(pr["BTC"], 4), pos_eth=round(pr["ETH"], 4), cap_modeled=round(cm, 2), cap_real=round(cr, 2))) st.update(last_ts=int(new_ts[-1]), n_bars=st.get("n_bars", 0) + len(new_ts), pos_modeled=pm, pos_real=pr, cap_modeled=cm, cap_real=cr, peak_modeled=pkm, peak_real=pkr, dd_modeled=ddm, dd_real=ddr, n_trades=ntr) return st 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 rr = st["cap_real"] / REAL_CAPITAL - 1 print(f"\n PREVDAY-BREAKOUT forward-monitor (PAPER, lead ortogonale a TP01 — non deploy)") print(f" forward da {pd.Timestamp(st['start_ts'], unit='ms', tz='UTC').date()} " f"({st['n_bars']} barre 1h ~{days:.0f}g) trade(flip): {st['n_trades']}") print(f" posizione corrente: BTC {st['pos_real']['BTC']:+.3f} ETH {st['pos_real']['ETH']:+.3f}") 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") def main(): ap = argparse.ArgumentParser() ap.add_argument("--status", action="store_true") ap.add_argument("--reset", action="store_true") args = ap.parse_args() dfs = build_bars() if args.reset: for p in (STATE_FILE, TRADES_FILE, RETURNS_FILE): if p.exists(): p.unlink() st = init_state(dfs); _state_io(st) print("forward-monitor inizializzato (forward-only da ora).") print_status(st, dfs); return st = _state_io() if st is None: st = init_state(dfs); _state_io(st) print("forward-monitor inizializzato (forward-only da ora).") print_status(st, dfs); return if not args.status: st = advance(st, dfs); _state_io(st) print_status(st, dfs) if __name__ == "__main__": main()