nautilus_dydx/http/
models.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Data models for dYdX v4 Indexer REST API responses.
17//!
18//! This module contains Rust types that mirror the JSON structures returned
19//! by the dYdX v4 Indexer API endpoints.
20//!
21//! # API Documentation
22//!
23//! - Indexer HTTP API: <https://docs.dydx.exchange/api_integration-indexer/indexer_api>
24//! - Markets: <https://docs.dydx.exchange/api_integration-indexer/indexer_api#markets>
25//! - Accounts: <https://docs.dydx.exchange/api_integration-indexer/indexer_api#accounts>
26
27use std::collections::HashMap;
28
29use chrono::{DateTime, Utc};
30use nautilus_model::enums::OrderSide;
31use rust_decimal::Decimal;
32use serde::{Deserialize, Serialize};
33use serde_with::{DisplayFromStr, serde_as};
34use ustr::Ustr;
35
36use crate::common::enums::{
37    DydxCandleResolution, DydxConditionType, DydxFillType, DydxLiquidity, DydxMarketStatus,
38    DydxOrderExecution, DydxOrderStatus, DydxPositionStatus, DydxTickerType, DydxTimeInForce,
39    DydxTradeType,
40};
41
42////////////////////////////////////////////////////////////////////////////////
43// Markets
44////////////////////////////////////////////////////////////////////////////////
45
46/// Response wrapper for markets endpoint.
47#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct MarketsResponse {
49    /// Map of market ticker to perpetual market data.
50    pub markets: HashMap<String, PerpetualMarket>,
51}
52
53/// Perpetual market definition.
54#[serde_as]
55#[derive(Debug, Clone, Serialize, Deserialize)]
56#[serde(rename_all = "camelCase")]
57pub struct PerpetualMarket {
58    /// Unique identifier for the CLOB pair.
59    #[serde_as(as = "DisplayFromStr")]
60    pub clob_pair_id: u32,
61    /// Market ticker (e.g., "BTC-USD").
62    pub ticker: String,
63    /// Market status (ACTIVE, PAUSED, etc.).
64    pub status: DydxMarketStatus,
65    /// Base asset symbol (optional, not always returned by API).
66    #[serde(default)]
67    pub base_asset: Option<String>,
68    /// Quote asset symbol (optional, not always returned by API).
69    #[serde(default)]
70    pub quote_asset: Option<String>,
71    /// Step size for order quantities (minimum increment).
72    #[serde_as(as = "DisplayFromStr")]
73    pub step_size: Decimal,
74    /// Tick size for order prices (minimum increment).
75    #[serde_as(as = "DisplayFromStr")]
76    pub tick_size: Decimal,
77    /// Index price for the market (optional, not always returned by API).
78    #[serde(default)]
79    #[serde_as(as = "Option<DisplayFromStr>")]
80    pub index_price: Option<Decimal>,
81    /// Oracle price for the market.
82    #[serde_as(as = "DisplayFromStr")]
83    pub oracle_price: Decimal,
84    /// Price change over 24 hours.
85    #[serde(rename = "priceChange24H")]
86    #[serde_as(as = "DisplayFromStr")]
87    pub price_change_24h: Decimal,
88    /// Next funding rate.
89    #[serde_as(as = "DisplayFromStr")]
90    pub next_funding_rate: Decimal,
91    /// Next funding time (ISO8601, optional).
92    #[serde(default)]
93    pub next_funding_at: Option<DateTime<Utc>>,
94    /// Minimum order size in base currency (optional).
95    #[serde(default)]
96    #[serde_as(as = "Option<DisplayFromStr>")]
97    pub min_order_size: Option<Decimal>,
98    /// Market type (always PERPETUAL for dYdX v4, optional).
99    #[serde(rename = "type", default)]
100    pub market_type: Option<DydxTickerType>,
101    /// Initial margin fraction.
102    #[serde_as(as = "DisplayFromStr")]
103    pub initial_margin_fraction: Decimal,
104    /// Maintenance margin fraction.
105    #[serde_as(as = "DisplayFromStr")]
106    pub maintenance_margin_fraction: Decimal,
107    /// Base position notional value (optional).
108    #[serde(default)]
109    #[serde_as(as = "Option<DisplayFromStr>")]
110    pub base_position_notional: Option<Decimal>,
111    /// Incremental position size for margin scaling (optional).
112    #[serde(default)]
113    #[serde_as(as = "Option<DisplayFromStr>")]
114    pub incremental_position_size: Option<Decimal>,
115    /// Incremental initial margin fraction (optional).
116    #[serde(default)]
117    #[serde_as(as = "Option<DisplayFromStr>")]
118    pub incremental_initial_margin_fraction: Option<Decimal>,
119    /// Maximum position size (optional).
120    #[serde(default)]
121    #[serde_as(as = "Option<DisplayFromStr>")]
122    pub max_position_size: Option<Decimal>,
123    /// Open interest in base currency.
124    #[serde_as(as = "DisplayFromStr")]
125    pub open_interest: Decimal,
126    /// Atomic resolution (power of 10 for quantum conversion).
127    pub atomic_resolution: i32,
128    /// Quantum conversion exponent (deprecated, use atomic_resolution).
129    pub quantum_conversion_exponent: i32,
130    /// Subticks per tick.
131    pub subticks_per_tick: u32,
132    /// Step base quantums.
133    pub step_base_quantums: u64,
134    /// Is the market in reduce-only mode.
135    #[serde(default)]
136    pub is_reduce_only: bool,
137}
138
139/// Orderbook snapshot response.
140#[derive(Debug, Clone, Serialize, Deserialize)]
141pub struct OrderbookResponse {
142    /// Bids (buy orders).
143    pub bids: Vec<OrderbookLevel>,
144    /// Asks (sell orders).
145    pub asks: Vec<OrderbookLevel>,
146}
147
148/// Single level in the orderbook.
149#[serde_as]
150#[derive(Debug, Clone, Serialize, Deserialize)]
151pub struct OrderbookLevel {
152    /// Price level.
153    #[serde_as(as = "DisplayFromStr")]
154    pub price: Decimal,
155    /// Size at this level.
156    #[serde_as(as = "DisplayFromStr")]
157    pub size: Decimal,
158}
159
160/// Response wrapper for trades endpoint.
161#[derive(Debug, Clone, Serialize, Deserialize)]
162pub struct TradesResponse {
163    /// List of trades.
164    pub trades: Vec<Trade>,
165}
166
167/// Individual trade.
168#[serde_as]
169#[derive(Debug, Clone, Serialize, Deserialize)]
170#[serde(rename_all = "camelCase")]
171pub struct Trade {
172    /// Unique trade ID.
173    pub id: String,
174    /// Order side that was the taker.
175    pub side: OrderSide,
176    /// Trade size in base currency.
177    #[serde_as(as = "DisplayFromStr")]
178    pub size: Decimal,
179    /// Trade price.
180    #[serde_as(as = "DisplayFromStr")]
181    pub price: Decimal,
182    /// Trade timestamp.
183    pub created_at: DateTime<Utc>,
184    /// Height of block containing this trade.
185    #[serde_as(as = "DisplayFromStr")]
186    pub created_at_height: u64,
187    /// Trade type (LIMIT, MARKET, LIQUIDATED, etc.).
188    #[serde(rename = "type")]
189    pub trade_type: DydxTradeType,
190}
191
192/// Response wrapper for candles endpoint.
193#[derive(Debug, Clone, Serialize, Deserialize)]
194pub struct CandlesResponse {
195    /// List of candles.
196    pub candles: Vec<Candle>,
197}
198
199/// OHLCV candle data.
200#[serde_as]
201#[derive(Debug, Clone, Serialize, Deserialize)]
202#[serde(rename_all = "camelCase")]
203pub struct Candle {
204    /// Candle start time.
205    pub started_at: DateTime<Utc>,
206    /// Market ticker.
207    pub ticker: String,
208    /// Candle resolution.
209    pub resolution: DydxCandleResolution,
210    /// Opening price.
211    #[serde_as(as = "DisplayFromStr")]
212    pub open: Decimal,
213    /// Highest price.
214    #[serde_as(as = "DisplayFromStr")]
215    pub high: Decimal,
216    /// Lowest price.
217    #[serde_as(as = "DisplayFromStr")]
218    pub low: Decimal,
219    /// Closing price.
220    #[serde_as(as = "DisplayFromStr")]
221    pub close: Decimal,
222    /// Base asset volume.
223    #[serde_as(as = "DisplayFromStr")]
224    pub base_token_volume: Decimal,
225    /// Quote asset volume (USD).
226    #[serde_as(as = "DisplayFromStr")]
227    pub usd_volume: Decimal,
228    /// Number of trades in this candle.
229    pub trades: u64,
230    /// Block height at candle start.
231    #[serde_as(as = "DisplayFromStr")]
232    pub starting_open_interest: Decimal,
233}
234
235////////////////////////////////////////////////////////////////////////////////
236// Accounts
237////////////////////////////////////////////////////////////////////////////////
238
239/// Response for subaccount endpoint.
240#[derive(Debug, Clone, Serialize, Deserialize)]
241pub struct SubaccountResponse {
242    /// Subaccount data.
243    pub subaccount: Subaccount,
244}
245
246/// Subaccount information.
247#[serde_as]
248#[derive(Debug, Clone, Serialize, Deserialize)]
249#[serde(rename_all = "camelCase")]
250pub struct Subaccount {
251    /// Subaccount address (dydx...).
252    pub address: String,
253    /// Subaccount number.
254    pub subaccount_number: u32,
255    /// Account equity in USD.
256    #[serde_as(as = "DisplayFromStr")]
257    pub equity: Decimal,
258    /// Free collateral.
259    #[serde_as(as = "DisplayFromStr")]
260    pub free_collateral: Decimal,
261    /// Open perpetual positions.
262    #[serde(default)]
263    pub open_perpetual_positions: HashMap<String, PerpetualPosition>,
264    /// Asset positions (e.g., USDC).
265    #[serde(default)]
266    pub asset_positions: HashMap<String, AssetPosition>,
267    /// Margin enabled flag.
268    #[serde(default)]
269    pub margin_enabled: bool,
270    /// Last updated height.
271    #[serde_as(as = "DisplayFromStr")]
272    pub updated_at_height: u64,
273}
274
275/// Perpetual position.
276#[serde_as]
277#[derive(Debug, Clone, Serialize, Deserialize)]
278#[serde(rename_all = "camelCase")]
279pub struct PerpetualPosition {
280    /// Market ticker.
281    pub market: String,
282    /// Position status.
283    pub status: DydxPositionStatus,
284    /// Position side (determined by size sign).
285    pub side: OrderSide,
286    /// Position size (negative for short).
287    #[serde_as(as = "DisplayFromStr")]
288    pub size: Decimal,
289    /// Maximum size reached.
290    #[serde_as(as = "DisplayFromStr")]
291    pub max_size: Decimal,
292    /// Average entry price.
293    #[serde_as(as = "DisplayFromStr")]
294    pub entry_price: Decimal,
295    /// Exit price (if closed).
296    #[serde_as(as = "Option<DisplayFromStr>")]
297    #[serde(skip_serializing_if = "Option::is_none")]
298    pub exit_price: Option<Decimal>,
299    /// Realized PnL.
300    #[serde_as(as = "DisplayFromStr")]
301    pub realized_pnl: Decimal,
302    /// Creation height.
303    #[serde_as(as = "DisplayFromStr")]
304    pub created_at_height: u64,
305    /// Creation time.
306    pub created_at: DateTime<Utc>,
307    /// Sum of all open order sizes.
308    #[serde_as(as = "DisplayFromStr")]
309    pub sum_open: Decimal,
310    /// Sum of all close order sizes.
311    #[serde_as(as = "DisplayFromStr")]
312    pub sum_close: Decimal,
313    /// Net funding paid/received.
314    #[serde_as(as = "DisplayFromStr")]
315    pub net_funding: Decimal,
316    /// Unrealized PnL.
317    #[serde_as(as = "DisplayFromStr")]
318    pub unrealized_pnl: Decimal,
319    /// Closed time (if closed).
320    #[serde(skip_serializing_if = "Option::is_none")]
321    pub closed_at: Option<DateTime<Utc>>,
322}
323
324/// Asset position (e.g., USDC balance).
325#[serde_as]
326#[derive(Debug, Clone, Serialize, Deserialize)]
327#[serde(rename_all = "camelCase")]
328pub struct AssetPosition {
329    /// Asset symbol.
330    pub symbol: Ustr,
331    /// Position side (always LONG for assets).
332    pub side: OrderSide,
333    /// Asset size (balance).
334    #[serde_as(as = "DisplayFromStr")]
335    pub size: Decimal,
336    /// Asset ID.
337    pub asset_id: String,
338}
339
340/// Response for orders endpoint.
341#[derive(Debug, Clone, Serialize, Deserialize)]
342pub struct OrdersResponse {
343    /// List of orders.
344    pub orders: Vec<Order>,
345}
346
347/// Order information.
348#[serde_as]
349#[derive(Debug, Clone, Serialize, Deserialize)]
350#[serde(rename_all = "camelCase")]
351pub struct Order {
352    /// Unique order ID.
353    pub id: String,
354    /// Subaccount ID.
355    pub subaccount_id: String,
356    /// Client-provided order ID.
357    pub client_id: String,
358    /// CLOB pair ID.
359    #[serde_as(as = "DisplayFromStr")]
360    pub clob_pair_id: u32,
361    /// Order side.
362    pub side: OrderSide,
363    /// Order size.
364    #[serde_as(as = "DisplayFromStr")]
365    pub size: Decimal,
366    /// Remaining size to be filled.
367    #[serde_as(as = "DisplayFromStr")]
368    pub remaining_size: Decimal,
369    /// Limit price.
370    #[serde_as(as = "DisplayFromStr")]
371    pub price: Decimal,
372    /// Order status.
373    pub status: DydxOrderStatus,
374    /// Order type (LIMIT, MARKET, etc.).
375    #[serde(rename = "type")]
376    pub order_type: String,
377    /// Time-in-force.
378    pub time_in_force: DydxTimeInForce,
379    /// Reduce-only flag.
380    pub reduce_only: bool,
381    /// Post-only flag.
382    pub post_only: bool,
383    /// Order flags (bitfield).
384    pub order_flags: u32,
385    /// Good-til-block (for short-term orders).
386    #[serde_as(as = "Option<DisplayFromStr>")]
387    #[serde(skip_serializing_if = "Option::is_none")]
388    pub good_til_block: Option<u64>,
389    /// Good-til-time (ISO8601).
390    #[serde(skip_serializing_if = "Option::is_none")]
391    pub good_til_block_time: Option<DateTime<Utc>>,
392    /// Creation height.
393    #[serde_as(as = "DisplayFromStr")]
394    pub created_at_height: u64,
395    /// Client metadata.
396    pub client_metadata: u32,
397    /// Trigger price (for conditional orders).
398    #[serde_as(as = "Option<DisplayFromStr>")]
399    #[serde(skip_serializing_if = "Option::is_none")]
400    pub trigger_price: Option<Decimal>,
401    /// Condition type (STOP_LOSS, TAKE_PROFIT, UNSPECIFIED).
402    #[serde(skip_serializing_if = "Option::is_none")]
403    pub condition_type: Option<DydxConditionType>,
404    /// Conditional order trigger in subticks.
405    #[serde_as(as = "Option<DisplayFromStr>")]
406    #[serde(skip_serializing_if = "Option::is_none")]
407    pub conditional_order_trigger_subticks: Option<u64>,
408    /// Order execution type.
409    #[serde(skip_serializing_if = "Option::is_none")]
410    pub execution: Option<DydxOrderExecution>,
411    /// Updated timestamp.
412    pub updated_at: DateTime<Utc>,
413    /// Updated height.
414    #[serde_as(as = "DisplayFromStr")]
415    pub updated_at_height: u64,
416}
417
418/// Response for fills endpoint.
419#[derive(Debug, Clone, Serialize, Deserialize)]
420pub struct FillsResponse {
421    /// List of fills.
422    pub fills: Vec<Fill>,
423}
424
425/// Order fill information.
426#[serde_as]
427#[derive(Debug, Clone, Serialize, Deserialize)]
428#[serde(rename_all = "camelCase")]
429pub struct Fill {
430    /// Unique fill ID.
431    pub id: String,
432    /// Order side.
433    pub side: OrderSide,
434    /// Liquidity side (MAKER/TAKER).
435    pub liquidity: DydxLiquidity,
436    /// Fill type.
437    #[serde(rename = "type")]
438    pub fill_type: DydxFillType,
439    /// Market ticker.
440    pub market: String,
441    /// Market type.
442    pub market_type: DydxTickerType,
443    /// Fill price.
444    #[serde_as(as = "DisplayFromStr")]
445    pub price: Decimal,
446    /// Fill size.
447    #[serde_as(as = "DisplayFromStr")]
448    pub size: Decimal,
449    /// Fee paid.
450    #[serde_as(as = "DisplayFromStr")]
451    pub fee: Decimal,
452    /// Fill timestamp.
453    pub created_at: DateTime<Utc>,
454    /// Fill height.
455    #[serde_as(as = "DisplayFromStr")]
456    pub created_at_height: u64,
457    /// Order ID.
458    pub order_id: String,
459    /// Client order ID.
460    pub client_metadata: u32,
461}
462
463/// Response for transfers endpoint.
464#[derive(Debug, Clone, Serialize, Deserialize)]
465pub struct TransfersResponse {
466    /// List of transfers.
467    pub transfers: Vec<Transfer>,
468}
469
470/// Transfer information.
471#[serde_as]
472#[derive(Debug, Clone, Serialize, Deserialize)]
473#[serde(rename_all = "camelCase")]
474pub struct Transfer {
475    /// Unique transfer ID.
476    pub id: String,
477    /// Transfer type (DEPOSIT, WITHDRAWAL, TRANSFER_OUT, TRANSFER_IN).
478    #[serde(rename = "type")]
479    pub transfer_type: String,
480    /// Sender address.
481    pub sender: TransferAccount,
482    /// Recipient address.
483    pub recipient: TransferAccount,
484    /// Asset symbol.
485    pub asset: String,
486    /// Transfer amount.
487    #[serde_as(as = "DisplayFromStr")]
488    pub amount: Decimal,
489    /// Creation timestamp.
490    pub created_at: DateTime<Utc>,
491    /// Creation height.
492    #[serde_as(as = "DisplayFromStr")]
493    pub created_at_height: u64,
494    /// Transaction hash.
495    pub transaction_hash: String,
496}
497
498/// Transfer account information.
499#[derive(Debug, Clone, Serialize, Deserialize)]
500#[serde(rename_all = "camelCase")]
501pub struct TransferAccount {
502    /// Address.
503    pub address: String,
504    /// Subaccount number.
505    pub subaccount_number: u32,
506}
507
508////////////////////////////////////////////////////////////////////////////////
509// Utility
510////////////////////////////////////////////////////////////////////////////////
511
512/// Response for time endpoint.
513#[derive(Debug, Clone, Serialize, Deserialize)]
514pub struct TimeResponse {
515    /// Current ISO8601 timestamp.
516    pub iso: DateTime<Utc>,
517    /// Current Unix timestamp in milliseconds.
518    #[serde(rename = "epoch")]
519    pub epoch_ms: i64,
520}
521
522/// Response for height endpoint.
523#[serde_as]
524#[derive(Debug, Clone, Serialize, Deserialize)]
525pub struct HeightResponse {
526    /// Current blockchain height.
527    #[serde_as(as = "DisplayFromStr")]
528    pub height: u64,
529    /// Timestamp of the block.
530    pub time: DateTime<Utc>,
531}
532
533////////////////////////////////////////////////////////////////////////////////
534// Execution Models (Node API)
535////////////////////////////////////////////////////////////////////////////////
536
537/// Request to place an order via Node API.
538#[serde_as]
539#[derive(Debug, Clone, Serialize, Deserialize)]
540#[serde(rename_all = "camelCase")]
541pub struct PlaceOrderRequest {
542    /// Subaccount placing the order.
543    pub subaccount: SubaccountId,
544    /// Client-generated order ID.
545    pub client_id: u32,
546    /// Order type flags (bitfield for short-term, reduce-only, etc.).
547    pub order_flags: u32,
548    /// CLOB pair ID.
549    pub clob_pair_id: u32,
550    /// Order side.
551    pub side: OrderSide,
552    /// Order size in quantums.
553    pub quantums: u64,
554    /// Order subticks (price representation).
555    pub subticks: u64,
556    /// Time-in-force.
557    pub time_in_force: DydxTimeInForce,
558    /// Good-til-block (for short-term orders).
559    #[serde(skip_serializing_if = "Option::is_none")]
560    pub good_til_block: Option<u32>,
561    /// Good-til-block-time (Unix seconds, for stateful orders).
562    #[serde(skip_serializing_if = "Option::is_none")]
563    pub good_til_block_time: Option<u32>,
564    /// Reduce-only flag.
565    pub reduce_only: bool,
566    /// Optional authenticator IDs for permissioned keys.
567    #[serde(skip_serializing_if = "Option::is_none")]
568    pub authenticator_ids: Option<Vec<u64>>,
569}
570
571/// Subaccount identifier.
572#[derive(Debug, Clone, Serialize, Deserialize)]
573pub struct SubaccountId {
574    /// Owner address.
575    pub owner: String,
576    /// Subaccount number.
577    pub number: u32,
578}
579
580/// Request to cancel an order.
581#[derive(Debug, Clone, Serialize, Deserialize)]
582#[serde(rename_all = "camelCase")]
583pub struct CancelOrderRequest {
584    /// Subaccount ID.
585    pub subaccount_id: SubaccountId,
586    /// Client order ID to cancel.
587    pub client_id: u32,
588    /// CLOB pair ID.
589    pub clob_pair_id: u32,
590    /// Order flags.
591    pub order_flags: u32,
592    /// Good-til-block or good-til-block-time for the cancel.
593    pub good_til_block: Option<u32>,
594    pub good_til_block_time: Option<u32>,
595}
596
597/// Transaction response from Node.
598#[serde_as]
599#[derive(Debug, Clone, Serialize, Deserialize)]
600pub struct TransactionResponse {
601    /// Transaction hash.
602    pub tx_hash: String,
603    /// Block height.
604    #[serde_as(as = "DisplayFromStr")]
605    pub height: u64,
606    /// Result code (0 = success).
607    pub code: u32,
608    /// Raw log output.
609    pub raw_log: String,
610}