14522262e6
Reset del progetto su fondamenta verificate dopo la scoperta che l'intera libreria "validata OOS" era artefatto di feed contaminato (print fantasma del feed Cerbero TESTNET + storico Binance/USDT). - Storico ricostruito da Deribit MAINNET (ccxt pubblico, tokenless) e CERTIFICATO (certify_feed.py): BTC/ETH puliti su TUTTA la storia (mediana 2-6 bps vs Coinbase USD), integrita' OHLC + coerenza resample (maxΔ 0.00) + cross-venue OK. Alt esclusi (illiquidi/divergenti: LTC/DOGE 50-82% barre flat; XRP/BNB non certificabili). - Verdetto sul feed pulito: FADE / PAIRS / XS01 / TSM01 morti (ogni portafoglio Sharpe -2.3..-3.0, DD ~40%); solo SH01 e frammenti HONEST con segnale residuo, da ri-validare in isolamento. - Cleanup "restart pulito": strategie, stack live (src/live, src/portfolio, runner/executor, yml, docker), ~100 script ricerca/gate, waste/games/ portfolios, dati non certificati + cache e 60+ diari -> archiviati in Old/ (preservati, non cancellati). Diario consolidato in un unico documento. - Skeleton ricerca tenuto: Strategy ABC + indicatori + src/fractal + src/backtest/engine + load_data; tool dati certificati (rebuild_history, certify_feed, audit_feed, multi_source_check). - Universo dati ATTIVO: solo BTC/ETH (5m/15m/1h); guardrail fisico (load_data su alt -> FileNotFoundError). Esecuzione DISABILITATA, conto flat. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
102 lines
4.4 KiB
Python
102 lines
4.4 KiB
Python
"""PairsWorker esecuzione reale a 2 gambe (shadow): apertura/chiusura, leg-risk unwind,
|
|
ledger reale parallelo. Executor finto, nessuna rete."""
|
|
from types import SimpleNamespace
|
|
|
|
import numpy as np
|
|
import pandas as pd
|
|
import pytest
|
|
|
|
from src.live.execution import Fill, PairFill
|
|
from src.live.pairs_worker import PairsWorker
|
|
|
|
|
|
class FakePairsExec:
|
|
"""Simula PairsExecutionClient: fill deterministici, con possibilità di leg-fail."""
|
|
def __init__(self, fail_leg=None):
|
|
self.fail_leg = fail_leg # None | 'a' | 'b' : quale gamba NON filla all'open
|
|
self.opened = []
|
|
self.closed = []
|
|
self.unwound = []
|
|
|
|
def _fill(self, inst, side, price, amt=1.0, ok=True):
|
|
return Fill(inst, side, 0.0, amt if ok else 0.0, price if ok else None,
|
|
0.0, 0.05 if ok else 0.0, "oid" if ok else None,
|
|
"filled" if ok else "error", ok,
|
|
filled_amount=amt if ok else 0.0)
|
|
|
|
def open_pair(self, inst_a, inst_b, direction, notional, label=None):
|
|
side_a = "buy" if direction == 1 else "sell"
|
|
side_b = "sell" if direction == 1 else "buy"
|
|
ok_a = self.fail_leg != 'a'
|
|
ok_b = self.fail_leg != 'b'
|
|
fa = self._fill(inst_a, side_a, 100.0, ok=ok_a)
|
|
fb = self._fill(inst_b, side_b, 50.0, ok=ok_b)
|
|
self.opened.append((inst_a, inst_b, direction))
|
|
if ok_a and ok_b:
|
|
return PairFill(True, fa, fb)
|
|
if ok_a and fa.amount > 0:
|
|
self.unwound.append(inst_a)
|
|
if ok_b and fb.amount > 0:
|
|
self.unwound.append(inst_b)
|
|
return PairFill(False, fa, fb, unwound=bool(self.unwound), notes="leg-fail")
|
|
|
|
def close_pair(self, inst_a, inst_b, side_a, side_b, amt_a, amt_b, label=None):
|
|
opp_a = "sell" if side_a == "buy" else "buy"
|
|
opp_b = "sell" if side_b == "buy" else "buy"
|
|
self.closed.append((inst_a, inst_b))
|
|
# prezzi di uscita: A salito a 102, B fermo a 50 -> long-A/short-B guadagna
|
|
return PairFill(True, self._fill(inst_a, opp_a, 102.0, amt_a),
|
|
self._fill(inst_b, opp_b, 50.0, amt_b))
|
|
|
|
|
|
INST = {"ETH": "ETH_USDC-PERPETUAL", "BTC": "BTC_USDC-PERPETUAL"}
|
|
|
|
|
|
def _worker(tmp_path, fake, monkeypatch, notified=None):
|
|
if notified is not None:
|
|
monkeypatch.setattr("src.live.pairs_worker.notify_event",
|
|
lambda ev, data=None: notified.append(ev))
|
|
return PairsWorker("ETH", "BTC", "1h", capital=200.0, position_size=0.2, leverage=2.0,
|
|
data_dir=tmp_path, executor=fake, exec_instruments=INST)
|
|
|
|
|
|
def test_execution_enabled_wired(tmp_path):
|
|
w = _worker(tmp_path, FakePairsExec(), pytest.MonkeyPatch())
|
|
assert w.execution_enabled and w.inst_a == "ETH_USDC-PERPETUAL" and w.inst_b == "BTC_USDC-PERPETUAL"
|
|
|
|
|
|
def test_real_open_and_close_pair(tmp_path, monkeypatch):
|
|
notified = []
|
|
fake = FakePairsExec()
|
|
w = _worker(tmp_path, fake, monkeypatch, notified)
|
|
w._open(1, 100.0, 50.0, -2.5) # long ratio
|
|
assert w.real_in_position and w.real_dir == 1
|
|
assert w.real_side_a == "buy" and w.real_side_b == "sell"
|
|
assert fake.opened and "REAL_EXEC_LIVE" in notified
|
|
cap0 = w.real_capital
|
|
w._close(102.0, 50.0, 0.3, "mean_revert")
|
|
assert not w.real_in_position and fake.closed
|
|
assert w.real_capital > cap0 # A +2% / B 0 -> long-A/short-B in utile
|
|
assert w.real_trades == 1
|
|
|
|
|
|
def test_leg_fail_unwinds_and_no_position(tmp_path, monkeypatch):
|
|
notified = []
|
|
fake = FakePairsExec(fail_leg='b') # gamba B non filla
|
|
w = _worker(tmp_path, fake, monkeypatch, notified)
|
|
w._open(1, 100.0, 50.0, -2.5)
|
|
assert not w.real_in_position # niente posizione reale
|
|
assert "ETH_USDC-PERPETUAL" in fake.unwound # la gamba A fillata e' stata richiusa
|
|
assert "REAL_OPEN_FAIL" in notified
|
|
|
|
|
|
def test_real_ledger_persists_and_resumes(tmp_path, monkeypatch):
|
|
fake = FakePairsExec()
|
|
w = _worker(tmp_path, fake, monkeypatch, [])
|
|
w._open(-1, 100.0, 50.0, 2.5) # short ratio
|
|
assert w.real_in_position and w.real_dir == -1
|
|
w2 = PairsWorker("ETH", "BTC", "1h", capital=200.0, position_size=0.2, leverage=2.0,
|
|
data_dir=tmp_path, executor=fake, exec_instruments=INST)
|
|
assert w2.real_in_position and w2.real_dir == -1
|
|
assert w2.real_side_a == "sell" and w2.real_amount_a > 0
|