From 2bf4f9cceb4f006c52365a126cae0e3d059a7d91 Mon Sep 17 00:00:00 2001 From: "Adriano (goldrake)" Date: Sun, 27 Sep 2026 13:37:36 +0000 Subject: [PATCH] live/deribit: position_usd legge il segno da direction, non dal solo size (#17) Il gateway manda size/size_currency in assoluto e il segno a parte in direction: letta com'era, uno short risultava un long e close()/ensure_disaster_sl decidevano lato ordine e prezzo dello stop dal segno sbagliato. Co-Authored-By: Claude Sonnet 5 --- src/live/deribit.py | 15 +++++- tests/test_execution_u1.py | 108 +++++++++++++++++++++++++++++++++++++ 2 files changed, 121 insertions(+), 2 deletions(-) create mode 100644 tests/test_execution_u1.py diff --git a/src/live/deribit.py b/src/live/deribit.py index e4f751d..5a142bb 100644 --- a/src/live/deribit.py +++ b/src/live/deribit.py @@ -236,11 +236,22 @@ class DeribitRead: return out.get("positions", []) if isinstance(out, dict) else [] def position_usd(self, instrument: str) -> float: - """Size netta (USD notional, segno = direzione) della posizione su `instrument`. 0 se flat.""" + """Size netta (USD notional, segno = direzione) della posizione su `instrument`. 0 se flat. + + ⚠️ Il gateway manda `size`/`size_currency` in ASSOLUTO e il segno a parte in `direction` + (#17): letta com'era, uno short risultava un long, e close()/ensure_disaster_sl decidevano + lato ordine e prezzo dello stop dal segno sbagliato. `direction` assente o `"zero"` -> ci si + fida di `size` cosi' com'e' (compatibile con un gateway che un giorno mandasse gia' il segno).""" cur = _CONTRACT[instrument]["settle"] for p in self.positions(cur): if p.get("instrument_name") == instrument or p.get("instrument") == instrument: - return float(p.get("size") or p.get("size_currency") or 0.0) + s = float(p.get("size") or p.get("size_currency") or 0.0) + direction = str(p.get("direction") or "").lower() + if direction in ("sell", "short"): + return -abs(s) + if direction in ("buy", "long"): + return abs(s) + return s return 0.0 def trade_history(self, instrument: str, limit: int = 20) -> list[dict]: diff --git a/tests/test_execution_u1.py b/tests/test_execution_u1.py new file mode 100644 index 0000000..5e266ea --- /dev/null +++ b/tests/test_execution_u1.py @@ -0,0 +1,108 @@ +"""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. + +Nessun test tocca la rete. +""" +from __future__ import annotations + +from src.live.deribit import DeribitRead +from src.live.execution import DeribitTrader, Fill + +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