live(monitor): prevday-breakout in FORWARD-MONITOR (paper, non deploy)
Il lead ortogonale a TP01 sopravvissuto all'onda intraday entra in forward-monitor (stesso trattamento di XS01 STAT-MODE / STA05), NON in esecuzione reale. - src/strategies/prevday_breakout.py: segnale CONGELATO (params fissi anchor=1, k=0.30, simmetrico, vol-target 0.20/30/2.0), self-contained. Bit-identico all'agent di ricerca (max diff 0.0): BTC full Sh 1.18/hold 0.92, ETH 1.09/1.42; marginal ADDS, earns_slot, corr_hold -0.01, non-hedge. - scripts/live/paper_prevday.py: forward-only paper, traccia DUE libri — MODELED ($2000 continuo) e REAL-$600 (salta i ribilanciamenti < min-order $5) -> il gap = haircut di fill reale che lo scettico aveva segnalato. Inizializzato forward-only da oggi. - cron_daily.sh: avanza il monitor ogni giorno. - test: param congelati + causale + bounded + long-short. Suite intera verde. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,182 @@
|
||||
"""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()
|
||||
Reference in New Issue
Block a user