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>
152 lines
6.3 KiB
Python
152 lines
6.3 KiB
Python
"""_real_close: cancel del disaster-SL (retry + alert orfano) e alert REAL_DIVERGENCE.
|
|
|
|
Usa un executor finto: nessuna rete, nessun ordine reale.
|
|
"""
|
|
from types import SimpleNamespace
|
|
|
|
import pytest
|
|
|
|
from src.live.execution import Fill
|
|
from src.live.strategy_worker import StrategyWorker
|
|
|
|
|
|
class FakeExec:
|
|
verify_polls = 1
|
|
verify_sleep = 0.0
|
|
disaster_sl_pct = 0.30
|
|
|
|
def __init__(self, cancel_responses):
|
|
self.cancel_calls = []
|
|
self._responses = list(cancel_responses)
|
|
|
|
def cancel_order(self, oid):
|
|
self.cancel_calls.append(oid)
|
|
return self._responses.pop(0) if self._responses else {"state": "error", "error": "boom"}
|
|
|
|
def resting_fills(self, instrument, oid):
|
|
return 0.0, None, 0.0
|
|
|
|
def close_amount(self, instrument, side, amount, label=None):
|
|
return Fill(instrument, "sell", 0.0, amount, 100.0, 0.0, 0.0,
|
|
"oid-close", "filled", True, filled_amount=amount)
|
|
|
|
|
|
class FakeFullExec(FakeExec):
|
|
"""Aggiunge open/place_tp_limit/place_disaster_sl per testare il ciclo SH01 completo."""
|
|
def __init__(self):
|
|
super().__init__([{"state": "untriggered"}])
|
|
self.tp_calls = 0
|
|
self.dsl_calls = 0
|
|
self.close_amounts = []
|
|
|
|
def open(self, instrument, side, notional, label=None):
|
|
amt = round(notional / 100.0, 6)
|
|
return Fill(instrument, side, notional, amt, 100.0, 0.0, 0.05, "oid-open", "filled", True)
|
|
|
|
def place_tp_limit(self, *a, **k):
|
|
self.tp_calls += 1 # NON deve essere chiamato per SH01 (no TP)
|
|
return Fill("x", "sell", 0.0, 0.0, None, 0.0, 0.0, None, None, False)
|
|
|
|
def place_disaster_sl(self, instrument, side, amount, stop, label=None):
|
|
self.dsl_calls += 1
|
|
return Fill(instrument, "sell", 0.0, amount, None, 0.0, 0.0, "dsl-1", "untriggered", True)
|
|
|
|
def close_amount(self, instrument, side, amount, label=None):
|
|
self.close_amounts.append(amount)
|
|
return Fill(instrument, "sell", 0.0, amount, 105.0, 0.0, 0.05, "oid-close",
|
|
"filled", True, filled_amount=amount)
|
|
|
|
|
|
def test_sh01_real_open_close_no_tp(tmp_path, monkeypatch):
|
|
"""SH01 (no TP/SL, exit a orizzonte): apre reale, NON piazza TP, piazza disaster,
|
|
chiude TUTTA la quota a market reduce-only."""
|
|
from src.live.strategy_worker import StrategyWorker
|
|
from src.strategies.base import Signal
|
|
notified = []
|
|
monkeypatch.setattr("src.live.strategy_worker.notify_event",
|
|
lambda ev, data=None: notified.append(ev))
|
|
fake = FakeFullExec()
|
|
w = StrategyWorker(strategy=SimpleNamespace(name="SH01_shape_ml", fee_rt=0.001),
|
|
asset="BTC", tf="1h", capital=100.0, position_size=0.5, leverage=2.0,
|
|
data_dir=tmp_path, executor=fake, exec_instrument="BTC_USDC-PERPETUAL")
|
|
w._open_position(Signal(idx=0, direction=1, entry_price=100.0,
|
|
metadata={"max_bars": 12}), 100.0) # niente tp/sl
|
|
assert w.real_in_position and fake.tp_calls == 0 # nessun TP per SH01
|
|
assert fake.dsl_calls == 1 and w.real_dsl_order_id # disaster bracket sì
|
|
amt = w.real_amount
|
|
w._close_position(105.0, "time_limit")
|
|
assert not w.real_in_position
|
|
assert fake.close_amounts == [amt] # chiude TUTTA la quota a market
|
|
|
|
|
|
def _worker(tmp_path, fake, monkeypatch, notified):
|
|
monkeypatch.setattr("src.live.strategy_worker.notify_event",
|
|
lambda ev, data=None: notified.append(ev))
|
|
w = StrategyWorker(strategy=SimpleNamespace(name="FAKE", fee_rt=0.001),
|
|
asset="BTC", tf="1h", capital=100.0, data_dir=tmp_path,
|
|
executor=fake, exec_instrument="BTC_USDC-PERPETUAL")
|
|
w.real_in_position = True
|
|
w.real_side = "buy"
|
|
w.real_amount = 0.001
|
|
w.real_entry_price = 100.0
|
|
w.real_entry_notional = 0.1
|
|
w.real_dsl_order_id = "DSL-1"
|
|
return w
|
|
|
|
|
|
def test_dsl_cancel_untriggered_is_success(tmp_path, monkeypatch):
|
|
notified = []
|
|
fake = FakeExec([{"order_id": "DSL-1", "state": "untriggered"}])
|
|
w = _worker(tmp_path, fake, monkeypatch, notified)
|
|
w._real_close(100.0, "time_limit", 0.0)
|
|
assert fake.cancel_calls == ["DSL-1"] # nessun retry
|
|
assert "REAL_DSL_CANCEL_FAIL" not in notified
|
|
assert w.real_dsl_order_id == ""
|
|
|
|
|
|
def test_dsl_cancel_not_found_logs_but_no_alert(tmp_path, monkeypatch):
|
|
# order_not_found = probabile trigger durante outage: log, NIENTE Telegram, no retry
|
|
notified = []
|
|
fake = FakeExec([{"order_id": "DSL-1", "state": "error", "error": "order_not_found"}])
|
|
w = _worker(tmp_path, fake, monkeypatch, notified)
|
|
w._real_close(100.0, "time_limit", 0.0)
|
|
assert fake.cancel_calls == ["DSL-1"]
|
|
assert "REAL_DSL_CANCEL_FAIL" not in notified
|
|
|
|
|
|
def test_dsl_cancel_transient_error_retries_then_succeeds(tmp_path, monkeypatch):
|
|
notified = []
|
|
fake = FakeExec([{"state": "error", "error": "timeout"},
|
|
{"order_id": "DSL-1", "state": "cancelled"}])
|
|
w = _worker(tmp_path, fake, monkeypatch, notified)
|
|
w._real_close(100.0, "time_limit", 0.0)
|
|
assert fake.cancel_calls == ["DSL-1", "DSL-1"] # retry eseguito
|
|
assert "REAL_DSL_CANCEL_FAIL" not in notified
|
|
|
|
|
|
def test_dsl_cancel_persistent_error_alerts_orphan(tmp_path, monkeypatch):
|
|
notified = []
|
|
fake = FakeExec([{"state": "error", "error": "timeout"},
|
|
{"state": "error", "error": "timeout"}])
|
|
w = _worker(tmp_path, fake, monkeypatch, notified)
|
|
w._real_close(100.0, "time_limit", 0.0)
|
|
assert fake.cancel_calls == ["DSL-1", "DSL-1"]
|
|
assert "REAL_DSL_CANCEL_FAIL" in notified # stop forse orfano -> Telegram
|
|
|
|
|
|
def test_real_divergence_alert_on_close(tmp_path, monkeypatch):
|
|
# fill reale 100.0 vs sim_exit 105.0 -> ~-476bps -> REAL_DIVERGENCE
|
|
notified = []
|
|
fake = FakeExec([{"order_id": "DSL-1", "state": "untriggered"}])
|
|
w = _worker(tmp_path, fake, monkeypatch, notified)
|
|
w._real_close(105.0, "take_profit", 0.5)
|
|
assert "REAL_DIVERGENCE" in notified
|
|
|
|
|
|
def test_no_divergence_alert_when_fill_matches(tmp_path, monkeypatch):
|
|
notified = []
|
|
fake = FakeExec([{"order_id": "DSL-1", "state": "untriggered"}])
|
|
w = _worker(tmp_path, fake, monkeypatch, notified)
|
|
w._real_close(100.05, "take_profit", 0.0) # ~-5bps: sotto soglia
|
|
assert "REAL_DIVERGENCE" not in notified
|