4dc0e77ee5
src/live/pairs_worker.py: PairsWorker market-neutral (long A / short B sullo z-score del log-ratio, exit |z|<=z_exit o max_bars, FEE SU 2 GAMBE = 2*fee_rt*lev, stato persistente come StrategyWorker). multi_runner: sezione `pairs:` nello YAML, fetch di entrambe le gambe, tick/status/shutdown; INSTRUMENT_MAP esteso agli alt. strategies.yml: 5 coppie PR01 (config universale n50 z2 zx0.75 mb72). Validazione (scripts/analysis/validate_worker_pairs.py): replay live bar-per-bar == backtest pairs_sim ESATTAMENTE -> ETH/BTC capitale 2.870.429 = 2.870.429, 1754 trade, win 74.1% identici. Caveat: shortabilita'/liquidita' del perp B sugli alt da verificare in trading reale. CLAUDE.md / docstring PR01 aggiornati. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
75 lines
3.3 KiB
Python
75 lines
3.3 KiB
Python
"""Valida il PairsWorker: replay bar-per-bar sui dati storici == backtest pairs_sim?
|
|
|
|
Come validate_worker_mr01 per MR01: alimenta il PairsWorker con finestre trailing
|
|
crescenti (simula il feed live) e confronta trade/capitale finale col backtest di
|
|
riferimento scripts/analysis/pairs_research.pairs_sim. Se combaciano, la semantica
|
|
live (z-score causale, exit |z|<=z_exit o max_bars, fee 2 gambe) e' fedele.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import shutil
|
|
import sys
|
|
import tempfile
|
|
from pathlib import Path
|
|
|
|
import pandas as pd
|
|
|
|
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
|
sys.path.insert(0, str(PROJECT_ROOT))
|
|
|
|
from src.live.pairs_worker import PairsWorker
|
|
from scripts.analysis.pairs_research import aligned, pairs_sim
|
|
from scripts.strategies.PR01_pairs_reversion import PAIRS
|
|
|
|
WINDOW = 60 # finestra trailing minima (>= n+2): z[i] corretto, replay veloce
|
|
|
|
|
|
def replay(a: str, b: str, params: dict, data_dir: Path) -> PairsWorker:
|
|
m = aligned(a, b)
|
|
df_a = m[["timestamp"]].copy(); df_a["close"] = m["close_a"].values
|
|
df_b = m[["timestamp"]].copy(); df_b["close"] = m["close_b"].values
|
|
w = PairsWorker(a, b, "1h", params=params, fee_rt=0.001, data_dir=data_dir)
|
|
# replay veloce: niente I/O su file / log / notifiche ad ogni tick (servono solo le metriche finali)
|
|
w._save_state = lambda: None
|
|
w._log = lambda *a, **k: None
|
|
w._notify = lambda *a, **k: None
|
|
n = w.n
|
|
for k in range(n + 2, len(m) + 1):
|
|
lo = max(0, k - WINDOW)
|
|
w.tick(df_a.iloc[lo:k], df_b.iloc[lo:k])
|
|
# chiudi eventuale posizione aperta a fine serie (come fa il backtest col troncamento)
|
|
return w
|
|
|
|
|
|
def main():
|
|
print("=" * 96)
|
|
print(" VALIDAZIONE PairsWorker — replay live vs backtest pairs_sim (fee 0.20% RT/coppia)")
|
|
print("=" * 96)
|
|
print(f" {'coppia':<10s}{'WORKER cap':>12s}{'trd':>5s}{'win%':>6s} | {'BACKTEST cap':>13s}{'trd':>5s}{'win%':>6s} match?")
|
|
print(" " + "-" * 88)
|
|
# Sottoinsieme rappresentativo: il codice del worker e' identico per ogni coppia,
|
|
# quindi 2 coppie con strutture diverse (alt/major e major/alt) bastano a provare
|
|
# l'equivalenza. ~135s/coppia su 73k barre orarie. Per validarle tutte: usa PAIRS.
|
|
subset = [pp for pp in PAIRS if (pp[0], pp[1]) in {("ETH", "BTC"), ("BTC", "LTC")}]
|
|
tmp = Path(tempfile.mkdtemp(prefix="pairs_validate_"))
|
|
try:
|
|
for a, b, p in subset:
|
|
w = replay(a, b, p, tmp)
|
|
bt = pairs_sim(a, b, **p)
|
|
bt_cap = 1000.0 * (1 + bt["ret"] / 100)
|
|
cap_match = abs(w.capital - bt_cap) / bt_cap < 0.02 if bt_cap else False
|
|
trd_match = abs(w.total_trades - bt["trades"]) <= max(2, bt["trades"] * 0.02)
|
|
ok = "OK" if (cap_match and trd_match) else "DIFF"
|
|
ww = w.total_wins / w.total_trades * 100 if w.total_trades else 0
|
|
print(f" {a+'/'+b:<10s}{w.capital:>12.0f}{w.total_trades:>5d}{ww:>6.1f} | "
|
|
f"{bt_cap:>13.0f}{bt['trades']:>5d}{bt['win']:>6.1f} {ok}")
|
|
finally:
|
|
shutil.rmtree(tmp, ignore_errors=True)
|
|
print(" " + "-" * 88)
|
|
print(" match = capitale entro 2% e trade entro 2% del backtest. Differenze minime sono")
|
|
print(" attese (gestione bar finale/troncamento), ma la semantica deve coincidere.")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|