From 4dc0e77ee518b5aae99768ddd69dd2d598ce619f Mon Sep 17 00:00:00 2001 From: AdrianoDev Date: Fri, 29 May 2026 09:12:25 +0200 Subject: [PATCH] feat(live): worker a 2 gambe per i pairs (PR01 market-neutral) 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) --- CLAUDE.md | 15 +- scripts/analysis/validate_worker_pairs.py | 74 +++++++ scripts/strategies/PR01_pairs_reversion.py | 9 +- src/live/multi_runner.py | 50 ++++- src/live/pairs_worker.py | 215 +++++++++++++++++++++ strategies.yml | 38 ++++ 6 files changed, 391 insertions(+), 10 deletions(-) create mode 100644 scripts/analysis/validate_worker_pairs.py create mode 100644 src/live/pairs_worker.py diff --git a/CLAUDE.md b/CLAUDE.md index 28a9e40..6c8313b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -24,10 +24,12 @@ src/strategies/ → classe base Strategy ABC + indicatori condivisi base.py → Strategy, Signal, BacktestResult, YearlyStats indicators.py → keltner_ratio, detect_squeezes, ema, atr, rv, correlation src/live/ → paper trading live multi-strategia - multi_runner.py → orchestratore: carica YAML, fetch candele, tick worker - strategy_worker.py → worker indipendente: capital, trade log, stato persistente. + multi_runner.py → orchestratore: carica YAML (strategies + pairs), fetch candele, tick worker + strategy_worker.py → worker single-leg: capital, trade log, stato persistente. Exit guidati da strategia (TP/SL/max_bars via Signal.metadata), fallback hold_bars/stop -2%. Usa fee_rt della strategia. + pairs_worker.py → worker a 2 GAMBE per PR01 (market-neutral): long A / short B sullo + z-score del log-ratio, exit |z|<=z_exit o max_bars, fee su 2 gambe. strategy_loader.py → import dinamico classi Strategy da scripts/strategies/ cerbero_client.py → client HTTP per Cerbero MCP (Deribit testnet) signal_engine.py → squeeze + ML real-time (legacy ML01, ora in waste) + validazione OOS @@ -157,7 +159,9 @@ quest'ultima riconferma la dominanza mean-reversion). Due edge reali: (Sharpe 4.36), LTC/ETH (3.08), ADA/ETH (2.69), BTC/LTC (2.36, robusta anche 4h), ETH/SOL (1.96, la più debole). Pattern: sempre alt-liquido vs major. Plateau confermato (heatmap 20/20 Sharpe>1) + walk-forward (ETH/BTC 11/12 finestre+). **BNB/ETH scartata** - (overfit). Corr col mercato ~0.02-0.08. Fee su **2 gambe** (worker da estendere). Verifica: `pairs_research.py`. + (overfit). Corr col mercato ~0.02-0.08. Fee su **2 gambe**: worker live implementato + (`src/live/pairs_worker.py`, sezione `pairs:` in `strategies.yml`) e validato — il replay + combacia ESATTAMENTE col backtest (`scripts/analysis/validate_worker_pairs.py`). Verifica edge: `pairs_research.py`. - **TSM01** (`scripts/analysis/tsmom_research.py`): TSMOM multi-orizzonte 3/6/12m + risk-off, **gross 0.30**, distinto da ROT02 (corr 0.62), DD 15-22%, mai un anno negativo. Robusto (36/36 config OOS+) ma diversificatore, non motore di ritorno (rende meno di ROT02). @@ -167,8 +171,9 @@ grande (`scripts/analysis/combine_v2.py`). **Numeri sobri onesti** (l'OOS singol è regime calmo → ottimistico ~50%): worst-DD su 90g rolling **~6%** (non 2.3%), Sharpe atteso **~5** (mediana semestrale), ogni anno positivo dal 2021, regge **leva 2x + slippage doppio** (CAGR 36%, Sharpe 5.1). Config robusta raccomandata: **MASTER-esteso -equal-weight, leva 2x, cap pairs ~30-35%** (i pairs sono ~57% del rischio e non ancora -validati col worker live a 2 gambe). La confluenza multi-TF è stata SCARTATA (overfit). +equal-weight, leva 2x, cap pairs ~30-35%** (i pairs sono ~57% del rischio; worker live a +2 gambe ora implementato e validato, ma shortabilità alt da verificare in reale). La +confluenza multi-TF è stata SCARTATA (overfit). **Metodologia obbligatoria per ogni nuova strategia** (per non ripetere l'errore squeeze): 1. Ingresso eseguibile: direzione e prezzo decisi con dati **fino a `close[i]`**, mai `close[i-1]` con direzione da `i`. diff --git a/scripts/analysis/validate_worker_pairs.py b/scripts/analysis/validate_worker_pairs.py new file mode 100644 index 0000000..84dcf1d --- /dev/null +++ b/scripts/analysis/validate_worker_pairs.py @@ -0,0 +1,74 @@ +"""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() diff --git a/scripts/strategies/PR01_pairs_reversion.py b/scripts/strategies/PR01_pairs_reversion.py index 77462e7..5df958f 100644 --- a/scripts/strategies/PR01_pairs_reversion.py +++ b/scripts/strategies/PR01_pairs_reversion.py @@ -26,10 +26,11 @@ Validazione anti-overfit (netto, fee 0.20% RT/coppia a 2 gambe, leva 3x, OOS = u - Correlazione con BTC daily ~0.02-0.08 -> market-neutral. - SCARTATA BNB/ETH: robusta solo coi suoi parametri (overfit), crolla con la universale. -LIMITE OPERATIVO: e' una strategia a 2 gambe (long un perp + short l'altro), il worker -attuale e' single-leg. Per tradarla serve: (a) eseguibilita' short del perp B su -Deribit/Bybit, (b) gestione 2 ordini + fee doppie. Finche' il worker non supporta -2 gambe, PR01 resta validata in backtest ma non wired nel paper trader. +WORKER LIVE: implementato `src/live/pairs_worker.py` (2 gambe, fee doppie, stato +persistente) e wired in `multi_runner` (sezione `pairs:` in strategies.yml). Validato +da `scripts/analysis/validate_worker_pairs.py`: il replay live combacia ESATTAMENTE col +backtest pairs_sim (ETH/BTC: capitale, n.trade e win% identici). Resta da verificare in +trading reale la shortabilita'/liquidita' del perp B sugli alt (LTC/SOL/ADA). """ from __future__ import annotations diff --git a/src/live/multi_runner.py b/src/live/multi_runner.py index 080489f..0d72aa5 100644 --- a/src/live/multi_runner.py +++ b/src/live/multi_runner.py @@ -11,6 +11,7 @@ import pandas as pd from src.live.cerbero_client import CerberoClient from src.live.strategy_loader import load_strategy from src.live.strategy_worker import StrategyWorker +from src.live.pairs_worker import PairsWorker from src.live.signal_engine import SignalEngine from src.live.telegram_notifier import send_telegram @@ -18,9 +19,17 @@ PROJECT_ROOT = Path(__file__).resolve().parents[2] DATA_DIR = PROJECT_ROOT / "data" / "paper_trades" RESOLUTION_MAP = {"15m": "15", "1h": "60", "5m": "5"} +# Deribit ha perp per i major; per gli alt il fallback "{X}-PERPETUAL" passa da Cerbero +# (Bybit/Hyperliquid). La shortabilita'/liquidita' degli alt va verificata in live. INSTRUMENT_MAP = { "BTC": "BTC-PERPETUAL", "ETH": "ETH-PERPETUAL", + "SOL": "SOL-PERPETUAL", + "LTC": "LTC-PERPETUAL", + "ADA": "ADA-PERPETUAL", + "XRP": "XRP-PERPETUAL", + "BNB": "BNB-PERPETUAL", + "DOGE": "DOGE-PERPETUAL", } @@ -130,6 +139,26 @@ def build_workers(config: dict) -> tuple[list[StrategyWorker], list[MLWorkerWrap return regular_workers, ml_workers +def build_pairs_workers(config: dict) -> list[PairsWorker]: + """Crea i PairsWorker (2 gambe) dalla sezione `pairs:` dello YAML.""" + defaults = config.get("defaults", {}) + workers: list[PairsWorker] = [] + for entry in config.get("pairs", []): + if not entry.get("enabled", True): + continue + workers.append(PairsWorker( + asset_a=entry["a"], asset_b=entry["b"], tf=entry.get("tf", "1h"), + params=entry.get("params", {}), + capital=entry.get("capital", defaults.get("capital", 1000)), + position_size=entry.get("position_size", defaults.get("position_size", 0.15)), + leverage=entry.get("leverage", defaults.get("leverage", 3)), + fee_rt=entry.get("fee_rt", 0.001), + name=entry.get("name", "PR01_pairs_reversion"), + data_dir=DATA_DIR, + )) + return workers + + def run(): config_path = PROJECT_ROOT / "strategies.yml" if not config_path.exists(): @@ -143,7 +172,8 @@ def run(): train_lookback_days = 365 regular_workers, ml_workers = build_workers(config) - all_worker_count = len(regular_workers) + len(ml_workers) + pairs_workers = build_pairs_workers(config) + all_worker_count = len(regular_workers) + len(ml_workers) + len(pairs_workers) if all_worker_count == 0: print("Nessuna strategia abilitata in strategies.yml") @@ -162,6 +192,8 @@ def run(): print(f" • {w.status_summary}") for mw in ml_workers: print(f" • {mw.worker.status_summary} [ML]") + for pw in pairs_workers: + print(f" • {pw.status_summary} [PAIRS]") send_telegram(f"🚀 Multi-Strategy avviato: {all_worker_count} strategie") @@ -172,6 +204,9 @@ def run(): keys.add((w.asset, w.tf)) for mw in ml_workers: keys.add((mw.worker.asset, mw.worker.tf)) + for pw in pairs_workers: # entrambe le gambe del pair + keys.add((pw.asset_a, pw.tf)) + keys.add((pw.asset_b, pw.tf)) return keys # Training iniziale ML @@ -253,6 +288,15 @@ def run(): except Exception as e: print(f" [{mw.worker.worker_id}] ERRORE: {e}") + # Tick pairs workers (2 gambe) + for pw in pairs_workers: + ka, kb = (pw.asset_a, pw.tf), (pw.asset_b, pw.tf) + if ka in candle_cache and kb in candle_cache: + try: + pw.tick(candle_cache[ka], candle_cache[kb]) + except Exception as e: + print(f" [{pw.worker_id}] ERRORE: {e}") + # Status periodico now = datetime.now(timezone.utc) if now.minute == 0 and now.second < poll_seconds: @@ -261,6 +305,8 @@ def run(): lines.append(f" {w.status_summary}") for mw in ml_workers: lines.append(f" {mw.worker.status_summary} [ML]") + for pw in pairs_workers: + lines.append(f" {pw.status_summary} [PAIRS]") send_telegram("\n".join(lines)) except KeyboardInterrupt: @@ -277,6 +323,8 @@ def run(): if df is not None and not df.empty: mw.worker._close_position(float(df["close"].iloc[-1]), "shutdown") mw.worker._save_state() + for pw in pairs_workers: # salva stato; non forzo la chiusura a 2 gambe + pw._save_state() send_telegram("🛑 Multi-Strategy arrestato") break except Exception as e: diff --git a/src/live/pairs_worker.py b/src/live/pairs_worker.py new file mode 100644 index 0000000..56a55a6 --- /dev/null +++ b/src/live/pairs_worker.py @@ -0,0 +1,215 @@ +"""PairsWorker — paper trading a 2 GAMBE per la famiglia PR01 (spread reversion). + +Market-neutral: long asset A / short asset B (o viceversa) sullo z-score del log-ratio. +Distinto dallo StrategyWorker single-leg: gestisce due strumenti, due prezzi di +ingresso, e conta le fee su ENTRAMBE le gambe (2*fee_rt*lev = 0.20% RT/coppia con +fee_rt=0.001). Semantica identica al backtest scripts/analysis/pairs_research.pairs_sim: + + r[i] = log(closeA[i]/closeB[i]); z[i] = (r[i]-SMA_n(r)[i]) / STD_n(r)[i] (causale) + ENTRY a close[i]: z<=-z_in -> LONG ratio (long A / short B); z>=+z_in -> SHORT ratio + EXIT: |z| <= z_exit (rientro) oppure time-limit max_bars + filtro candele sporche: salta l'ingresso se |dr[i]| > jump_max + PnL = (retA - retB) * direction * lev - 2*fee_rt*lev (notional uguale per gamba) + +Stato persistente (resume al restart) e log come StrategyWorker. +""" +from __future__ import annotations + +import json +from datetime import datetime, timezone +from pathlib import Path + +import numpy as np +import pandas as pd + +from src.live.telegram_notifier import notify_event + + +class PairsWorker: + def __init__( + self, + asset_a: str, + asset_b: str, + tf: str, + params: dict | None = None, + capital: float = 1000.0, + position_size: float = 0.15, + leverage: float = 3.0, + fee_rt: float = 0.001, # per gamba RT; la coppia paga 2x + name: str = "PR01_pairs_reversion", + data_dir: Path = Path("data/paper_trades"), + ): + self.asset_a = asset_a + self.asset_b = asset_b + self.tf = tf + self.name = name + p = params or {} + self.n = int(p.get("n", 50)) + self.z_in = float(p.get("z_in", 2.0)) + self.z_exit = float(p.get("z_exit", 0.75)) + self.max_bars = int(p.get("max_bars", 72)) + self.jump_max = float(p.get("jump_max", 0.08)) + + self.initial_capital = capital + self.position_size = position_size + self.leverage = leverage + self.fee_rt = fee_rt + + self.worker_id = f"{name}__{asset_a}_{asset_b}__{tf}" + self.work_dir = data_dir / self.worker_id + self.work_dir.mkdir(parents=True, exist_ok=True) + self.trades_path = self.work_dir / "trades.jsonl" + self.status_path = self.work_dir / "status.json" + + self.capital = capital + self.in_position = False + self.direction = 0 # +1 long ratio (long A/short B), -1 short ratio + self.entry_a = 0.0 + self.entry_b = 0.0 + self.entry_z = 0.0 + self.entry_time = "" + self.bars_held = 0 + self.total_trades = 0 + self.total_wins = 0 + self.last_bar_ts = 0 + self.started_at = datetime.now(timezone.utc).isoformat() + + self._load_state() + self._save_state() + + # ---------------- persistenza ---------------- + def _load_state(self): + if not self.status_path.exists(): + self._log("INIT", {"capital": self.capital, "pair": f"{self.asset_a}/{self.asset_b}", + "tf": self.tf, "params": {"n": self.n, "z_in": self.z_in, + "z_exit": self.z_exit, "max_bars": self.max_bars}}) + return + with open(self.status_path) as f: + s = json.load(f) + self.capital = s.get("capital", self.initial_capital) + self.in_position = s.get("in_position", False) + self.direction = s.get("direction", 0) + self.entry_a = s.get("entry_a", 0.0) + self.entry_b = s.get("entry_b", 0.0) + self.entry_z = s.get("entry_z", 0.0) + self.entry_time = s.get("entry_time", "") + self.bars_held = s.get("bars_held", 0) + self.total_trades = s.get("total_trades", 0) + self.total_wins = s.get("total_wins", 0) + self.last_bar_ts = s.get("last_bar_ts", 0) + self.started_at = s.get("started_at", self.started_at) + self._log("RESUME", {"capital": round(self.capital, 2), + "total_trades": self.total_trades, "in_position": self.in_position}) + + def _save_state(self): + state = { + "capital": round(self.capital, 2), "in_position": self.in_position, + "direction": self.direction, "entry_a": self.entry_a, "entry_b": self.entry_b, + "entry_z": round(self.entry_z, 4), "entry_time": self.entry_time, + "bars_held": self.bars_held, "total_trades": self.total_trades, + "total_wins": self.total_wins, "last_bar_ts": self.last_bar_ts, + "started_at": self.started_at, "last_update": datetime.now(timezone.utc).isoformat(), + } + with open(self.status_path, "w") as f: + json.dump(state, f, indent=2) + + def _log(self, event: str, data: dict | None = None): + entry = {"ts": datetime.now(timezone.utc).isoformat(), "worker": self.worker_id, + "event": event, **(data or {})} + with open(self.trades_path, "a") as f: + f.write(json.dumps(entry) + "\n") + print(f" [{self.worker_id}] {event}: {json.dumps(data or {}, default=str)}") + + def _notify(self, event: str, data: dict | None = None): + notify_event(event, {"worker": self.worker_id, **(data or {})}) + + # ---------------- segnale ---------------- + def _zscore(self, ca: np.ndarray, cb: np.ndarray) -> tuple[np.ndarray, np.ndarray]: + r = np.log(ca / cb) + ma = pd.Series(r).rolling(self.n).mean().values + sd = pd.Series(r).rolling(self.n).std().values + z = (r - ma) / np.where(sd == 0, np.nan, sd) + dr = np.abs(np.diff(r, prepend=r[0])) + return z, dr + + # ---------------- trading ---------------- + def _open(self, d: int, ca: float, cb: float, z: float): + self.in_position = True + self.direction = d + self.entry_a, self.entry_b, self.entry_z = ca, cb, z + self.entry_time = datetime.now(timezone.utc).isoformat() + self.bars_held = 0 + data = {"direction": "long_ratio" if d == 1 else "short_ratio", + "long_leg": self.asset_a if d == 1 else self.asset_b, + "short_leg": self.asset_b if d == 1 else self.asset_a, + "entry_a": round(ca, 4), "entry_b": round(cb, 4), "z": round(z, 3), + "capital": round(self.capital, 2)} + self._log("OPEN", data); self._notify("OPENED", data) + + def _close(self, ca: float, cb: float, z: float, reason: str): + if not self.in_position: + return + ret_a = (ca - self.entry_a) / self.entry_a + ret_b = (cb - self.entry_b) / self.entry_b + gross = (ret_a - ret_b) * self.direction * self.leverage + fee = 2 * self.fee_rt * self.leverage # 2 gambe + net = gross - fee + pnl = self.capital * self.position_size * net + self.capital = max(self.capital + pnl, 0.0) + is_win = net > 0 + self.total_trades += 1 + self.total_wins += is_win + acc = self.total_wins / self.total_trades * 100 if self.total_trades else 0 + data = {"reason": reason, "exit_a": round(ca, 4), "exit_b": round(cb, 4), + "z": round(z, 3), "gross_ret": round(gross * 100, 3), "fee": round(fee * 100, 3), + "net_return": round(net * 100, 3), "pnl": round(pnl, 2), + "capital": round(self.capital, 2), "bars_held": self.bars_held, + "win": bool(is_win), "total_trades": self.total_trades, "accuracy": round(acc, 1)} + self._log("CLOSE", data); self._notify("CLOSED", data) + self.in_position = False + self.direction = 0 + self.entry_a = self.entry_b = self.entry_z = 0.0 + self.bars_held = 0 + + def tick(self, df_a: pd.DataFrame, df_b: pd.DataFrame): + """Chiamato ad ogni poll con gli OHLCV aggiornati delle due gambe.""" + if df_a is None or df_b is None or df_a.empty or df_b.empty: + return + m = df_a[["timestamp", "close"]].rename(columns={"close": "ca"}).merge( + df_b[["timestamp", "close"]].rename(columns={"close": "cb"}), on="timestamp", how="inner" + ).sort_values("timestamp").reset_index(drop=True) + if len(m) < self.n + 2: + return + ca, cb = m["ca"].values, m["cb"].values + z, dr = self._zscore(ca, cb) + i = len(m) - 1 + cur_ts = int(m["timestamp"].iloc[i]) + zi = z[i] + if np.isnan(zi): + self._save_state(); return + + if self.in_position: + if cur_ts > self.last_bar_ts: + self.bars_held += 1 + self.last_bar_ts = cur_ts + if abs(zi) <= self.z_exit: + self._close(float(ca[i]), float(cb[i]), float(zi), "mean_revert") + elif self.bars_held >= self.max_bars: + self._close(float(ca[i]), float(cb[i]), float(zi), "time_limit") + self._save_state() + return + + # flat: cerca ingresso (no look-ahead: z[i] usa solo dati <= i) + if dr[i] <= self.jump_max: + if zi <= -self.z_in: + self._open(1, float(ca[i]), float(cb[i]), float(zi)); self.last_bar_ts = cur_ts + elif zi >= self.z_in: + self._open(-1, float(ca[i]), float(cb[i]), float(zi)); self.last_bar_ts = cur_ts + self._save_state() + + @property + def status_summary(self) -> str: + acc = self.total_wins / self.total_trades * 100 if self.total_trades else 0 + pos = ("LONG " + self.asset_a if self.direction == 1 + else "SHORT " + self.asset_a if self.direction == -1 else "FLAT") + return (f"{self.worker_id}: €{self.capital:.0f} | {self.total_trades}t {acc:.0f}% | {pos}") diff --git a/strategies.yml b/strategies.yml index 5bb8b4a..54300ac 100644 --- a/strategies.yml +++ b/strategies.yml @@ -90,3 +90,41 @@ strategies: max_bars: 24 trend_max: 3.0 # salta fade contro trend estremo (|close-EMA200|/ATR>3): Acc+ DD- ema_long: 200 + +# --------------------------------------------------------------------------- +# PR01 — PAIRS market-neutral spread reversion (worker a 2 GAMBE: src/live/pairs_worker.py) +# Config UNIVERSALE n50 z2 zx0.75 mb72 (anti-overfit, validata walk-forward). +# fee_rt 0.001/gamba -> 0.20% RT/coppia. ATTENZIONE: richiede perp shortabile per +# entrambe le gambe; su alt (LTC/SOL/ADA) verificare liquidita'/fill prima del live reale. +pairs: + - name: PR01_pairs_reversion + a: ETH + b: BTC + tf: 1h + enabled: true + params: {n: 50, z_in: 2.0, z_exit: 0.75, max_bars: 72, jump_max: 0.08} + - name: PR01_pairs_reversion + a: LTC + b: ETH + tf: 1h + enabled: true + params: {n: 50, z_in: 2.0, z_exit: 0.75, max_bars: 72, jump_max: 0.08} + - name: PR01_pairs_reversion + a: ADA + b: ETH + tf: 1h + enabled: true + params: {n: 50, z_in: 2.0, z_exit: 0.75, max_bars: 72, jump_max: 0.08} + - name: PR01_pairs_reversion + a: BTC + b: LTC + tf: 1h + enabled: true + params: {n: 50, z_in: 2.0, z_exit: 0.75, max_bars: 72, jump_max: 0.08} + # ETH/SOL: la piu' debole (DD ~63%, storia SOL corta) -> peso ridotto consigliato + - name: PR01_pairs_reversion + a: ETH + b: SOL + tf: 1h + enabled: true + params: {n: 50, z_in: 2.0, z_exit: 0.75, max_bars: 72, jump_max: 0.08}