"""Tre riparazioni sul percorso con soldi veri (unita' U1, 2026-09-27), provate contro il codice che dovrebbe farle — non discusse. #17. `DeribitRead.position_usd` leggeva `size`/`size_currency` in ASSOLUTO e ignorava `direction`: uno short risultava un long, e close()/ensure_disaster_sl decidevano lato ordine e prezzo dello stop dal segno sbagliato. #18. Tetto per ordine d'apertura: `MAX_AMOUNT` era un tetto FISSO uguale per tutti i conti; ora il libro deriva il proprio tetto dal cap di config via `set_open_cap()`. #24. `DeribitRead.open_orders` ingoiava gli errori di ENTRAMBE le chiamate ('all' e 'trigger_all') e ritornava []: un bracket disaster-SL perso passava per "nessun bracket". Ora solleva, e `ensure_disaster_sl` distingue lo stato "unknown" (letture fallite, non si cancella ne' si piazza) da "naked" (protezione tolta e non rimessa). Nessun test tocca la rete. """ from __future__ import annotations import pytest from src.live.deribit import DeribitRead from src.live.execution import MAX_AMOUNT, DeribitTrader, Fill, GuardrailError ETH = "ETH_USDC-PERPETUAL" # ================================================================================================= # #17 — position_usd: il segno viene da `direction`, non assunto dal solo `size`. # ================================================================================================= class _FakeRead(DeribitRead): """DeribitRead senza rete: `positions()` finto, per esercitare `position_usd` con la forma REALE del gateway (size in assoluto + direction a parte).""" def __init__(self, raw: list[dict]) -> None: self._raw = raw def positions(self, currency): return self._raw def test_position_usd_sell_e_negativo(): r = _FakeRead([{"instrument_name": ETH, "size": 560, "direction": "sell"}]) assert r.position_usd(ETH) == -560.0 def test_position_usd_buy_e_positivo(): r = _FakeRead([{"instrument_name": ETH, "size": 560, "direction": "buy"}]) assert r.position_usd(ETH) == 560.0 def test_position_usd_senza_direction_si_fida_del_segno_di_size(): """Compatibile con un gateway che un giorno mandasse gia' il segno in `size`.""" r = _FakeRead([{"instrument_name": ETH, "size": -3}]) assert r.position_usd(ETH) == -3.0 # ================================================================================================= # Verifica a valle di #17: close() e ensure_disaster_sl() decidono lato/stop da `pos > 0`. # Si mocka `positions()` (dati grezzi del gateway), NON `position_usd()`: cosi' si esercita # davvero la pipeline positions() -> position_usd() -> close()/ensure_disaster_sl(). # ================================================================================================= class _FakeTraderShort(DeribitTrader): """DeribitTrader senza rete: posizione SHORT letta come il gateway la manda davvero, mark fisso, `_submit` registra la chiamata invece di piazzare l'ordine.""" def __init__(self, raw_positions: list[dict], mark: float = 2694.0) -> None: self._open_cap: dict[str, float] = {} self._raw = raw_positions self._mark = mark self.submitted: list[dict] = [] self.cancelled: list[str] = [] def positions(self, currency): return self._raw def mark_price(self, instrument): return self._mark def open_orders(self, instrument): return [] def cancel_order(self, order_id): self.cancelled.append(order_id) return {} def _submit(self, instrument, side, amount, *, reduce_only, label, order_type="market", price=None): self.submitted.append(dict(instrument=instrument, side=side, amount=amount, reduce_only=reduce_only, label=label, order_type=order_type, price=price)) return Fill(instrument=instrument, side=side, amount=amount, filled=amount, price=price, fee_usdc=0.0, order_id="1", state="filled", verified=True) def _short_560(): return _FakeTraderShort([{"instrument_name": ETH, "size": 560, "direction": "sell"}]) def test_close_su_short_usa_side_buy_reduce_only(): t = _short_560() f = t.close(ETH) assert f is not None assert len(t.submitted) == 1 call = t.submitted[0] assert call["side"] == "buy" and call["reduce_only"] is True assert call["label"] == "tp01-close" and call["amount"] > 0 def test_ensure_disaster_sl_su_short_mette_lo_stop_sopra_il_mark_lato_buy(): t = _short_560() ds = t.ensure_disaster_sl(ETH, 0.30) assert ds["state"] == "placed" assert len(t.submitted) == 1 call = t.submitted[0] assert call["side"] == "buy" assert call["order_type"] == "stop_market" assert call["price"] > t._mark # stop SOPRA il mark per uno short # ================================================================================================= # #18 — set_open_cap(): tetto di apertura per strumento, derivato dal cap di config. # ================================================================================================= class _FakeTraderCap(DeribitTrader): """DeribitTrader senza rete: `_post` risponde con un ordine fillato finto, cosi' la parte di _submit DOPO il guardrail (che qui non ci interessa) non tocca la rete.""" def __init__(self) -> None: self._open_cap: dict[str, float] = {} def _post(self, path, payload): amt = payload["amount"] return {"order": {"order_id": "1", "order_state": "filled", "filled_amount": amt}, "trades": [{"price": 1.0, "amount": amt, "fee": 0.0}]} def test_set_open_cap_valida_gli_argomenti(): t = _FakeTraderCap() with pytest.raises(ValueError): t.set_open_cap(ETH, 0.0, 2694.0) with pytest.raises(ValueError): t.set_open_cap(ETH, 2291.5, 0.0) def test_set_open_cap_apertura_entro_il_tetto_non_solleva(): """Equity 4583, cap/asset 2291.5, ETH mark 2694 -> apertura di 0,2807 ETH e' ben dentro il tetto (1,10 * 2291.5 / 2694 ~= 0,9355 ETH).""" t = _FakeTraderCap() t.set_open_cap(ETH, 2291.5, 2694.0) t._submit(ETH, "buy", 0.2807, reduce_only=False, label="book-open") # non deve solllevare def test_set_open_cap_3x_il_tetto_solleva_guardrail(): t = _FakeTraderCap() t.set_open_cap(ETH, 2291.5, 2694.0) cap = t._open_cap[ETH] with pytest.raises(GuardrailError): t._submit(ETH, "buy", cap * 3, reduce_only=False, label="book-open") def test_senza_set_open_cap_resta_il_default_max_amount(): t = _FakeTraderCap() dentro = MAX_AMOUNT[ETH] * 0.5 t._submit(ETH, "buy", dentro, reduce_only=False, label="book-open") # non deve solllevare with pytest.raises(GuardrailError): t._submit(ETH, "buy", MAX_AMOUNT[ETH] * 3, reduce_only=False, label="book-open") # ================================================================================================= # #24 — open_orders non ingoia gli errori; ensure_disaster_sl diventa "unknown", non a caso. # ================================================================================================= class _FakeOpenOrders(DeribitRead): """DeribitRead senza rete: 'all' risponde vuoto, 'trigger_all' solleva — replica un bracket disaster-SL perso su un solo dei due tipi di chiamata.""" def __init__(self) -> None: pass # niente token, niente rete def _post(self, path, payload): if payload.get("type") == "trigger_all": raise RuntimeError("502 Server Error: Bad Gateway") return {"result": []} def test_open_orders_trigger_all_solleva_runtime_error(): r = _FakeOpenOrders() with pytest.raises(RuntimeError, match="open_orders illeggibile"): r.open_orders(ETH) class _FakeTraderOpenOrdersRompe(DeribitTrader): """position_usd legge una posizione REALE, ma open_orders solleva: ensure_disaster_sl non deve decidere su meta' dei dati.""" def __init__(self) -> None: self._open_cap: dict[str, float] = {} self.submitted: list[dict] = [] self.cancelled: list[str] = [] def positions(self, currency): return [{"instrument_name": ETH, "size": 560, "direction": "sell"}] def mark_price(self, instrument): return 2694.0 def open_orders(self, instrument): raise RuntimeError("open_orders illeggibile: RuntimeError: 502 Server Error: Bad Gateway") def cancel_order(self, order_id): self.cancelled.append(order_id) return {} def _submit(self, instrument, side, amount, *, reduce_only, label, order_type="market", price=None): self.submitted.append(dict(side=side, reduce_only=reduce_only)) return Fill(instrument=instrument, side=side, amount=amount, filled=amount, price=price, fee_usdc=0.0, order_id="1", state="filled", verified=True) def test_ensure_disaster_sl_con_open_orders_che_solleva_diventa_unknown_senza_agire(): t = _FakeTraderOpenOrdersRompe() ds = t.ensure_disaster_sl(ETH, 0.30) assert ds["state"] == "unknown" assert "502" in ds["notes"] assert t.cancelled == [] # nessuna cancel assert t.submitted == [] # nessun piazzamento