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 <noreply@anthropic.com>
This commit is contained in:
+13
-2
@@ -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]:
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user