Skip to main content

nautilus_dydx/http/
models.rs

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