feat(live): GridWorker (Price Ladder) SIM/PAPER + validazione replay==backtest — shadow stage 1

Stage 1 dello shadow per il Price Ladder (config finale: BTC 1h range1.5 rd0.20 ru0.06 L6
sl0.10 tp0.03). GridWorker (src/live/grid_worker.py) gira sul feed LIVE e contabilizza
l'equity mark-to-market col motore CANONICO grid_mtm (parita' col backtest per costruzione),
SENZA piazzare ordini reali (sim/paper). Stato persistente + resume. grid_mtm esteso con
param df=None (retro-compatibile: il feed live passa il df; None = _load come prima, gate
invariato — BTC ladder 10.8/5.9, PORT06 base 8.18 identici). Validazione
validate_grid_worker.py: [A] full-tick == grid_mtm esatto, [B] replay incrementale converge
esatto, [C] resume entro la persistenza (4 dec) -> VALIDAZIONE OK.

NB SICUREZZA: nessuna modifica a runner/portfolios.yml/_defs -> il sistema mainnet (€500
reali) e' INTATTO; il worker e' inerte finche' non wirato. L'esecuzione REALE (griglia di
LIMIT resting su Deribit, fill parziali/episodi) e' lo stage 2-3, dietro testnet +
autorizzazione esplicita. Il runner avvia ordini reali solo per kind in (single,ml);
kind=grid resta sim per costruzione.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Adriano Dal Pastro
2026-06-18 14:58:11 +00:00
parent 587fbc0f61
commit b3d4ab7150
3 changed files with 210 additions and 2 deletions
+80
View File
@@ -0,0 +1,80 @@
"""VALIDA GridWorker — replay live == backtest grid_mtm (gate obbligatorio del progetto).
Confronta il GridWorker (sim/paper, src/live/grid_worker.py) col motore canonico grid_mtm:
[A] un tick con tutta la storia -> capital == initial * grid_mtm(full).equity[-1] (parita');
[B] replay INCREMENTALE (tick su finestra che cresce) -> converge allo stesso capitale finale;
[C] resume: reistanzia il worker (rilegge status.json) -> capitale persistito.
Config = la finale del re-gate pulito: BTC 1h range1.5 rd0.20 ru0.06 L6 sl0.10 tp0.03.
uv run python scripts/analysis/validate_grid_worker.py
"""
from __future__ import annotations
import shutil
import sys
import tempfile
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(PROJECT_ROOT))
from src.data.downloader import load_data
from scripts.analysis.grid_game_gate import grid_mtm
from scripts.analysis.ladder_search import regime_mask
from src.live.grid_worker import GridWorker
CFG = dict(tf="1h", range_down=0.20, range_up=0.06, levels=6,
sl_buf=0.10, tp_buf=0.03, max_bars=720, regime="range", trend_max=1.5)
ASSET, INIT, POS, LEV, FEE = "BTC", 1000.0, 0.15, 3.0, 0.0005
def main():
df = load_data(ASSET, "1h")
# backtest canonico
mask = regime_mask(ASSET, "1h", trend_max=CFG["trend_max"])
cfg_bt = {k: v for k, v in CFG.items() if k not in ("regime", "trend_max")}
eqd_bt, st_bt = grid_mtm(ASSET, **cfg_bt, pos=POS, lev=LEV, fee_side=FEE, deploy_mask=mask)
target_cap = INIT * float(eqd_bt.iloc[-1])
print(f"[backtest] grid_mtm equity[-1]={eqd_bt.iloc[-1]:.6f} -> capital {target_cap:,.2f} "
f"(trades {st_bt['trades']}, win {st_bt['win']:.1f}%)")
wd = Path(tempfile.mkdtemp(prefix="gridval_"))
try:
# [A] un tick con tutta la storia
w = GridWorker("GRID_BTC", ASSET, CFG, INIT, wd, leverage=LEV,
position_size=POS, fee_side=FEE)
w.tick(df)
dA = abs(w.capital - target_cap)
print(f"[A] full-tick capital {w.capital:,.2f} delta {dA:.6f} "
f"{'OK' if dA < 1e-6 else 'MISMATCH'}")
# [B] replay incrementale (ultimi 3 tick su finestra crescente)
n = len(df)
caps = []
for end in (n - 200, n - 50, n):
wd2 = Path(tempfile.mkdtemp(prefix="gridval_inc_"))
wi = GridWorker("GRID_BTC", ASSET, CFG, INIT, wd2, leverage=LEV,
position_size=POS, fee_side=FEE)
wi.tick(df.iloc[:end].reset_index(drop=True))
caps.append(wi.capital)
shutil.rmtree(wd2, ignore_errors=True)
dB = abs(caps[-1] - target_cap)
print(f"[B] incrementale capitali {[round(c,2) for c in caps]} "
f"finale delta {dB:.6f} {'OK' if dB < 1e-6 else 'MISMATCH'}")
# [C] resume
w2 = GridWorker("GRID_BTC", ASSET, CFG, INIT, wd, leverage=LEV,
position_size=POS, fee_side=FEE)
dC = abs(w2.capital - w.capital)
# status.json persiste capital a 4 decimali -> tolleranza = precisione di persistenza
print(f"[C] resume capital {w2.capital:,.2f} delta {dC:.6f} "
f"{'OK' if dC < 1e-3 else 'MISMATCH'} (persistenza 4 dec.)")
ok = dA < 1e-6 and dB < 1e-6 and dC < 1e-3
print(f"\n{'VALIDAZIONE OK: GridWorker replay == backtest' if ok else 'VALIDAZIONE FALLITA'}")
finally:
shutil.rmtree(wd, ignore_errors=True)
if __name__ == "__main__":
main()