Skip to content

Commit 1b6d357

Browse files
authored
Refine margin balance report for dYdX (#2154)
1 parent c216bd4 commit 1b6d357

File tree

3 files changed

+57
-29
lines changed

3 files changed

+57
-29
lines changed

nautilus_trader/adapters/dydx/data.py

Lines changed: 1 addition & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,6 @@
3232
from nautilus_trader.adapters.dydx.providers import DYDXInstrumentProvider
3333
from nautilus_trader.adapters.dydx.schemas.ws import DYDXWsCandlesChannelData
3434
from nautilus_trader.adapters.dydx.schemas.ws import DYDXWsCandlesSubscribedData
35-
from nautilus_trader.adapters.dydx.schemas.ws import DYDXWsMarketChannelData
36-
from nautilus_trader.adapters.dydx.schemas.ws import DYDXWsMarketSubscribedData
3735
from nautilus_trader.adapters.dydx.schemas.ws import DYDXWsMessageGeneral
3836
from nautilus_trader.adapters.dydx.schemas.ws import DYDXWsOrderbookBatchedData
3937
from nautilus_trader.adapters.dydx.schemas.ws import DYDXWsOrderbookChannelData
@@ -131,8 +129,7 @@ def __init__(
131129
self._decoder_ws_trade = msgspec.json.Decoder(DYDXWsTradeChannelData)
132130
self._decoder_ws_kline = msgspec.json.Decoder(DYDXWsCandlesChannelData)
133131
self._decoder_ws_kline_subscribed = msgspec.json.Decoder(DYDXWsCandlesSubscribedData)
134-
self._decoder_ws_instruments = msgspec.json.Decoder(DYDXWsMarketChannelData)
135-
self._decoder_ws_instruments_subscribed = msgspec.json.Decoder(DYDXWsMarketSubscribedData)
132+
136133
self._ws_client = DYDXWebsocketClient(
137134
clock=clock,
138135
handler=self._handle_ws_message,
@@ -242,8 +239,6 @@ def _handle_ws_message(self, raw: bytes) -> None:
242239
("v4_trades", "subscribed"): self._handle_trade_subscribed,
243240
("v4_candles", "channel_data"): self._handle_kline,
244241
("v4_candles", "subscribed"): self._handle_kline_subscribed,
245-
("v4_markets", "channel_data"): self._handle_markets,
246-
("v4_markets", "subscribed"): self._handle_markets_subscribed,
247242
}
248243
try:
249244
ws_message = self._decoder_ws_msg_general.decode(raw)
@@ -665,24 +660,6 @@ def _handle_kline_unsubscribed(self, msg: DYDXWsMessageGeneral) -> None:
665660
if msg.id is not None:
666661
self._topic_bar_type.pop(msg.id, None)
667662

668-
def _handle_markets(self, raw: bytes) -> None:
669-
try:
670-
msg: DYDXWsMarketChannelData = self._decoder_ws_instruments.decode(raw)
671-
672-
self._log.debug(f"{msg}")
673-
674-
except Exception as e:
675-
self._log.error(f"Failed to parse market data: {raw.decode()} with error {e}")
676-
677-
def _handle_markets_subscribed(self, raw: bytes) -> None:
678-
try:
679-
msg: DYDXWsMarketSubscribedData = self._decoder_ws_instruments_subscribed.decode(raw)
680-
681-
self._log.debug(f"{msg}")
682-
683-
except Exception as e:
684-
self._log.error(f"Failed to parse market channel data: {raw.decode()} with error {e}")
685-
686663
async def _subscribe_trade_ticks(
687664
self,
688665
instrument_id: InstrumentId,

nautilus_trader/adapters/dydx/execution.py

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,13 +53,16 @@
5353
from nautilus_trader.adapters.dydx.schemas.ws import DYDXWsBlockHeightChannelData
5454
from nautilus_trader.adapters.dydx.schemas.ws import DYDXWsBlockHeightSubscribedData
5555
from nautilus_trader.adapters.dydx.schemas.ws import DYDXWsFillSubaccountMessageContents
56+
from nautilus_trader.adapters.dydx.schemas.ws import DYDXWsMarketChannelData
57+
from nautilus_trader.adapters.dydx.schemas.ws import DYDXWsMarketSubscribedData
5658
from nautilus_trader.adapters.dydx.schemas.ws import DYDXWsMessageGeneral
5759
from nautilus_trader.adapters.dydx.schemas.ws import DYDXWsOrderSubaccountMessageContents
5860
from nautilus_trader.adapters.dydx.schemas.ws import DYDXWsSubaccountsChannelData
5961
from nautilus_trader.adapters.dydx.schemas.ws import DYDXWsSubaccountsSubscribed
6062
from nautilus_trader.adapters.dydx.websocket.client import DYDXWebsocketClient
6163
from nautilus_trader.cache.cache import Cache
6264
from nautilus_trader.common.component import LiveClock
65+
from nautilus_trader.common.component import Logger
6366
from nautilus_trader.common.component import MessageBus
6467
from nautilus_trader.common.enums import LogColor
6568
from nautilus_trader.core.correctness import PyCondition
@@ -110,6 +113,7 @@ def __init__(self, cache: Cache) -> None:
110113
Generate integer client order IDs.
111114
"""
112115
self._cache = cache
116+
self._log: Logger = Logger(type(self).__name__)
113117

114118
def generate_client_order_id_int(self, client_order_id: ClientOrderId) -> int:
115119
"""
@@ -143,6 +147,8 @@ def get_client_order_id_int(self, client_order_id: ClientOrderId) -> int | None:
143147

144148
if value is not None:
145149
result = int.from_bytes(value, byteorder="big")
150+
else:
151+
self._log.error(f"ClientOrderId integer not found in cache for {client_order_id!r}")
146152

147153
return result
148154

@@ -154,6 +160,8 @@ def get_client_order_id(self, client_order_id_int: int) -> ClientOrderId:
154160

155161
if value is not None:
156162
return ClientOrderId(value.decode("utf-8"))
163+
else:
164+
self._log.error(f"ClientOrderId not found in cache for integer {client_order_id_int}")
157165

158166
return ClientOrderId(str(client_order_id_int))
159167

@@ -263,11 +271,14 @@ def __init__(
263271
DYDXWsBlockHeightSubscribedData,
264272
)
265273
self._decoder_ws_block_height_channel = msgspec.json.Decoder(DYDXWsBlockHeightChannelData)
274+
self._decoder_ws_instruments = msgspec.json.Decoder(DYDXWsMarketChannelData)
275+
self._decoder_ws_instruments_subscribed = msgspec.json.Decoder(DYDXWsMarketSubscribedData)
266276

267277
# Hot caches
268278
self._order_builders: dict[InstrumentId, OrderBuilder] = {}
269279
self._generate_order_status_retries: dict[ClientOrderId, int] = {}
270280
self._block_height: int = 0
281+
self._oracle_prices: dict[InstrumentId, Decimal] = {}
271282

272283
self._retry_manager_pool = RetryManagerPool(
273284
pool_size=100,
@@ -288,11 +299,12 @@ async def _connect(self) -> None:
288299
await self._ws_client.connect()
289300

290301
# Subscribe account updates
302+
await self._ws_client.subscribe_markets()
303+
await self._ws_client.subscribe_block_height()
291304
await self._ws_client.subscribe_account_update(
292305
wallet_address=self._wallet_address,
293306
subaccount_number=self._subaccount,
294307
)
295-
await self._ws_client.subscribe_block_height()
296308

297309
self._block_height = await self._grpc_account.latest_block_height()
298310

@@ -304,11 +316,13 @@ async def _connect(self) -> None:
304316
)
305317

306318
async def _disconnect(self) -> None:
319+
await self._ws_client.unsubscribe_markets()
320+
await self._ws_client.unsubscribe_block_height()
307321
await self._ws_client.unsubscribe_account_update(
308322
wallet_address=self._wallet_address,
309323
subaccount_number=self._subaccount,
310324
)
311-
await self._ws_client.unsubscribe_block_height()
325+
312326
await self._ws_client.disconnect()
313327
await self._grpc_account.disconnect()
314328

@@ -710,10 +724,14 @@ def _handle_ws_message(self, raw: bytes) -> None:
710724
self._handle_block_height_channel_data(raw)
711725
elif ws_message.channel == "v4_subaccounts" and ws_message.type == "channel_data":
712726
self._handle_subaccounts_channel_data(raw)
727+
elif ws_message.channel == "v4_markets" and ws_message.type == "channel_data":
728+
self._handle_markets(raw)
713729
elif ws_message.channel == "v4_block_height" and ws_message.type == "subscribed":
714730
self._handle_block_height_subscribed(raw)
715731
elif ws_message.channel == "v4_subaccounts" and ws_message.type == "subscribed":
716732
self._handle_subaccounts_subscribed(raw)
733+
elif ws_message.channel == "v4_markets" and ws_message.type == "subscribed":
734+
self._handle_markets_subscribed(raw)
717735
elif ws_message.type == "unsubscribed":
718736
self._log.info(
719737
f"Unsubscribed from channel {ws_message.channel} for {ws_message.id}",
@@ -749,6 +767,30 @@ def _handle_block_height_channel_data(self, raw: bytes) -> None:
749767
f"Failed to parse block height channel message: {raw.decode()} with error {e}",
750768
)
751769

770+
def _handle_markets(self, raw: bytes) -> None:
771+
try:
772+
msg: DYDXWsMarketChannelData = self._decoder_ws_instruments.decode(raw)
773+
774+
if msg.contents.oraclePrices is not None:
775+
for symbol, oracle_price_market in msg.contents.oraclePrices.items():
776+
instrument_id = DYDXSymbol(symbol).to_instrument_id()
777+
self._oracle_prices[instrument_id] = Decimal(oracle_price_market.oraclePrice)
778+
779+
except Exception as e:
780+
self._log.error(f"Failed to parse market data: {raw.decode()} with error {e}")
781+
782+
def _handle_markets_subscribed(self, raw: bytes) -> None:
783+
try:
784+
msg: DYDXWsMarketSubscribedData = self._decoder_ws_instruments_subscribed.decode(raw)
785+
786+
for symbol, oracle_price_market in msg.contents.markets.items():
787+
if oracle_price_market.oraclePrice is not None:
788+
instrument_id = DYDXSymbol(symbol).to_instrument_id()
789+
self._oracle_prices[instrument_id] = Decimal(oracle_price_market.oraclePrice)
790+
791+
except Exception as e:
792+
self._log.error(f"Failed to parse market channel data: {raw.decode()} with error {e}")
793+
752794
def _handle_subaccounts_subscribed(self, raw: bytes) -> None:
753795
try:
754796
msg: DYDXWsSubaccountsSubscribed = self._decoder_ws_msg_subaccounts_subscribed.decode(
@@ -780,6 +822,7 @@ def _handle_subaccounts_subscribed(self, raw: bytes) -> None:
780822
margin_balance = perpetual_position.parse_margin_balance(
781823
margin_init=instrument.margin_init,
782824
margin_maint=instrument.margin_maint,
825+
oracle_price=self._oracle_prices.get(instrument.id),
783826
)
784827

785828
initial_margins[

nautilus_trader/adapters/dydx/schemas/account/address.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,20 +75,28 @@ def quote_currency(self) -> str:
7575
currency = self.market.split("-")[1]
7676
return CURRENCY_MAP.get(currency, currency)
7777

78-
def parse_margin_balance(self, margin_init: Decimal, margin_maint: Decimal) -> MarginBalance:
78+
def parse_margin_balance(
79+
self,
80+
margin_init: Decimal,
81+
margin_maint: Decimal,
82+
oracle_price: Decimal | None = None,
83+
) -> MarginBalance:
7984
"""
8085
Parse the position message into a margin balance report.
8186
"""
8287
currency = Currency.from_str(self.quote_currency())
8388

8489
if self.status == DYDXPerpetualPositionStatus.OPEN:
90+
if oracle_price is None:
91+
oracle_price = Decimal(self.entryPrice)
92+
8593
return MarginBalance(
8694
initial=Money(
87-
margin_init * abs(Decimal(self.size)) * Decimal(self.entryPrice),
95+
margin_init * abs(Decimal(self.size)) * oracle_price,
8896
currency,
8997
),
9098
maintenance=Money(
91-
margin_maint * abs(Decimal(self.size)) * Decimal(self.entryPrice),
99+
margin_maint * abs(Decimal(self.size)) * oracle_price,
92100
currency,
93101
),
94102
)

0 commit comments

Comments
 (0)