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