Skip to main content

nautilus_deribit/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//! Deribit HTTP API models and types.
17//!
18//! Types with decimal fields borrow their JSON tokens, so they deserialize only from borrowed input
19//! such as `serde_json::from_str` or `serde_json::from_slice`.
20
21use ahash::AHashMap;
22use nautilus_core::serialization::{decimal, deserialize_optional_decimal_token_borrowed};
23use rust_decimal::Decimal;
24use serde::{Deserialize, Serialize};
25use ustr::Ustr;
26
27use crate::common::serialization::{
28    deserialize_decimal_token_or_zero_borrowed, deserialize_decimal_token_pairs_borrowed,
29    deserialize_decimal_token_vec_borrowed,
30};
31pub use crate::common::{
32    enums::{DeribitCurrency, DeribitOptionType, DeribitProductType},
33    models::DeribitTradeLeg,
34    rpc::{DeribitJsonRpcError, DeribitJsonRpcRequest, DeribitJsonRpcResponse},
35};
36
37/// JSON-RPC 2.0 response payload (either success or error).
38#[derive(Debug, Serialize, Deserialize)]
39#[serde(untagged)]
40pub enum DeribitResponsePayload<T> {
41    /// Successful response with result data
42    Success { result: T },
43    /// Error response with error details
44    Error { error: DeribitJsonRpcError },
45}
46
47/// Deribit instrument definition.
48#[derive(Clone, Debug, Serialize, Deserialize)]
49pub struct DeribitInstrument {
50    /// The underlying currency being traded
51    pub base_currency: Ustr,
52    /// Block trade commission for instrument
53    #[serde(
54        default,
55        deserialize_with = "deserialize_optional_decimal_token_borrowed"
56    )]
57    pub block_trade_commission: Option<Decimal>,
58    /// Minimum amount for block trading
59    #[serde(
60        default,
61        deserialize_with = "deserialize_optional_decimal_token_borrowed"
62    )]
63    pub block_trade_min_trade_amount: Option<Decimal>,
64    /// Specifies minimal price change for block trading
65    #[serde(
66        default,
67        deserialize_with = "deserialize_optional_decimal_token_borrowed"
68    )]
69    pub block_trade_tick_size: Option<Decimal>,
70    /// Contract size for instrument
71    #[serde(deserialize_with = "deserialize_decimal_token_or_zero_borrowed")]
72    pub contract_size: Decimal,
73    /// Counter currency for the instrument
74    pub counter_currency: Option<Ustr>,
75    /// The time when the instrument was first created (milliseconds since UNIX epoch)
76    pub creation_timestamp: i64,
77    /// The time when the instrument will expire (milliseconds since UNIX epoch)
78    pub expiration_timestamp: Option<i64>,
79    /// Future type (deprecated, use instrument_type instead)
80    pub future_type: Option<String>,
81    /// Instrument ID
82    pub instrument_id: i64,
83    /// Unique instrument identifier (e.g., "BTC-PERPETUAL")
84    pub instrument_name: Ustr,
85    /// Type of the instrument: "linear" or "reversed"
86    pub instrument_type: Option<String>,
87    /// Indicates if the instrument can currently be traded
88    pub is_active: bool,
89    /// Product type: "future", "option", "spot", "future_combo", "option_combo"
90    pub kind: DeribitProductType,
91    /// Maker commission for instrument
92    #[serde(deserialize_with = "deserialize_decimal_token_or_zero_borrowed")]
93    pub maker_commission: Decimal,
94    /// Maximal leverage for instrument (only for futures)
95    pub max_leverage: Option<i64>,
96    /// Maximal liquidation trade commission for instrument (only for futures)
97    #[serde(
98        default,
99        deserialize_with = "deserialize_optional_decimal_token_borrowed"
100    )]
101    pub max_liquidation_commission: Option<Decimal>,
102    /// Minimum amount for trading
103    #[serde(deserialize_with = "deserialize_decimal_token_or_zero_borrowed")]
104    pub min_trade_amount: Decimal,
105    /// The option type (only for options)
106    pub option_type: Option<DeribitOptionType>,
107    /// Name of price index that is used for this instrument
108    pub price_index: Option<String>,
109    /// The currency in which the instrument prices are quoted
110    pub quote_currency: Ustr,
111    /// Settlement currency for the instrument (not present for spot)
112    pub settlement_currency: Option<Ustr>,
113    /// The settlement period (not present for spot)
114    pub settlement_period: Option<String>,
115    /// The strike value (only for options)
116    #[serde(
117        default,
118        deserialize_with = "deserialize_optional_decimal_token_borrowed"
119    )]
120    pub strike: Option<Decimal>,
121    /// Taker commission for instrument
122    #[serde(deserialize_with = "deserialize_decimal_token_or_zero_borrowed")]
123    pub taker_commission: Decimal,
124    /// Specifies minimal price change and number of decimal places for instrument prices
125    #[serde(deserialize_with = "deserialize_decimal_token_or_zero_borrowed")]
126    pub tick_size: Decimal,
127    /// Tick size steps for different price ranges
128    pub tick_size_steps: Option<Vec<DeribitTickSizeStep>>,
129}
130
131/// Tick size step definition for price-dependent tick sizes.
132#[derive(Clone, Debug, Serialize, Deserialize)]
133pub struct DeribitTickSizeStep {
134    /// The price from which the increased tick size applies
135    #[serde(deserialize_with = "deserialize_decimal_token_or_zero_borrowed")]
136    pub above_price: Decimal,
137    /// Tick size to be used above the price
138    #[serde(deserialize_with = "deserialize_decimal_token_or_zero_borrowed")]
139    pub tick_size: Decimal,
140}
141
142/// Deribit combo definition from `public/get_combos`.
143#[derive(Clone, Debug, Serialize, Deserialize)]
144pub struct DeribitCombo {
145    /// Unique combo identifier, matching the combo instrument name.
146    pub id: Ustr,
147    /// Combo state, e.g. `active`.
148    pub state: String,
149    /// Combo leg makeup.
150    pub legs: Vec<DeribitComboLeg>,
151    /// Combo creation timestamp in milliseconds since UNIX epoch.
152    pub creation_timestamp: i64,
153    /// Numeric Deribit instrument ID.
154    pub instrument_id: i64,
155    /// State timestamp in milliseconds since UNIX epoch.
156    pub state_timestamp: i64,
157}
158
159/// A single leg entry of a Deribit combo definition.
160#[derive(Clone, Debug, Serialize, Deserialize)]
161pub struct DeribitComboLeg {
162    /// Signed leg amount. Positive is long, negative is short.
163    #[serde(deserialize_with = "deserialize_decimal_token_or_zero_borrowed")]
164    pub amount: Decimal,
165    /// Leg instrument name.
166    pub instrument_name: Ustr,
167}
168
169/// Wrapper for the account summaries response.
170///
171/// The API returns an object with a `summaries` field containing the array of account summaries,
172/// plus account-level metadata.
173#[derive(Debug, Clone, Serialize, Deserialize)]
174pub struct DeribitAccountSummariesResponse {
175    /// Array of per-currency account summaries
176    pub summaries: Vec<DeribitAccountSummary>,
177    /// Account ID
178    #[serde(default)]
179    pub id: Option<i64>,
180    /// Account email
181    #[serde(default)]
182    pub email: Option<String>,
183    /// System name
184    #[serde(default)]
185    pub system_name: Option<String>,
186    /// Account username
187    #[serde(default)]
188    pub username: Option<String>,
189    /// Account type (e.g., "main", "subaccount")
190    #[serde(rename = "type", default)]
191    pub account_type: Option<String>,
192    /// Account creation timestamp (milliseconds since UNIX epoch)
193    #[serde(default)]
194    pub creation_timestamp: Option<i64>,
195    /// Referrer ID (affiliation program)
196    #[serde(default)]
197    pub referrer_id: Option<String>,
198    /// Whether login is enabled for this account
199    #[serde(default)]
200    pub login_enabled: Option<bool>,
201    /// Whether security keys are enabled
202    #[serde(default)]
203    pub security_keys_enabled: Option<bool>,
204    /// Whether MMP (Market Maker Protection) is enabled
205    #[serde(default)]
206    pub mmp_enabled: Option<bool>,
207    /// Whether inter-user transfers are enabled
208    #[serde(default)]
209    pub interuser_transfers_enabled: Option<bool>,
210    /// Self-trading reject mode
211    #[serde(default)]
212    pub self_trading_reject_mode: Option<String>,
213    /// Whether self-trading is extended to subaccounts
214    #[serde(default)]
215    pub self_trading_extended_to_subaccounts: Option<bool>,
216    /// Block RFQ self match prevention
217    #[serde(default)]
218    pub block_rfq_self_match_prevention: Option<bool>,
219}
220
221/// Account summary for a single currency.
222///
223/// Contains balance, equity, margin information, and profit/loss data.
224#[derive(Debug, Clone, Serialize, Deserialize)]
225pub struct DeribitAccountSummary {
226    /// Currency code (e.g., "BTC", "ETH")
227    pub currency: Ustr,
228    /// Account equity (balance + unrealized PnL)
229    #[serde(deserialize_with = "deserialize_decimal_token_or_zero_borrowed")]
230    pub equity: Decimal,
231    /// Account balance
232    #[serde(deserialize_with = "deserialize_decimal_token_or_zero_borrowed")]
233    pub balance: Decimal,
234    /// Available funds for trading
235    #[serde(deserialize_with = "deserialize_decimal_token_or_zero_borrowed")]
236    pub available_funds: Decimal,
237    /// Margin balance (for derivatives)
238    #[serde(deserialize_with = "deserialize_decimal_token_or_zero_borrowed")]
239    pub margin_balance: Decimal,
240    /// Initial margin (required for current positions)
241    #[serde(
242        default,
243        deserialize_with = "deserialize_optional_decimal_token_borrowed"
244    )]
245    pub initial_margin: Option<Decimal>,
246    /// Maintenance margin
247    #[serde(
248        default,
249        deserialize_with = "deserialize_optional_decimal_token_borrowed"
250    )]
251    pub maintenance_margin: Option<Decimal>,
252    /// Total profit/loss
253    #[serde(
254        default,
255        deserialize_with = "deserialize_optional_decimal_token_borrowed"
256    )]
257    pub total_pl: Option<Decimal>,
258    /// Session unrealized profit/loss
259    #[serde(
260        default,
261        deserialize_with = "deserialize_optional_decimal_token_borrowed"
262    )]
263    pub session_upl: Option<Decimal>,
264    /// Session realized profit/loss
265    #[serde(
266        default,
267        deserialize_with = "deserialize_optional_decimal_token_borrowed"
268    )]
269    pub session_rpl: Option<Decimal>,
270    /// Portfolio margining enabled
271    #[serde(default)]
272    pub portfolio_margining_enabled: Option<bool>,
273    /// Margin model (e.g., "segregated_sm", "cross_sm", "cross_pm")
274    #[serde(default)]
275    pub margin_model: Option<String>,
276    /// Whether cross-collateral is enabled for this currency
277    #[serde(default)]
278    pub cross_collateral_enabled: Option<bool>,
279    /// Available withdrawal funds (per-currency withdrawable amount)
280    #[serde(
281        default,
282        deserialize_with = "deserialize_optional_decimal_token_borrowed"
283    )]
284    pub available_withdrawal_funds: Option<Decimal>,
285}
286
287/// Extended account summary with additional account details.
288///
289/// Returned by `private/get_account_summary` with `extended=true`.
290/// Contains all fields from [`DeribitAccountSummary`] plus account metadata,
291/// position Greeks, detailed margins, and fee structures.
292#[derive(Debug, Clone, Serialize, Deserialize)]
293pub struct DeribitAccountSummaryExtended {
294    /// Currency code (e.g., "BTC", "ETH")
295    pub currency: Ustr,
296    /// Account equity (balance + unrealized PnL)
297    #[serde(deserialize_with = "deserialize_decimal_token_or_zero_borrowed")]
298    pub equity: Decimal,
299    /// Account balance
300    #[serde(deserialize_with = "deserialize_decimal_token_or_zero_borrowed")]
301    pub balance: Decimal,
302    /// Available funds for trading
303    #[serde(deserialize_with = "deserialize_decimal_token_or_zero_borrowed")]
304    pub available_funds: Decimal,
305    /// Margin balance (for derivatives)
306    #[serde(deserialize_with = "deserialize_decimal_token_or_zero_borrowed")]
307    pub margin_balance: Decimal,
308    /// Initial margin (required for current positions)
309    #[serde(
310        default,
311        deserialize_with = "deserialize_optional_decimal_token_borrowed"
312    )]
313    pub initial_margin: Option<Decimal>,
314    /// Maintenance margin
315    #[serde(
316        default,
317        deserialize_with = "deserialize_optional_decimal_token_borrowed"
318    )]
319    pub maintenance_margin: Option<Decimal>,
320    /// Total profit/loss
321    #[serde(
322        default,
323        deserialize_with = "deserialize_optional_decimal_token_borrowed"
324    )]
325    pub total_pl: Option<Decimal>,
326    /// Session unrealized profit/loss
327    #[serde(
328        default,
329        deserialize_with = "deserialize_optional_decimal_token_borrowed"
330    )]
331    pub session_upl: Option<Decimal>,
332    /// Session realized profit/loss
333    #[serde(
334        default,
335        deserialize_with = "deserialize_optional_decimal_token_borrowed"
336    )]
337    pub session_rpl: Option<Decimal>,
338    /// Portfolio margining enabled
339    #[serde(default)]
340    pub portfolio_margining_enabled: Option<bool>,
341    // Extended fields below
342    /// Account ID
343    #[serde(default)]
344    pub id: Option<i64>,
345    /// Account email
346    #[serde(default)]
347    pub email: Option<String>,
348    /// Account username
349    #[serde(default)]
350    pub username: Option<String>,
351    /// System name
352    #[serde(default)]
353    pub system_name: Option<String>,
354    /// Account type (e.g., "main", "subaccount")
355    #[serde(rename = "type", default)]
356    pub account_type: Option<String>,
357    /// Futures session unrealized P&L
358    #[serde(
359        default,
360        deserialize_with = "deserialize_optional_decimal_token_borrowed"
361    )]
362    pub futures_session_upl: Option<Decimal>,
363    /// Futures session realized P&L
364    #[serde(
365        default,
366        deserialize_with = "deserialize_optional_decimal_token_borrowed"
367    )]
368    pub futures_session_rpl: Option<Decimal>,
369    /// Options session unrealized P&L
370    #[serde(
371        default,
372        deserialize_with = "deserialize_optional_decimal_token_borrowed"
373    )]
374    pub options_session_upl: Option<Decimal>,
375    /// Options session realized P&L
376    #[serde(
377        default,
378        deserialize_with = "deserialize_optional_decimal_token_borrowed"
379    )]
380    pub options_session_rpl: Option<Decimal>,
381    /// Futures profit/loss
382    #[serde(
383        default,
384        deserialize_with = "deserialize_optional_decimal_token_borrowed"
385    )]
386    pub futures_pl: Option<Decimal>,
387    /// Options profit/loss
388    #[serde(
389        default,
390        deserialize_with = "deserialize_optional_decimal_token_borrowed"
391    )]
392    pub options_pl: Option<Decimal>,
393    /// Options delta
394    #[serde(
395        default,
396        deserialize_with = "deserialize_optional_decimal_token_borrowed"
397    )]
398    pub options_delta: Option<Decimal>,
399    /// Options gamma
400    #[serde(
401        default,
402        deserialize_with = "deserialize_optional_decimal_token_borrowed"
403    )]
404    pub options_gamma: Option<Decimal>,
405    /// Options vega
406    #[serde(
407        default,
408        deserialize_with = "deserialize_optional_decimal_token_borrowed"
409    )]
410    pub options_vega: Option<Decimal>,
411    /// Options theta
412    #[serde(
413        default,
414        deserialize_with = "deserialize_optional_decimal_token_borrowed"
415    )]
416    pub options_theta: Option<Decimal>,
417    /// Options value
418    #[serde(
419        default,
420        deserialize_with = "deserialize_optional_decimal_token_borrowed"
421    )]
422    pub options_value: Option<Decimal>,
423    /// Total delta across all positions
424    #[serde(
425        default,
426        deserialize_with = "deserialize_optional_decimal_token_borrowed"
427    )]
428    pub delta_total: Option<Decimal>,
429    /// Projected delta total
430    #[serde(
431        default,
432        deserialize_with = "deserialize_optional_decimal_token_borrowed"
433    )]
434    pub projected_delta_total: Option<Decimal>,
435    /// Projected initial margin
436    #[serde(
437        default,
438        deserialize_with = "deserialize_optional_decimal_token_borrowed"
439    )]
440    pub projected_initial_margin: Option<Decimal>,
441    /// Projected maintenance margin
442    #[serde(
443        default,
444        deserialize_with = "deserialize_optional_decimal_token_borrowed"
445    )]
446    pub projected_maintenance_margin: Option<Decimal>,
447    /// Estimated liquidation ratio
448    #[serde(
449        default,
450        deserialize_with = "deserialize_optional_decimal_token_borrowed"
451    )]
452    pub estimated_liquidation_ratio: Option<Decimal>,
453    /// Available withdrawal funds
454    #[serde(
455        default,
456        deserialize_with = "deserialize_optional_decimal_token_borrowed"
457    )]
458    pub available_withdrawal_funds: Option<Decimal>,
459    /// Spot reserve
460    #[serde(
461        default,
462        deserialize_with = "deserialize_optional_decimal_token_borrowed"
463    )]
464    pub spot_reserve: Option<Decimal>,
465    /// Fee balance
466    #[serde(
467        default,
468        deserialize_with = "deserialize_optional_decimal_token_borrowed"
469    )]
470    pub fee_balance: Option<Decimal>,
471    /// Margin model (e.g., "segregated_sm", "cross_pm")
472    #[serde(default)]
473    pub margin_model: Option<String>,
474    /// Cross collateral enabled
475    #[serde(default)]
476    pub cross_collateral_enabled: Option<bool>,
477    /// Account creation timestamp (milliseconds since UNIX epoch)
478    #[serde(default)]
479    pub creation_timestamp: Option<i64>,
480    /// Whether login is enabled for this account
481    #[serde(default)]
482    pub login_enabled: Option<bool>,
483    /// Whether security keys are enabled
484    #[serde(default)]
485    pub security_keys_enabled: Option<bool>,
486    /// Whether MMP (Market Maker Protection) is enabled
487    #[serde(default)]
488    pub mmp_enabled: Option<bool>,
489    /// Whether inter-user transfers are enabled
490    #[serde(default)]
491    pub interuser_transfers_enabled: Option<bool>,
492    /// Self-trading reject mode
493    #[serde(default)]
494    pub self_trading_reject_mode: Option<String>,
495    /// Whether self-trading is extended to subaccounts
496    #[serde(default)]
497    pub self_trading_extended_to_subaccounts: Option<bool>,
498    /// Referrer ID (affiliation program)
499    #[serde(default)]
500    pub referrer_id: Option<String>,
501    /// Block RFQ self match prevention
502    #[serde(default)]
503    pub block_rfq_self_match_prevention: Option<bool>,
504}
505
506/// Deribit public trade data from the market data API.
507///
508/// Represents a single trade returned by `/public/get_last_trades_by_instrument_and_time`
509/// and other trade-related endpoints.
510#[derive(Clone, Debug, Serialize, Deserialize)]
511pub struct DeribitPublicTrade {
512    /// Trade amount. For perpetual and inverse futures the amount is in USD units.
513    /// For options and linear futures it is the underlying base currency coin.
514    #[serde(deserialize_with = "deserialize_decimal_token_or_zero_borrowed")]
515    pub amount: Decimal,
516    /// Trade size in contract units (optional, may be absent in historical trades).
517    #[serde(
518        default,
519        deserialize_with = "deserialize_optional_decimal_token_borrowed"
520    )]
521    pub contracts: Option<Decimal>,
522    /// Direction of the trade: "buy" or "sell"
523    pub direction: String,
524    /// Index Price at the moment of trade (can be empty for some trade types).
525    #[serde(
526        default,
527        deserialize_with = "deserialize_optional_decimal_token_borrowed"
528    )]
529    pub index_price: Option<Decimal>,
530    /// Unique instrument identifier.
531    pub instrument_name: String,
532    /// Option implied volatility for the price (Option only).
533    #[serde(
534        default,
535        deserialize_with = "deserialize_optional_decimal_token_borrowed"
536    )]
537    pub iv: Option<Decimal>,
538    /// Optional field (only for trades caused by liquidation).
539    #[serde(default)]
540    pub liquidation: Option<String>,
541    /// Mark Price at the moment of trade (can be empty for some trade types).
542    #[serde(
543        default,
544        deserialize_with = "deserialize_optional_decimal_token_borrowed"
545    )]
546    pub mark_price: Option<Decimal>,
547    /// Price in base currency.
548    #[serde(deserialize_with = "deserialize_decimal_token_or_zero_borrowed")]
549    pub price: Decimal,
550    /// Direction of the "tick" (0 = Plus Tick, 1 = Zero-Plus Tick, 2 = Minus Tick, 3 = Zero-Minus Tick).
551    pub tick_direction: i32,
552    /// The timestamp of the trade (milliseconds since the UNIX epoch).
553    pub timestamp: i64,
554    /// Unique (per currency) trade identifier.
555    pub trade_id: String,
556    /// The sequence number of the trade within instrument.
557    pub trade_seq: i64,
558    /// Block trade id - when trade was part of a block trade.
559    #[serde(default)]
560    pub block_trade_id: Option<String>,
561    /// Block trade leg count - when trade was part of a block trade.
562    #[serde(default)]
563    pub block_trade_leg_count: Option<i32>,
564    /// ID of the Block RFQ - when trade was part of the Block RFQ.
565    #[serde(default)]
566    pub block_rfq_id: Option<i64>,
567    /// Optional field containing combo instrument name if the trade is a combo trade.
568    #[serde(default)]
569    pub combo_id: Option<String>,
570    /// Optional field containing combo trade identifier if the trade is a combo trade.
571    #[serde(default)]
572    pub combo_trade_id: Option<String>,
573    /// Per-leg trades when this is the parent combo trade.
574    #[serde(default)]
575    pub legs: Option<Vec<DeribitTradeLeg>>,
576}
577
578/// Response wrapper for trades endpoints.
579///
580/// Contains the trades array and pagination information.
581#[derive(Clone, Debug, Serialize, Deserialize)]
582pub struct DeribitTradesResponse {
583    /// Whether there are more trades available.
584    pub has_more: bool,
585    /// Array of trade objects.
586    pub trades: Vec<DeribitPublicTrade>,
587}
588
589/// Expiration strings grouped by instrument kind.
590#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
591pub struct DeribitExpirationsByKind {
592    /// Future expirations, including `PERPETUAL` when present.
593    #[serde(default)]
594    pub future: Vec<String>,
595    /// Option expirations.
596    #[serde(default)]
597    pub option: Vec<String>,
598    /// Future combo expirations.
599    #[serde(default)]
600    pub future_combo: Vec<String>,
601    /// Option combo expirations.
602    #[serde(default)]
603    pub option_combo: Vec<String>,
604}
605
606/// Response from `public/get_expirations` endpoint.
607///
608/// Deribit returns a currency-keyed object for concrete or `grouped` currencies,
609/// and a direct kind-keyed object for `currency=any`.
610#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
611#[serde(untagged)]
612pub enum DeribitExpirationsResponse {
613    /// Expirations keyed by lowercase currency code.
614    ByCurrency(AHashMap<String, DeribitExpirationsByKind>),
615    /// Expirations returned without currency grouping.
616    Expirations(DeribitExpirationsByKind),
617}
618
619impl DeribitExpirationsResponse {
620    /// Returns expirations for a currency code, or the direct result for `currency=any`.
621    #[must_use]
622    pub fn expirations_for_currency(&self, currency: &str) -> Option<&DeribitExpirationsByKind> {
623        match self {
624            Self::ByCurrency(expirations) => {
625                let key = currency.to_ascii_lowercase();
626                expirations.get(&key)
627            }
628            Self::Expirations(expirations) => Some(expirations),
629        }
630    }
631}
632
633/// Response from `public/get_tradingview_chart_data` endpoint.
634///
635/// Contains OHLCV data in array format where each array element corresponds
636/// to a single candle at the index in the `ticks` array.
637#[derive(Clone, Debug, Serialize, Deserialize)]
638pub struct DeribitTradingViewChartData {
639    /// List of prices at close (one per candle)
640    #[serde(deserialize_with = "deserialize_decimal_token_vec_borrowed")]
641    pub close: Vec<Decimal>,
642    /// List of cost bars (volume in quote currency, one per candle)
643    #[serde(deserialize_with = "deserialize_decimal_token_vec_borrowed")]
644    pub cost: Vec<Decimal>,
645    /// List of highest price levels (one per candle)
646    #[serde(deserialize_with = "deserialize_decimal_token_vec_borrowed")]
647    pub high: Vec<Decimal>,
648    /// List of lowest price levels (one per candle)
649    #[serde(deserialize_with = "deserialize_decimal_token_vec_borrowed")]
650    pub low: Vec<Decimal>,
651    /// List of prices at open (one per candle)
652    #[serde(deserialize_with = "deserialize_decimal_token_vec_borrowed")]
653    pub open: Vec<Decimal>,
654    /// Status of the query: "ok" or "no_data"
655    pub status: String,
656    /// Values of the time axis given in milliseconds since UNIX epoch
657    pub ticks: Vec<i64>,
658    /// List of volume bars (in base currency, one per candle)
659    #[serde(deserialize_with = "deserialize_decimal_token_vec_borrowed")]
660    pub volume: Vec<Decimal>,
661}
662
663/// Response from `public/get_order_book` endpoint.
664///
665/// Contains the current order book state with bids, asks, and market data.
666#[derive(Clone, Debug, Serialize, Deserialize)]
667pub struct DeribitOrderBook {
668    /// The timestamp of the order book (milliseconds since UNIX epoch)
669    pub timestamp: i64,
670    /// Unique instrument identifier
671    pub instrument_name: String,
672    /// List of bids as [price, amount] pairs.
673    #[serde(deserialize_with = "deserialize_decimal_token_pairs_borrowed")]
674    pub bids: Vec<[Decimal; 2]>,
675    /// List of asks as [price, amount] pairs.
676    #[serde(deserialize_with = "deserialize_decimal_token_pairs_borrowed")]
677    pub asks: Vec<[Decimal; 2]>,
678    /// The state of the order book: "open" or "closed"
679    pub state: String,
680    /// The current best bid price (null if there aren't any bids)
681    #[serde(
682        default,
683        deserialize_with = "deserialize_optional_decimal_token_borrowed"
684    )]
685    pub best_bid_price: Option<Decimal>,
686    /// The current best ask price (null if there aren't any asks)
687    #[serde(
688        default,
689        deserialize_with = "deserialize_optional_decimal_token_borrowed"
690    )]
691    pub best_ask_price: Option<Decimal>,
692    /// The order size of all best bids
693    #[serde(
694        default,
695        deserialize_with = "deserialize_optional_decimal_token_borrowed"
696    )]
697    pub best_bid_amount: Option<Decimal>,
698    /// The order size of all best asks
699    #[serde(
700        default,
701        deserialize_with = "deserialize_optional_decimal_token_borrowed"
702    )]
703    pub best_ask_amount: Option<Decimal>,
704    /// The mark price for the instrument
705    #[serde(
706        default,
707        deserialize_with = "deserialize_optional_decimal_token_borrowed"
708    )]
709    pub mark_price: Option<Decimal>,
710    /// The price for the last trade
711    #[serde(
712        default,
713        deserialize_with = "deserialize_optional_decimal_token_borrowed"
714    )]
715    pub last_price: Option<Decimal>,
716    /// Current index price
717    #[serde(
718        default,
719        deserialize_with = "deserialize_optional_decimal_token_borrowed"
720    )]
721    pub index_price: Option<Decimal>,
722    /// The total amount of outstanding contracts
723    #[serde(
724        default,
725        deserialize_with = "deserialize_optional_decimal_token_borrowed"
726    )]
727    pub open_interest: Option<Decimal>,
728    /// The maximum price for the future
729    #[serde(
730        default,
731        deserialize_with = "deserialize_optional_decimal_token_borrowed"
732    )]
733    pub max_price: Option<Decimal>,
734    /// The minimum price for the future
735    #[serde(
736        default,
737        deserialize_with = "deserialize_optional_decimal_token_borrowed"
738    )]
739    pub min_price: Option<Decimal>,
740    /// Current funding (perpetual only)
741    #[serde(
742        default,
743        deserialize_with = "deserialize_optional_decimal_token_borrowed"
744    )]
745    pub current_funding: Option<Decimal>,
746    /// Funding 8h (perpetual only)
747    #[serde(
748        default,
749        deserialize_with = "deserialize_optional_decimal_token_borrowed"
750    )]
751    pub funding_8h: Option<Decimal>,
752    /// The settlement price for the instrument (when state = open)
753    #[serde(
754        default,
755        deserialize_with = "deserialize_optional_decimal_token_borrowed"
756    )]
757    pub settlement_price: Option<Decimal>,
758    /// The settlement/delivery price for the instrument (when state = closed)
759    #[serde(
760        default,
761        deserialize_with = "deserialize_optional_decimal_token_borrowed"
762    )]
763    pub delivery_price: Option<Decimal>,
764    /// (Only for option) implied volatility for best bid
765    #[serde(
766        default,
767        deserialize_with = "deserialize_optional_decimal_token_borrowed"
768    )]
769    pub bid_iv: Option<Decimal>,
770    /// (Only for option) implied volatility for best ask
771    #[serde(
772        default,
773        deserialize_with = "deserialize_optional_decimal_token_borrowed"
774    )]
775    pub ask_iv: Option<Decimal>,
776    /// (Only for option) implied volatility for mark price
777    #[serde(
778        default,
779        deserialize_with = "deserialize_optional_decimal_token_borrowed"
780    )]
781    pub mark_iv: Option<Decimal>,
782    /// Underlying price for implied volatility calculations (options only)
783    #[serde(
784        default,
785        deserialize_with = "deserialize_optional_decimal_token_borrowed"
786    )]
787    pub underlying_price: Option<Decimal>,
788    /// Name of the underlying future, or index_price (options only)
789    #[serde(default)]
790    pub underlying_index: Option<serde_json::Value>,
791    /// Interest rate used in implied volatility calculations (options only)
792    #[serde(
793        default,
794        deserialize_with = "deserialize_optional_decimal_token_borrowed"
795    )]
796    pub interest_rate: Option<Decimal>,
797}
798
799/// Wire DTO for `/public/get_book_summary_by_currency` entries.
800///
801/// Pure venue payload (no engine timestamps). Convert to the domain
802/// [`crate::data_types::DeribitBookSummary`] before emitting custom data.
803#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
804pub struct DeribitBookSummaryRaw {
805    /// Unique instrument identifier (e.g. "BTC-28MAR25-90000-C")
806    pub instrument_name: String,
807    /// The forward/underlying price for implied volatility calculations
808    #[serde(
809        default,
810        deserialize_with = "deserialize_optional_decimal_token_borrowed"
811    )]
812    pub underlying_price: Option<Decimal>,
813    /// Name of the underlying future or index (e.g. "BTC-28MAR25" or "SYN.BTC-28MAR25")
814    #[serde(default)]
815    pub underlying_index: Option<String>,
816    /// Mark price for the instrument
817    #[serde(
818        default,
819        deserialize_with = "deserialize_optional_decimal_token_borrowed"
820    )]
821    pub mark_price: Option<Decimal>,
822    /// Mid price
823    #[serde(
824        default,
825        deserialize_with = "deserialize_optional_decimal_token_borrowed"
826    )]
827    pub mid_price: Option<Decimal>,
828    /// Best bid price
829    #[serde(
830        default,
831        deserialize_with = "deserialize_optional_decimal_token_borrowed"
832    )]
833    pub bid_price: Option<Decimal>,
834    /// Best ask price
835    #[serde(
836        default,
837        deserialize_with = "deserialize_optional_decimal_token_borrowed"
838    )]
839    pub ask_price: Option<Decimal>,
840    /// Last traded price (`last` on the wire)
841    #[serde(
842        default,
843        rename = "last",
844        deserialize_with = "deserialize_optional_decimal_token_borrowed"
845    )]
846    pub last_price: Option<Decimal>,
847    /// Mark implied volatility (options; may be absent on some product kinds)
848    #[serde(
849        default,
850        deserialize_with = "deserialize_optional_decimal_token_borrowed"
851    )]
852    pub mark_iv: Option<Decimal>,
853    /// Bid implied volatility (options)
854    #[serde(
855        default,
856        deserialize_with = "deserialize_optional_decimal_token_borrowed"
857    )]
858    pub bid_iv: Option<Decimal>,
859    /// Ask implied volatility (options)
860    #[serde(
861        default,
862        deserialize_with = "deserialize_optional_decimal_token_borrowed"
863    )]
864    pub ask_iv: Option<Decimal>,
865    /// Interest rate used in IV calculations (options)
866    #[serde(
867        default,
868        deserialize_with = "deserialize_optional_decimal_token_borrowed"
869    )]
870    pub interest_rate: Option<Decimal>,
871    /// Open interest
872    #[serde(
873        default,
874        deserialize_with = "deserialize_optional_decimal_token_borrowed"
875    )]
876    pub open_interest: Option<Decimal>,
877    /// Open interest value when provided by the venue
878    #[serde(
879        default,
880        deserialize_with = "deserialize_optional_decimal_token_borrowed"
881    )]
882    pub open_interest_value: Option<Decimal>,
883    /// 24h volume in contracts / base units
884    #[serde(
885        default,
886        deserialize_with = "deserialize_optional_decimal_token_borrowed"
887    )]
888    pub volume: Option<Decimal>,
889    /// 24h volume in USD
890    #[serde(
891        default,
892        deserialize_with = "deserialize_optional_decimal_token_borrowed"
893    )]
894    pub volume_usd: Option<Decimal>,
895    /// 24h notional volume
896    #[serde(
897        default,
898        deserialize_with = "deserialize_optional_decimal_token_borrowed"
899    )]
900    pub volume_notional: Option<Decimal>,
901    /// 24h volume in BTC (when provided)
902    #[serde(
903        default,
904        deserialize_with = "deserialize_optional_decimal_token_borrowed"
905    )]
906    pub volume_btc: Option<Decimal>,
907    /// 24h high price
908    #[serde(
909        default,
910        deserialize_with = "deserialize_optional_decimal_token_borrowed"
911    )]
912    pub high: Option<Decimal>,
913    /// 24h low price
914    #[serde(
915        default,
916        deserialize_with = "deserialize_optional_decimal_token_borrowed"
917    )]
918    pub low: Option<Decimal>,
919    /// 24h price change (`price_change` on the wire)
920    #[serde(
921        default,
922        rename = "price_change",
923        deserialize_with = "deserialize_optional_decimal_token_borrowed"
924    )]
925    pub price_change: Option<Decimal>,
926    /// Estimated delivery price
927    #[serde(
928        default,
929        deserialize_with = "deserialize_optional_decimal_token_borrowed"
930    )]
931    pub estimated_delivery_price: Option<Decimal>,
932    /// Settlement/delivery price when present
933    #[serde(
934        default,
935        deserialize_with = "deserialize_optional_decimal_token_borrowed"
936    )]
937    pub delivery_price: Option<Decimal>,
938    /// Base currency
939    #[serde(default)]
940    pub base_currency: Option<String>,
941    /// Quote currency
942    #[serde(default)]
943    pub quote_currency: Option<String>,
944    /// Book summary snapshot time (milliseconds since UNIX epoch)
945    #[serde(default)]
946    pub creation_timestamp: i64,
947}
948
949#[cfg(test)]
950mod book_summary_tests {
951    use rstest::rstest;
952    use rust_decimal_macros::dec;
953
954    use super::*;
955
956    #[rstest]
957    fn test_book_summary_raw_deserializes_full_option_payload() {
958        let json = r#"{
959            "instrument_name": "BTC-28MAR25-90000-C",
960            "underlying_price": 95000.5,
961            "underlying_index": "SYN.BTC-28MAR25",
962            "mark_price": 0.042,
963            "mid_price": 0.041,
964            "bid_price": 0.040,
965            "ask_price": 0.042,
966            "last": 0.0415,
967            "mark_iv": 55.2,
968            "bid_iv": 54.0,
969            "ask_iv": 56.1,
970            "interest_rate": 0.0,
971            "open_interest": 123.5,
972            "volume": 10.0,
973            "volume_usd": 950000.0,
974            "volume_notional": 10.0,
975            "high": 0.05,
976            "low": 0.03,
977            "price_change": 0.01,
978            "estimated_delivery_price": 94900.0,
979            "base_currency": "BTC",
980            "quote_currency": "USD",
981            "creation_timestamp": 1710000000000
982        }"#;
983
984        let summary: DeribitBookSummaryRaw = serde_json::from_str(json).unwrap();
985        assert_eq!(summary.instrument_name, "BTC-28MAR25-90000-C");
986        assert_eq!(summary.underlying_price, Some(dec!(95000.5)));
987        assert_eq!(summary.underlying_index.as_deref(), Some("SYN.BTC-28MAR25"));
988        assert_eq!(summary.mark_price, Some(dec!(0.042)));
989        assert_eq!(summary.last_price, Some(dec!(0.0415)));
990        assert_eq!(summary.mark_iv, Some(dec!(55.2)));
991        assert_eq!(summary.bid_price, Some(dec!(0.040)));
992        assert_eq!(summary.ask_price, Some(dec!(0.042)));
993        assert_eq!(summary.open_interest, Some(dec!(123.5)));
994        assert_eq!(summary.volume_usd, Some(dec!(950000.0)));
995        assert_eq!(summary.price_change, Some(dec!(0.01)));
996        assert_eq!(summary.creation_timestamp, 1_710_000_000_000);
997    }
998
999    #[rstest]
1000    fn test_book_summary_raw_deserializes_minimal_payload() {
1001        let json = r#"{
1002            "instrument_name": "BTC-28MAR25-90000-C",
1003            "creation_timestamp": 1710000000000
1004        }"#;
1005
1006        let summary: DeribitBookSummaryRaw = serde_json::from_str(json).unwrap();
1007        assert_eq!(summary.instrument_name, "BTC-28MAR25-90000-C");
1008        assert!(summary.mark_iv.is_none());
1009        assert!(summary.open_interest.is_none());
1010        assert_eq!(summary.creation_timestamp, 1_710_000_000_000);
1011    }
1012}
1013
1014/// Ticker data from `/public/ticker` endpoint.
1015///
1016/// Selected option ticker fields are included;
1017/// serde will ignore the many additional fields returned by the API.
1018#[derive(Clone, Debug, Serialize, Deserialize)]
1019pub struct DeribitTicker {
1020    /// Unique instrument identifier (e.g., "BTC-28FEB26-65000-C")
1021    pub instrument_name: String,
1022    /// Underlying price for implied volatility calculations (options only)
1023    #[serde(
1024        default,
1025        deserialize_with = "deserialize_optional_decimal_token_borrowed"
1026    )]
1027    pub underlying_price: Option<Decimal>,
1028    /// Name of the underlying future or index (e.g., "BTC-28MAR25" or "SYN.BTC-28MAR25")
1029    #[serde(default)]
1030    pub underlying_index: Option<String>,
1031}
1032
1033/// Position data from `/private/get_positions` endpoint.
1034///
1035/// Contains information about a single position in a specific instrument.
1036#[derive(Debug, Clone, Serialize, Deserialize)]
1037pub struct DeribitPosition {
1038    /// Unique instrument identifier
1039    pub instrument_name: Ustr,
1040    /// Position direction: "buy" (long), "sell" (short), or "zero" (flat)
1041    pub direction: String,
1042    /// Position size in contracts (positive = long, negative = short)
1043    #[serde(
1044        serialize_with = "decimal::serialize",
1045        deserialize_with = "deserialize_decimal_token_or_zero_borrowed"
1046    )]
1047    pub size: Decimal,
1048    /// Average entry price
1049    #[serde(
1050        serialize_with = "decimal::serialize",
1051        deserialize_with = "deserialize_decimal_token_or_zero_borrowed"
1052    )]
1053    pub average_price: Decimal,
1054    /// Current mark price
1055    #[serde(
1056        serialize_with = "decimal::serialize",
1057        deserialize_with = "deserialize_decimal_token_or_zero_borrowed"
1058    )]
1059    pub mark_price: Decimal,
1060    /// Current index price
1061    #[serde(
1062        default,
1063        serialize_with = "decimal::serialize_optional",
1064        deserialize_with = "deserialize_optional_decimal_token_borrowed"
1065    )]
1066    pub index_price: Option<Decimal>,
1067    /// Maintenance margin
1068    #[serde(
1069        serialize_with = "decimal::serialize",
1070        deserialize_with = "deserialize_decimal_token_or_zero_borrowed"
1071    )]
1072    pub maintenance_margin: Decimal,
1073    /// Initial margin
1074    #[serde(
1075        serialize_with = "decimal::serialize",
1076        deserialize_with = "deserialize_decimal_token_or_zero_borrowed"
1077    )]
1078    pub initial_margin: Decimal,
1079    /// Current available leverage for a future position (can be fractional)
1080    #[serde(
1081        default,
1082        serialize_with = "decimal::serialize_optional",
1083        deserialize_with = "deserialize_optional_decimal_token_borrowed"
1084    )]
1085    pub leverage: Option<Decimal>,
1086    /// Current unrealized profit/loss
1087    #[serde(
1088        serialize_with = "decimal::serialize",
1089        deserialize_with = "deserialize_decimal_token_or_zero_borrowed"
1090    )]
1091    pub floating_profit_loss: Decimal,
1092    /// Realized profit/loss for this position
1093    #[serde(
1094        serialize_with = "decimal::serialize",
1095        deserialize_with = "deserialize_decimal_token_or_zero_borrowed"
1096    )]
1097    pub realized_profit_loss: Decimal,
1098    /// Total profit/loss (floating + realized)
1099    #[serde(
1100        serialize_with = "decimal::serialize",
1101        deserialize_with = "deserialize_decimal_token_or_zero_borrowed"
1102    )]
1103    pub total_profit_loss: Decimal,
1104    /// Product type: future, option, spot, etc.
1105    pub kind: DeribitProductType,
1106    /// Position size in currency units (for currency-quoted positions)
1107    #[serde(
1108        default,
1109        serialize_with = "decimal::serialize_optional",
1110        deserialize_with = "deserialize_optional_decimal_token_borrowed"
1111    )]
1112    pub size_currency: Option<Decimal>,
1113    /// Estimated liquidation price
1114    #[serde(
1115        default,
1116        serialize_with = "decimal::serialize_optional",
1117        deserialize_with = "deserialize_optional_decimal_token_borrowed"
1118    )]
1119    pub estimated_liquidation_price: Option<Decimal>,
1120    /// Position delta (for options)
1121    #[serde(
1122        default,
1123        serialize_with = "decimal::serialize_optional",
1124        deserialize_with = "deserialize_optional_decimal_token_borrowed"
1125    )]
1126    pub delta: Option<Decimal>,
1127    /// Position gamma (for options)
1128    #[serde(
1129        default,
1130        serialize_with = "decimal::serialize_optional",
1131        deserialize_with = "deserialize_optional_decimal_token_borrowed"
1132    )]
1133    pub gamma: Option<Decimal>,
1134    /// Position vega (for options)
1135    #[serde(
1136        default,
1137        serialize_with = "decimal::serialize_optional",
1138        deserialize_with = "deserialize_optional_decimal_token_borrowed"
1139    )]
1140    pub vega: Option<Decimal>,
1141    /// Position theta (for options)
1142    #[serde(
1143        default,
1144        serialize_with = "decimal::serialize_optional",
1145        deserialize_with = "deserialize_optional_decimal_token_borrowed"
1146    )]
1147    pub theta: Option<Decimal>,
1148    /// Settlement price (if settled)
1149    #[serde(
1150        default,
1151        serialize_with = "decimal::serialize_optional",
1152        deserialize_with = "deserialize_optional_decimal_token_borrowed"
1153    )]
1154    pub settlement_price: Option<Decimal>,
1155    /// Open orders margin for this position
1156    #[serde(
1157        default,
1158        serialize_with = "decimal::serialize_optional",
1159        deserialize_with = "deserialize_optional_decimal_token_borrowed"
1160    )]
1161    pub open_orders_margin: Option<Decimal>,
1162    /// Average price in USD (for currency-margined contracts)
1163    #[serde(
1164        default,
1165        serialize_with = "decimal::serialize_optional",
1166        deserialize_with = "deserialize_optional_decimal_token_borrowed"
1167    )]
1168    pub average_price_usd: Option<Decimal>,
1169    /// Realized profit loss (session)
1170    #[serde(
1171        default,
1172        serialize_with = "decimal::serialize_optional",
1173        deserialize_with = "deserialize_optional_decimal_token_borrowed"
1174    )]
1175    pub realized_profit_loss_session: Option<Decimal>,
1176    /// Floating profit loss in USD
1177    #[serde(
1178        default,
1179        serialize_with = "decimal::serialize_optional",
1180        deserialize_with = "deserialize_optional_decimal_token_borrowed"
1181    )]
1182    pub floating_profit_loss_usd: Option<Decimal>,
1183}
1184
1185/// Response wrapper for user trades endpoints.
1186///
1187/// Contains the trades array and pagination information.
1188/// Used by `/private/get_user_trades_by_*` endpoints.
1189#[derive(Clone, Debug, Serialize, Deserialize)]
1190pub struct DeribitUserTradesResponse {
1191    /// Whether there are more trades available.
1192    pub has_more: bool,
1193    /// Array of user trade objects.
1194    pub trades: Vec<crate::websocket::messages::DeribitUserTradeMsg>,
1195}
1196
1197#[cfg(test)]
1198mod tests {
1199    use rstest::rstest;
1200    use rust_decimal_macros::dec;
1201
1202    use super::*;
1203
1204    // One decimal field keeps the error independent of `serde_json/preserve_order` key order
1205    #[rstest]
1206    fn test_decimal_fields_reject_value_input() {
1207        let value = serde_json::json!({ "amount": -1, "instrument_name": "BTC-PERPETUAL" });
1208
1209        let error = serde_json::from_value::<DeribitComboLeg>(value).unwrap_err();
1210
1211        assert_eq!(
1212            error.to_string(),
1213            r#"invalid type: string "-1", expected raw value"#
1214        );
1215    }
1216
1217    #[rstest]
1218    #[case::integer(r#""leverage": 50,"#, Some(dec!(50)))]
1219    #[case::whole_float(r#""leverage": 50.0,"#, Some(dec!(50)))]
1220    #[case::fractional(r#""leverage": 35.639422,"#, Some(dec!(35.639422)))]
1221    #[case::null(r#""leverage": null,"#, None)]
1222    #[case::missing("", None)]
1223    fn test_deserialize_position_leverage(#[case] field: &str, #[case] expected: Option<Decimal>) {
1224        let json = format!(
1225            r#"{{
1226                "average_price": 7440.18,
1227                "direction": "buy",
1228                "floating_profit_loss": 0,
1229                "initial_margin": 0.000197283,
1230                "instrument_name": "BTC-PERPETUAL",
1231                "kind": "future",
1232                {field}
1233                "maintenance_margin": 0.000143783,
1234                "mark_price": 7476.65,
1235                "realized_profit_loss": -9e-9,
1236                "size": 50,
1237                "total_profit_loss": 0.000032781
1238            }}"#
1239        );
1240
1241        let position: DeribitPosition = serde_json::from_str(&json).unwrap();
1242
1243        assert_eq!(position.leverage, expected);
1244    }
1245
1246    #[rstest]
1247    fn test_deserialize_public_trade_with_empty_mark_and_index_price() {
1248        let json = r#"{
1249            "amount": 1.0,
1250            "direction": "sell",
1251            "index_price": "",
1252            "instrument_name": "ETH-PERPETUAL",
1253            "mark_price": "",
1254            "price": 2968.3,
1255            "tick_direction": 0,
1256            "timestamp": 1766332040636,
1257            "trade_id": "ETH-123",
1258            "trade_seq": 1
1259        }"#;
1260
1261        let trade: DeribitPublicTrade = serde_json::from_str(json).unwrap();
1262        assert_eq!(trade.index_price, None);
1263        assert_eq!(trade.mark_price, None);
1264        assert_eq!(trade.price, dec!(2968.3));
1265    }
1266
1267    #[rstest]
1268    fn test_deserialize_public_trade_with_missing_mark_and_index_price() {
1269        let json = r#"{
1270            "amount": 1.0,
1271            "direction": "sell",
1272            "instrument_name": "ETH-PERPETUAL",
1273            "price": 2968.3,
1274            "tick_direction": 0,
1275            "timestamp": 1766332040636,
1276            "trade_id": "ETH-123",
1277            "trade_seq": 1
1278        }"#;
1279
1280        let trade: DeribitPublicTrade = serde_json::from_str(json).unwrap();
1281        assert_eq!(trade.index_price, None);
1282        assert_eq!(trade.mark_price, None);
1283    }
1284
1285    #[rstest]
1286    fn test_deserialize_public_trade_with_present_mark_and_index_price() {
1287        let json = r#"{
1288            "amount": 1.0,
1289            "direction": "sell",
1290            "index_price": 2967.73,
1291            "instrument_name": "ETH-PERPETUAL",
1292            "mark_price": 2968.01,
1293            "price": 2968.3,
1294            "tick_direction": 0,
1295            "timestamp": 1766332040636,
1296            "trade_id": "ETH-123",
1297            "trade_seq": 1
1298        }"#;
1299
1300        let trade: DeribitPublicTrade = serde_json::from_str(json).unwrap();
1301        assert_eq!(trade.index_price, Some(dec!(2967.73)));
1302        assert_eq!(trade.mark_price, Some(dec!(2968.01)));
1303    }
1304
1305    #[rstest]
1306    fn test_deserialize_expirations_currency_keyed_response() {
1307        let json = r#"{
1308            "btc": {
1309                "option": ["20MAY26", "26JUN26"],
1310                "future": ["20MAY26", "PERPETUAL"]
1311            }
1312        }"#;
1313
1314        let response: DeribitExpirationsResponse = serde_json::from_str(json).unwrap();
1315        let expirations = response.expirations_for_currency("BTC").unwrap();
1316        assert_eq!(expirations.option, vec!["20MAY26", "26JUN26"]);
1317        assert_eq!(expirations.future, vec!["20MAY26", "PERPETUAL"]);
1318    }
1319
1320    #[rstest]
1321    fn test_deserialize_expirations_direct_response() {
1322        let json = r#"{
1323            "option": ["20MAY26", "26JUN26"],
1324            "future": ["20MAY26", "PERPETUAL"]
1325        }"#;
1326
1327        let response: DeribitExpirationsResponse = serde_json::from_str(json).unwrap();
1328        let expirations = response.expirations_for_currency("any").unwrap();
1329        assert_eq!(expirations.option, vec!["20MAY26", "26JUN26"]);
1330        assert_eq!(expirations.future, vec!["20MAY26", "PERPETUAL"]);
1331    }
1332}