From f509f51543771c2d57d3c80b9013b1b6ca3cdd30 Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Mon, 8 Jun 2026 10:00:16 +0000 Subject: [PATCH] fix(live): ROT02/TSM01 scartano la barra 1d in formazione (forming-bar in _panel) Code-review follow-up: i worker daily valutavano momentum/regime e bookavano il return sulla candela 1d PARZIALE al primo poll dopo mezzanotte UTC, poi last_bar_ts bloccava la rivalutazione a giorno chiuso (stessa classe del bug gia' fixato su fade/TR01/Pairs). Fix in _panel (un punto solo -> ROT02 e TSM01 lo ereditano) via src.live.bars.last_bar_is_forming. Replay honest invariato (dati storici: nessuna barra in formazione). Test: 88/88 + parity drop forming daily bar. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/live/rotation_worker.py | 11 ++++++++++- tests/portfolio/test_rotation_worker.py | 16 ++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/live/rotation_worker.py b/src/live/rotation_worker.py index e9920b2..f7fbb8e 100644 --- a/src/live/rotation_worker.py +++ b/src/live/rotation_worker.py @@ -13,7 +13,14 @@ FEE_RT = 0.001 def _panel(data: dict, universe: list): - """Allinea {asset: df} sui timestamp comuni -> (df_panel, cols presenti).""" + """Allinea {asset: df} sui timestamp comuni -> (df_panel, cols presenti). + + Scarta la barra IN FORMAZIONE (riga -1 = candela in corso finche' non e' + trascorsa la sua durata): ROT02/TSM01 valutavano momentum/regime e bookavano + il return sulla barra 1d parziale al primo poll dopo mezzanotte UTC, poi + last_bar_ts bloccava la rivalutazione a giorno chiuso. Stessa lezione EXIT-16 + gia' applicata a fade/TR01/Pairs (detection condivisa src.live.bars).""" + from src.live.bars import last_bar_is_forming frames = {} for a in universe: df = data.get(a) @@ -25,6 +32,8 @@ def _panel(data: dict, universe: list): for a, f in frames.items(): panel = f if panel is None else panel.merge(f, on="timestamp", how="inner") panel = panel.sort_values("timestamp").reset_index(drop=True) + if len(panel) and last_bar_is_forming(panel["timestamp"].values): + panel = panel.iloc[:-1].reset_index(drop=True) cols = [a for a in universe if a in frames] return panel, cols diff --git a/tests/portfolio/test_rotation_worker.py b/tests/portfolio/test_rotation_worker.py index 0a7bfb7..6f4774e 100644 --- a/tests/portfolio/test_rotation_worker.py +++ b/tests/portfolio/test_rotation_worker.py @@ -47,6 +47,22 @@ def test_rotation_warns_once_on_short_panel(tmp_path, monkeypatch): assert w._panel_warned is False +def test_panel_drops_forming_daily_bar(tmp_path): + # serie 1d che termina ADESSO -> l'ultima riga e' la candela 1d in formazione + import time + from src.live.rotation_worker import _panel + n = 200 + now = int(time.time() * 1000) + ts = now - 86_400_000 * np.arange(n - 1, -1, -1) + def mk(slope): + c = np.linspace(100, 100 + slope * n, n) + return pd.DataFrame({"timestamp": ts, "open": c, "high": c, "low": c, + "close": c, "volume": 1.0}) + panel, cols = _panel({"BTC": mk(1.0), "AAA": mk(2.0)}, ["BTC", "AAA"]) + assert len(panel) == n - 1 # barra in formazione scartata + assert int(panel["timestamp"].iloc[-1]) == int(ts[-2]) + + def test_rotation_no_warn_on_cold_start(tmp_path, monkeypatch): calls = [] monkeypatch.setattr("src.live.telegram_notifier.notify_event",