Skip to main content

nautilus_bitmex/websocket/
messages.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//! BitMEX WebSocket message structures and helper types.
17
18use std::collections::HashMap;
19
20use chrono::{DateTime, Utc};
21use rust_decimal::Decimal;
22use serde::{Deserialize, Deserializer, Serialize, de};
23use serde_json::Value;
24use strum::Display;
25use ustr::Ustr;
26use uuid::Uuid;
27
28use super::enums::{
29    BitmexAction, BitmexSide, BitmexTickDirection, BitmexWsAuthAction, BitmexWsOperation,
30};
31use crate::common::enums::{
32    BitmexContingencyType, BitmexExecInstruction, BitmexExecType, BitmexLiquidityIndicator,
33    BitmexOrderStatus, BitmexOrderType, BitmexPegPriceType, BitmexTimeInForce,
34};
35
36/// Custom deserializer for comma-separated `ExecInstruction` values.
37fn deserialize_exec_instructions<'de, D>(
38    deserializer: D,
39) -> Result<Option<Vec<BitmexExecInstruction>>, D::Error>
40where
41    D: serde::Deserializer<'de>,
42{
43    let s: Option<String> = Option::deserialize(deserializer)?;
44    match s {
45        None => Ok(None),
46        Some(ref s) if s.is_empty() => Ok(None),
47        Some(s) => {
48            let instructions: Result<Vec<BitmexExecInstruction>, _> = s
49                .split(',')
50                .map(|inst| {
51                    let trimmed = inst.trim();
52                    match trimmed {
53                        "ParticipateDoNotInitiate" => {
54                            Ok(BitmexExecInstruction::ParticipateDoNotInitiate)
55                        }
56                        "AllOrNone" => Ok(BitmexExecInstruction::AllOrNone),
57                        "MarkPrice" => Ok(BitmexExecInstruction::MarkPrice),
58                        "IndexPrice" => Ok(BitmexExecInstruction::IndexPrice),
59                        "LastPrice" => Ok(BitmexExecInstruction::LastPrice),
60                        "Close" => Ok(BitmexExecInstruction::Close),
61                        "ReduceOnly" => Ok(BitmexExecInstruction::ReduceOnly),
62                        "Fixed" => Ok(BitmexExecInstruction::Fixed),
63                        "" => Ok(BitmexExecInstruction::Unknown),
64                        _ => Err(format!("Unknown exec instruction: {trimmed}")),
65                    }
66                })
67                .collect();
68            instructions.map(Some).map_err(de::Error::custom)
69        }
70    }
71}
72
73/// BitMEX WebSocket authentication message.
74///
75/// The args array contains [api_key, expires/nonce, signature].
76/// The second element must be a number (not a string) for proper authentication.
77#[derive(Debug, Clone, Serialize, Deserialize)]
78pub struct BitmexAuthentication {
79    pub op: BitmexWsAuthAction,
80    pub args: (String, i64, String),
81}
82
83/// BitMEX WebSocket subscription message.
84#[derive(Debug, Clone, Serialize, Deserialize)]
85pub struct BitmexSubscription {
86    pub op: BitmexWsOperation,
87    pub args: Vec<Ustr>,
88}
89
90/// Output message from the BitMEX WebSocket handler.
91///
92/// Contains venue-specific types that consumers parse into Nautilus domain types.
93#[derive(Debug)]
94pub enum BitmexWsMessage {
95    /// Table-based data message from the BitMEX WS stream.
96    Table(BitmexTableMessage),
97    /// Emitted when the underlying WebSocket reconnects.
98    Reconnected,
99    /// Emitted when authentication succeeds.
100    Authenticated,
101}
102
103/// Represents all possible message types from the BitMEX WebSocket API.
104#[derive(Debug, Display, Deserialize)]
105#[serde(untagged)]
106pub(super) enum BitmexWsFrame {
107    /// Table websocket message.
108    Table(BitmexTableMessage),
109    /// Initial welcome message received when connecting to the WebSocket.
110    Welcome {
111        /// Welcome message text.
112        info: String,
113        /// API version string.
114        version: String,
115        /// Server timestamp.
116        timestamp: DateTime<Utc>,
117        /// Link to API documentation.
118        docs: String,
119        /// Whether heartbeat is enabled for this connection.
120        #[serde(rename = "heartbeatEnabled")]
121        heartbeat_enabled: bool,
122        /// Rate limit information (absent on some endpoints).
123        limit: Option<BitmexRateLimit>,
124        /// Application name (testnet only).
125        #[serde(rename = "appName")]
126        app_name: Option<String>,
127    },
128    /// Subscription response messages.
129    Subscription {
130        /// Whether the subscription request was successful.
131        success: bool,
132        /// The subscription topic if successful.
133        subscribe: Option<String>,
134        /// Original request metadata (present for subscribe/auth/unsubscribe).
135        request: Option<BitmexHttpRequest>,
136        /// Error message if subscription failed.
137        error: Option<String>,
138    },
139    /// WebSocket error message.
140    Error {
141        status: u16,
142        error: String,
143        meta: HashMap<String, String>,
144        request: BitmexHttpRequest,
145    },
146    /// Indicates a WebSocket reconnection has completed.
147    #[serde(skip)]
148    Reconnected,
149}
150
151#[derive(Clone, Debug, Deserialize, PartialEq, Eq)]
152pub struct BitmexHttpRequest {
153    pub op: String,
154    pub args: Vec<Value>,
155}
156
157/// Rate limit information from BitMEX API.
158#[derive(Debug, Deserialize)]
159pub struct BitmexRateLimit {
160    /// Number of requests remaining in the current time window.
161    pub remaining: Option<i32>,
162}
163
164/// Represents table-based messages.
165#[derive(Debug, Display, Deserialize)]
166#[serde(rename_all = "camelCase")]
167#[serde(tag = "table")]
168pub enum BitmexTableMessage {
169    OrderBookL2 {
170        action: BitmexAction,
171        data: Vec<BitmexOrderBookMsg>,
172    },
173    OrderBookL2_25 {
174        action: BitmexAction,
175        data: Vec<BitmexOrderBookMsg>,
176    },
177    OrderBook10 {
178        action: BitmexAction,
179        data: Vec<BitmexOrderBook10Msg>,
180    },
181    Quote {
182        action: BitmexAction,
183        data: Vec<BitmexQuoteMsg>,
184    },
185    Trade {
186        action: BitmexAction,
187        data: Vec<BitmexTradeMsg>,
188    },
189    TradeBin1m {
190        action: BitmexAction,
191        data: Vec<BitmexTradeBinMsg>,
192    },
193    TradeBin5m {
194        action: BitmexAction,
195        data: Vec<BitmexTradeBinMsg>,
196    },
197    TradeBin1h {
198        action: BitmexAction,
199        data: Vec<BitmexTradeBinMsg>,
200    },
201    TradeBin1d {
202        action: BitmexAction,
203        data: Vec<BitmexTradeBinMsg>,
204    },
205    Instrument {
206        action: BitmexAction,
207        data: Vec<BitmexInstrumentMsg>,
208    },
209    Order {
210        action: BitmexAction,
211        #[serde(deserialize_with = "deserialize_order_data")]
212        data: Vec<OrderData>,
213    },
214    Execution {
215        action: BitmexAction,
216        data: Vec<BitmexExecutionMsg>,
217    },
218    Position {
219        action: BitmexAction,
220        data: Vec<BitmexPositionMsg>,
221    },
222    Wallet {
223        action: BitmexAction,
224        data: Vec<BitmexWalletMsg>,
225    },
226    Margin {
227        action: BitmexAction,
228        data: Vec<BitmexMarginMsg>,
229    },
230    Funding {
231        action: BitmexAction,
232        data: Vec<BitmexFundingMsg>,
233    },
234    Insurance {
235        action: BitmexAction,
236        data: Vec<BitmexInsuranceMsg>,
237    },
238    Liquidation {
239        action: BitmexAction,
240        data: Vec<BitmexLiquidationMsg>,
241    },
242}
243
244/// Represents a single order book entry in the BitMEX order book.
245#[derive(Clone, Debug, Deserialize)]
246#[serde(rename_all = "camelCase")]
247pub struct BitmexOrderBookMsg {
248    /// The instrument symbol (e.g., "XBTUSD").
249    pub symbol: Ustr,
250    /// Unique order ID.
251    pub id: u64,
252    /// Side of the order ("Buy" or "Sell").
253    pub side: BitmexSide,
254    /// Size of the order, can be None for deletes.
255    pub size: Option<u64>,
256    /// Price level of the order.
257    pub price: f64,
258    /// Timestamp of the update.
259    pub timestamp: DateTime<Utc>,
260    /// Timestamp of the transaction.
261    pub transact_time: DateTime<Utc>,
262    pub pool: Option<Ustr>,
263}
264
265/// Represents a single order book entry in the BitMEX order book.
266#[derive(Clone, Debug, Deserialize)]
267#[serde(rename_all = "camelCase")]
268pub struct BitmexOrderBook10Msg {
269    /// The instrument symbol (e.g., "XBTUSD").
270    pub symbol: Ustr,
271    /// Array of bid levels, each containing [price, size].
272    pub bids: Vec<[f64; 2]>,
273    /// Array of ask levels, each containing [price, size].
274    pub asks: Vec<[f64; 2]>,
275    /// Timestamp of the orderbook snapshot.
276    pub timestamp: DateTime<Utc>,
277    pub pool: Option<Ustr>,
278}
279
280/// Represents a top-of-book quote.
281#[derive(Clone, Debug, Deserialize)]
282#[serde(rename_all = "camelCase")]
283pub struct BitmexQuoteMsg {
284    /// The instrument symbol (e.g., "XBTUSD").
285    pub symbol: Ustr,
286    /// Price of best bid.
287    pub bid_price: Option<f64>,
288    /// Size of best bid.
289    pub bid_size: Option<u64>,
290    /// Price of best ask.
291    pub ask_price: Option<f64>,
292    /// Size of best ask.
293    pub ask_size: Option<u64>,
294    /// Timestamp of the quote.
295    pub timestamp: DateTime<Utc>,
296    pub pool: Option<Ustr>,
297}
298
299/// Represents a single trade execution on BitMEX.
300#[derive(Clone, Debug, Deserialize)]
301#[serde(rename_all = "camelCase")]
302pub struct BitmexTradeMsg {
303    /// Timestamp of the trade.
304    pub timestamp: DateTime<Utc>,
305    /// The instrument symbol.
306    pub symbol: Ustr,
307    /// Side of the trade ("Buy" or "Sell").
308    pub side: BitmexSide,
309    /// Size of the trade.
310    pub size: u64,
311    /// Price the trade executed at.
312    pub price: f64,
313    /// Direction of the tick ("`PlusTick`", "`MinusTick`", "`ZeroPlusTick`", "`ZeroMinusTick`").
314    pub tick_direction: BitmexTickDirection,
315    /// Unique trade match ID.
316    #[serde(rename = "trdMatchID")]
317    pub trd_match_id: Option<Uuid>,
318    /// Gross value of the trade in satoshis.
319    pub gross_value: Option<i64>,
320    /// Home currency value of the trade.
321    pub home_notional: Option<f64>,
322    /// Foreign currency value of the trade.
323    pub foreign_notional: Option<f64>,
324    /// Trade type.
325    #[serde(rename = "trdType")]
326    pub trade_type: Ustr, // TODO: Add enum
327    pub pool: Option<Ustr>,
328}
329
330#[derive(Clone, Debug, Deserialize)]
331#[serde(rename_all = "camelCase")]
332pub struct BitmexTradeBinMsg {
333    /// Start time of the bin.
334    pub timestamp: DateTime<Utc>,
335    /// Trading instrument symbol.
336    pub symbol: Ustr,
337    /// Opening price for the period.
338    pub open: f64,
339    /// Highest price for the period.
340    pub high: f64,
341    /// Lowest price for the period.
342    pub low: f64,
343    /// Closing price for the period.
344    pub close: f64,
345    /// Number of trades in the period.
346    pub trades: i64,
347    /// Volume traded in the period.
348    pub volume: i64,
349    /// Volume weighted average price (None when trades=0).
350    pub vwap: Option<f64>,
351    /// Size of the last trade in the period (None when trades=0).
352    pub last_size: Option<i64>,
353    /// Turnover in satoshis.
354    pub turnover: i64,
355    /// Home currency volume.
356    pub home_notional: f64,
357    /// Foreign currency volume.
358    pub foreign_notional: f64,
359    pub pool: Option<Ustr>,
360}
361
362/// Represents a single order book entry in the BitMEX order book.
363#[derive(Clone, Debug, Deserialize)]
364#[serde(rename_all = "camelCase")]
365pub struct BitmexInstrumentMsg {
366    pub symbol: Ustr,
367    pub root_symbol: Option<Ustr>,
368    pub state: Option<Ustr>,
369    #[serde(rename = "typ")]
370    pub instrument_type: Option<Ustr>,
371    pub listing: Option<DateTime<Utc>>,
372    pub front: Option<DateTime<Utc>>,
373    pub expiry: Option<DateTime<Utc>>,
374    pub settle: Option<DateTime<Utc>>,
375    pub listed_settle: Option<DateTime<Utc>>,
376    pub position_currency: Option<Ustr>,
377    pub underlying: Option<Ustr>,
378    pub quote_currency: Option<Ustr>,
379    pub underlying_symbol: Option<Ustr>,
380    pub reference: Option<Ustr>,
381    pub reference_symbol: Option<Ustr>,
382    pub max_order_qty: Option<f64>,
383    pub max_price: Option<f64>,
384    pub min_price: Option<f64>,
385    pub lot_size: Option<f64>,
386    pub tick_size: Option<f64>,
387    pub multiplier: Option<f64>,
388    pub settl_currency: Option<Ustr>,
389    pub underlying_to_position_multiplier: Option<f64>,
390    pub underlying_to_settle_multiplier: Option<f64>,
391    pub quote_to_settle_multiplier: Option<f64>,
392    pub is_quanto: Option<bool>,
393    pub is_inverse: Option<bool>,
394    pub init_margin: Option<f64>,
395    pub maint_margin: Option<f64>,
396    pub risk_limit: Option<f64>,
397    pub risk_step: Option<f64>,
398    pub maker_fee: Option<f64>,
399    pub taker_fee: Option<f64>,
400    pub settlement_fee: Option<f64>,
401    pub funding_base_symbol: Option<Ustr>,
402    pub funding_quote_symbol: Option<Ustr>,
403    pub funding_premium_symbol: Option<Ustr>,
404    pub funding_timestamp: Option<DateTime<Utc>>,
405    pub funding_interval: Option<DateTime<Utc>>,
406    #[serde(default, with = "rust_decimal::serde::float_option")]
407    pub funding_rate: Option<Decimal>,
408    #[serde(default, with = "rust_decimal::serde::float_option")]
409    pub indicative_funding_rate: Option<Decimal>,
410    pub last_price: Option<f64>,
411    pub last_tick_direction: Option<BitmexTickDirection>,
412    pub mark_price: Option<f64>,
413    pub mark_method: Option<Ustr>,
414    pub index_price: Option<f64>,
415    pub indicative_settle_price: Option<f64>,
416    pub indicative_tax_rate: Option<f64>,
417    pub open_interest: Option<i64>,
418    pub open_value: Option<i64>,
419    pub fair_basis: Option<f64>,
420    pub fair_basis_rate: Option<f64>,
421    pub fair_price: Option<f64>,
422    pub timestamp: DateTime<Utc>,
423}
424
425impl TryFrom<BitmexInstrumentMsg> for crate::http::models::BitmexInstrument {
426    type Error = anyhow::Error;
427
428    fn try_from(msg: BitmexInstrumentMsg) -> Result<Self, Self::Error> {
429        use crate::common::enums::{BitmexInstrumentState, BitmexInstrumentType};
430
431        // Required fields
432        let root_symbol = msg
433            .root_symbol
434            .ok_or_else(|| anyhow::anyhow!("Missing root_symbol for {}", msg.symbol))?;
435        let underlying = msg
436            .underlying
437            .ok_or_else(|| anyhow::anyhow!("Missing underlying for {}", msg.symbol))?;
438        let quote_currency = msg
439            .quote_currency
440            .ok_or_else(|| anyhow::anyhow!("Missing quote_currency for {}", msg.symbol))?;
441        let tick_size = msg
442            .tick_size
443            .ok_or_else(|| anyhow::anyhow!("Missing tick_size for {}", msg.symbol))?;
444        let multiplier = msg
445            .multiplier
446            .ok_or_else(|| anyhow::anyhow!("Missing multiplier for {}", msg.symbol))?;
447        let is_quanto = msg
448            .is_quanto
449            .ok_or_else(|| anyhow::anyhow!("Missing is_quanto for {}", msg.symbol))?;
450        let is_inverse = msg
451            .is_inverse
452            .ok_or_else(|| anyhow::anyhow!("Missing is_inverse for {}", msg.symbol))?;
453
454        // Parse state - default to Open if not present
455        let state = msg
456            .state
457            .and_then(|s| serde_json::from_str::<BitmexInstrumentState>(&format!("\"{s}\"")).ok())
458            .unwrap_or(BitmexInstrumentState::Open);
459
460        // Parse instrument type - default to PerpetualContract if not present
461        let instrument_type = msg
462            .instrument_type
463            .and_then(|t| serde_json::from_str::<BitmexInstrumentType>(&format!("\"{t}\"")).ok())
464            .unwrap_or(BitmexInstrumentType::PerpetualContract);
465
466        Ok(Self {
467            symbol: msg.symbol,
468            root_symbol,
469            state,
470            instrument_type,
471            listing: msg.listing,
472            front: msg.front,
473            expiry: msg.expiry,
474            settle: msg.settle,
475            listed_settle: msg.listed_settle,
476            position_currency: msg.position_currency,
477            underlying,
478            quote_currency,
479            underlying_symbol: msg.underlying_symbol,
480            reference: msg.reference,
481            reference_symbol: msg.reference_symbol,
482            calc_interval: None,
483            publish_interval: None,
484            publish_time: None,
485            max_order_qty: msg.max_order_qty,
486            max_price: msg.max_price,
487            min_price: msg.min_price,
488            lot_size: msg.lot_size,
489            tick_size,
490            multiplier,
491            settl_currency: msg.settl_currency,
492            underlying_to_position_multiplier: msg.underlying_to_position_multiplier,
493            underlying_to_settle_multiplier: msg.underlying_to_settle_multiplier,
494            quote_to_settle_multiplier: msg.quote_to_settle_multiplier,
495            is_quanto,
496            is_inverse,
497            init_margin: msg.init_margin,
498            maint_margin: msg.maint_margin,
499            risk_limit: msg.risk_limit,
500            risk_step: msg.risk_step,
501            limit: None,
502            taxed: None,
503            deleverage: None,
504            maker_fee: msg.maker_fee,
505            taker_fee: msg.taker_fee,
506            settlement_fee: msg.settlement_fee,
507            funding_base_symbol: msg.funding_base_symbol,
508            funding_quote_symbol: msg.funding_quote_symbol,
509            funding_premium_symbol: msg.funding_premium_symbol,
510            funding_timestamp: msg.funding_timestamp,
511            funding_interval: msg.funding_interval,
512            funding_rate: msg.funding_rate,
513            indicative_funding_rate: msg.indicative_funding_rate,
514            rebalance_timestamp: None,
515            rebalance_interval: None,
516            prev_close_price: None,
517            limit_down_price: None,
518            limit_up_price: None,
519            prev_total_volume: None,
520            total_volume: None,
521            volume: None,
522            volume_24h: None,
523            prev_total_turnover: None,
524            total_turnover: None,
525            turnover: None,
526            turnover_24h: None,
527            home_notional_24h: None,
528            foreign_notional_24h: None,
529            prev_price_24h: None,
530            vwap: None,
531            high_price: None,
532            low_price: None,
533            last_price: msg.last_price,
534            last_price_protected: None,
535            last_tick_direction: None, // WebSocket uses different enum, skip for now
536            last_change_pcnt: None,
537            bid_price: None,
538            mid_price: None,
539            ask_price: None,
540            impact_bid_price: None,
541            impact_mid_price: None,
542            impact_ask_price: None,
543            has_liquidity: None,
544            open_interest: msg.open_interest.map(|v| v as f64),
545            open_value: msg.open_value.map(|v| v as f64),
546            fair_method: None,
547            fair_basis_rate: msg.fair_basis_rate,
548            fair_basis: msg.fair_basis,
549            fair_price: msg.fair_price,
550            mark_method: None,
551            mark_price: msg.mark_price,
552            indicative_settle_price: msg.indicative_settle_price,
553            settled_price_adjustment_rate: None,
554            settled_price: None,
555            instant_pnl: false,
556            min_tick: None,
557            funding_base_rate: None,
558            funding_quote_rate: None,
559            capped: None,
560            opening_timestamp: None,
561            closing_timestamp: None,
562            timestamp: msg.timestamp,
563        })
564    }
565}
566
567/// Represents an order update message with only changed fields.
568/// Used for `update` actions where only modified fields are sent.
569#[derive(Clone, Debug, Deserialize)]
570#[serde(rename_all = "camelCase")]
571pub struct BitmexOrderUpdateMsg {
572    #[serde(rename = "orderID")]
573    pub order_id: Uuid,
574    #[serde(rename = "clOrdID")]
575    pub cl_ord_id: Option<Ustr>,
576    pub account: i64,
577    pub symbol: Ustr,
578    pub side: Option<BitmexSide>,
579    pub price: Option<f64>,
580    pub currency: Option<Ustr>,
581    pub text: Option<Ustr>,
582    pub transact_time: Option<DateTime<Utc>>,
583    pub timestamp: Option<DateTime<Utc>>,
584    pub leaves_qty: Option<i64>,
585    pub cum_qty: Option<i64>,
586    pub ord_status: Option<BitmexOrderStatus>,
587}
588
589/// Represents a full order message from the WebSocket stream.
590/// Used for `insert` and `partial` actions where all fields are present.
591#[derive(Clone, Debug, Deserialize)]
592#[serde(rename_all = "camelCase")]
593pub struct BitmexOrderMsg {
594    #[serde(rename = "orderID")]
595    pub order_id: Uuid,
596    #[serde(rename = "clOrdID")]
597    pub cl_ord_id: Option<Ustr>,
598    #[serde(rename = "clOrdLinkID")]
599    pub cl_ord_link_id: Option<Ustr>,
600    pub account: i64,
601    pub symbol: Ustr,
602    pub side: BitmexSide,
603    pub order_qty: i64,
604    pub price: Option<f64>,
605    pub display_qty: Option<i64>,
606    pub stop_px: Option<f64>,
607    pub peg_offset_value: Option<f64>,
608    pub peg_price_type: Option<BitmexPegPriceType>,
609    pub currency: Ustr,
610    pub settl_currency: Ustr,
611    pub ord_type: Option<BitmexOrderType>,
612    pub time_in_force: Option<BitmexTimeInForce>,
613    #[serde(default, deserialize_with = "deserialize_exec_instructions")]
614    pub exec_inst: Option<Vec<BitmexExecInstruction>>,
615    pub contingency_type: Option<BitmexContingencyType>,
616    pub ord_status: BitmexOrderStatus,
617    pub triggered: Option<Ustr>,
618    pub working_indicator: bool,
619    pub ord_rej_reason: Option<Ustr>,
620    pub leaves_qty: i64,
621    pub cum_qty: i64,
622    pub avg_px: Option<f64>,
623    pub text: Option<Ustr>,
624    pub transact_time: DateTime<Utc>,
625    pub timestamp: DateTime<Utc>,
626    pub strategy: Option<Ustr>,
627    pub pool: Option<Ustr>,
628}
629
630/// Wrapper enum for order data that can be either full or update messages.
631#[derive(Clone, Debug)]
632pub enum OrderData {
633    Full(BitmexOrderMsg),
634    Update(BitmexOrderUpdateMsg),
635}
636
637/// Custom deserializer for order data that tries to deserialize as full message first,
638/// then falls back to update message if fields are missing.
639fn deserialize_order_data<'de, D>(deserializer: D) -> Result<Vec<OrderData>, D::Error>
640where
641    D: Deserializer<'de>,
642{
643    let raw_values: Vec<serde_json::Value> = Vec::deserialize(deserializer)?;
644    let mut result = Vec::new();
645
646    for value in raw_values {
647        // Try to deserialize as full message first
648        if let Ok(full_msg) = serde_json::from_value::<BitmexOrderMsg>(value.clone()) {
649            result.push(OrderData::Full(full_msg));
650        } else if let Ok(update_msg) = serde_json::from_value::<BitmexOrderUpdateMsg>(value) {
651            result.push(OrderData::Update(update_msg));
652        } else {
653            return Err(de::Error::custom(
654                "Failed to deserialize order data as either full or update message",
655            ));
656        }
657    }
658
659    Ok(result)
660}
661
662/// Raw Order and Balance Data.
663#[derive(Clone, Debug, Deserialize)]
664#[serde(rename_all = "camelCase")]
665pub struct BitmexExecutionMsg {
666    #[serde(rename = "execID")]
667    pub exec_id: Option<Uuid>,
668    #[serde(rename = "orderID")]
669    pub order_id: Option<Uuid>,
670    #[serde(rename = "clOrdID")]
671    pub cl_ord_id: Option<Ustr>,
672    #[serde(rename = "clOrdLinkID")]
673    pub cl_ord_link_id: Option<Ustr>,
674    pub account: Option<i64>,
675    pub symbol: Option<Ustr>,
676    pub side: Option<BitmexSide>,
677    pub last_qty: Option<i64>,
678    pub last_px: Option<f64>,
679    pub underlying_last_px: Option<f64>,
680    pub last_mkt: Option<Ustr>,
681    pub last_liquidity_ind: Option<BitmexLiquidityIndicator>,
682    pub order_qty: Option<i64>,
683    pub price: Option<f64>,
684    pub display_qty: Option<i64>,
685    pub stop_px: Option<f64>,
686    pub peg_offset_value: Option<f64>,
687    pub peg_price_type: Option<BitmexPegPriceType>,
688    pub currency: Option<Ustr>,
689    pub settl_currency: Option<Ustr>,
690    pub exec_type: Option<BitmexExecType>,
691    pub ord_type: Option<BitmexOrderType>,
692    pub time_in_force: Option<BitmexTimeInForce>,
693    #[serde(default, deserialize_with = "deserialize_exec_instructions")]
694    pub exec_inst: Option<Vec<BitmexExecInstruction>>,
695    pub contingency_type: Option<BitmexContingencyType>,
696    pub ex_destination: Option<Ustr>,
697    pub ord_status: Option<BitmexOrderStatus>,
698    pub triggered: Option<Ustr>,
699    pub working_indicator: Option<bool>,
700    pub ord_rej_reason: Option<Ustr>,
701    pub leaves_qty: Option<i64>,
702    pub cum_qty: Option<i64>,
703    pub avg_px: Option<f64>,
704    pub commission: Option<f64>,
705    pub trade_publish_indicator: Option<Ustr>,
706    pub multi_leg_reporting_type: Option<Ustr>,
707    pub text: Option<Ustr>,
708    #[serde(rename = "trdMatchID")]
709    pub trd_match_id: Option<Uuid>,
710    pub exec_cost: Option<i64>,
711    pub exec_comm: Option<i64>,
712    pub home_notional: Option<f64>,
713    pub foreign_notional: Option<f64>,
714    pub transact_time: Option<DateTime<Utc>>,
715    pub timestamp: Option<DateTime<Utc>>,
716    pub strategy: Option<Ustr>,
717    pub pool: Option<Ustr>,
718    pub exec_comm_ccy: Option<Ustr>,
719}
720
721/// Position status.
722#[derive(Clone, Debug, Deserialize)]
723#[serde(rename_all = "camelCase")]
724pub struct BitmexPositionMsg {
725    pub account: i64,
726    pub symbol: Ustr,
727    pub currency: Option<Ustr>,
728    pub underlying: Option<Ustr>,
729    pub quote_currency: Option<Ustr>,
730    pub commission: Option<f64>,
731    pub init_margin_req: Option<f64>,
732    pub maint_margin_req: Option<f64>,
733    pub risk_limit: Option<i64>,
734    pub leverage: Option<f64>,
735    pub cross_margin: Option<bool>,
736    pub deleverage_percentile: Option<f64>,
737    pub rebalanced_pnl: Option<i64>,
738    pub prev_realised_pnl: Option<i64>,
739    pub prev_unrealised_pnl: Option<i64>,
740    pub prev_close_price: Option<f64>,
741    pub opening_timestamp: Option<DateTime<Utc>>,
742    pub opening_qty: Option<i64>,
743    pub opening_cost: Option<i64>,
744    pub opening_comm: Option<i64>,
745    pub open_order_buy_qty: Option<i64>,
746    pub open_order_buy_cost: Option<i64>,
747    pub open_order_buy_premium: Option<i64>,
748    pub open_order_sell_qty: Option<i64>,
749    pub open_order_sell_cost: Option<i64>,
750    pub open_order_sell_premium: Option<i64>,
751    pub exec_buy_qty: Option<i64>,
752    pub exec_buy_cost: Option<i64>,
753    pub exec_sell_qty: Option<i64>,
754    pub exec_sell_cost: Option<i64>,
755    pub exec_qty: Option<i64>,
756    pub exec_cost: Option<i64>,
757    pub exec_comm: Option<i64>,
758    pub current_timestamp: Option<DateTime<Utc>>,
759    pub current_qty: Option<i64>,
760    pub current_cost: Option<i64>,
761    pub current_comm: Option<i64>,
762    pub realised_cost: Option<i64>,
763    pub unrealised_cost: Option<i64>,
764    pub gross_open_cost: Option<i64>,
765    pub gross_open_premium: Option<i64>,
766    pub gross_exec_cost: Option<i64>,
767    pub is_open: Option<bool>,
768    pub mark_price: Option<f64>,
769    pub mark_value: Option<i64>,
770    pub risk_value: Option<i64>,
771    pub home_notional: Option<f64>,
772    pub foreign_notional: Option<f64>,
773    pub pos_state: Option<Ustr>,
774    pub pos_cost: Option<i64>,
775    pub pos_cost2: Option<i64>,
776    pub pos_cross: Option<i64>,
777    pub pos_init: Option<i64>,
778    pub pos_comm: Option<i64>,
779    pub pos_loss: Option<i64>,
780    pub pos_margin: Option<i64>,
781    pub pos_maint: Option<i64>,
782    pub pos_allowance: Option<i64>,
783    pub taxable_margin: Option<i64>,
784    pub init_margin: Option<i64>,
785    pub maint_margin: Option<i64>,
786    pub session_margin: Option<i64>,
787    pub target_excess_margin: Option<i64>,
788    pub var_margin: Option<i64>,
789    pub realised_gross_pnl: Option<i64>,
790    pub realised_tax: Option<i64>,
791    pub realised_pnl: Option<i64>,
792    pub unrealised_gross_pnl: Option<i64>,
793    pub long_bankrupt: Option<i64>,
794    pub short_bankrupt: Option<i64>,
795    pub tax_base: Option<i64>,
796    pub indicative_tax_rate: Option<f64>,
797    pub indicative_tax: Option<i64>,
798    pub unrealised_tax: Option<i64>,
799    pub unrealised_pnl: Option<i64>,
800    pub unrealised_pnl_pcnt: Option<f64>,
801    pub unrealised_roe_pcnt: Option<f64>,
802    pub avg_cost_price: Option<f64>,
803    pub avg_entry_price: Option<f64>,
804    pub break_even_price: Option<f64>,
805    pub margin_call_price: Option<f64>,
806    pub liquidation_price: Option<f64>,
807    pub bankrupt_price: Option<f64>,
808    pub timestamp: Option<DateTime<Utc>>,
809    pub last_price: Option<f64>,
810    pub last_value: Option<i64>,
811    pub strategy: Option<Ustr>,
812}
813
814#[derive(Clone, Debug, Deserialize)]
815#[serde(rename_all = "camelCase")]
816pub struct BitmexWalletMsg {
817    pub account: i64,
818    pub currency: Ustr,
819    pub prev_deposited: Option<i64>,
820    pub prev_withdrawn: Option<i64>,
821    pub prev_transfer_in: Option<i64>,
822    pub prev_transfer_out: Option<i64>,
823    pub prev_amount: Option<i64>,
824    pub prev_timestamp: Option<DateTime<Utc>>,
825    pub delta_deposited: Option<i64>,
826    pub delta_withdrawn: Option<i64>,
827    pub delta_transfer_in: Option<i64>,
828    pub delta_transfer_out: Option<i64>,
829    pub delta_amount: Option<i64>,
830    pub deposited: Option<i64>,
831    pub withdrawn: Option<i64>,
832    pub transfer_in: Option<i64>,
833    pub transfer_out: Option<i64>,
834    pub amount: Option<i64>,
835    pub pending_credit: Option<i64>,
836    pub pending_debit: Option<i64>,
837    pub confirmed_debit: Option<i64>,
838    pub timestamp: Option<DateTime<Utc>>,
839    pub addr: Option<Ustr>,
840    pub script: Option<Ustr>,
841    pub withdrawal_lock: Option<Vec<Ustr>>,
842}
843
844/// Represents margin account information
845#[derive(Clone, Debug, Deserialize)]
846#[serde(rename_all = "camelCase")]
847pub struct BitmexMarginMsg {
848    /// Account identifier
849    pub account: i64,
850    /// Currency of the margin account
851    pub currency: Ustr,
852    /// Risk limit for the account
853    pub risk_limit: Option<i64>,
854    /// Current amount in the account
855    pub amount: Option<i64>,
856    /// Previously realized PnL
857    pub prev_realised_pnl: Option<i64>,
858    /// Gross commission
859    pub gross_comm: Option<i64>,
860    /// Gross open cost
861    pub gross_open_cost: Option<i64>,
862    /// Gross open premium
863    pub gross_open_premium: Option<i64>,
864    /// Gross execution cost
865    pub gross_exec_cost: Option<i64>,
866    /// Gross mark value
867    pub gross_mark_value: Option<i64>,
868    /// Risk value
869    pub risk_value: Option<i64>,
870    /// Initial margin requirement
871    pub init_margin: Option<i64>,
872    /// Maintenance margin requirement
873    pub maint_margin: Option<i64>,
874    /// Target excess margin
875    pub target_excess_margin: Option<i64>,
876    /// Realized profit and loss
877    pub realised_pnl: Option<i64>,
878    /// Unrealized profit and loss
879    pub unrealised_pnl: Option<i64>,
880    /// Wallet balance
881    pub wallet_balance: Option<i64>,
882    /// Margin balance
883    pub margin_balance: Option<i64>,
884    /// Margin leverage
885    pub margin_leverage: Option<f64>,
886    /// Margin used percentage
887    pub margin_used_pcnt: Option<f64>,
888    /// Excess margin
889    pub excess_margin: Option<i64>,
890    /// Available margin
891    pub available_margin: Option<i64>,
892    /// Withdrawable margin
893    pub withdrawable_margin: Option<i64>,
894    /// Maker fee discount
895    pub maker_fee_discount: Option<f64>,
896    /// Taker fee discount
897    pub taker_fee_discount: Option<f64>,
898    /// Timestamp of the margin update
899    pub timestamp: DateTime<Utc>,
900    /// Foreign margin balance
901    pub foreign_margin_balance: Option<i64>,
902    /// Foreign margin requirement
903    pub foreign_requirement: Option<i64>,
904}
905
906/// Represents a funding rate update.
907#[derive(Clone, Debug, Deserialize)]
908#[serde(rename_all = "camelCase")]
909pub struct BitmexFundingMsg {
910    /// Timestamp of the funding update.
911    pub timestamp: DateTime<Utc>,
912    /// The instrument symbol the funding applies to.
913    pub symbol: Ustr,
914    /// The interval for this funding.
915    pub funding_interval: DateTime<Utc>,
916    /// The funding rate for this interval.
917    #[serde(with = "rust_decimal::serde::float")]
918    pub funding_rate: Decimal,
919    /// The daily funding rate.
920    #[serde(with = "rust_decimal::serde::float")]
921    pub funding_rate_daily: Decimal,
922}
923
924/// Represents an insurance fund update.
925#[derive(Clone, Debug, Deserialize)]
926#[serde(rename_all = "camelCase")]
927pub struct BitmexInsuranceMsg {
928    /// The currency of the insurance fund.
929    pub currency: Ustr,
930    /// Timestamp of the update.
931    pub timestamp: DateTime<Utc>,
932    /// Current balance of the insurance wallet.
933    pub wallet_balance: i64,
934}
935
936/// Represents a liquidation order.
937#[derive(Clone, Debug, Deserialize)]
938#[serde(rename_all = "camelCase")]
939pub struct BitmexLiquidationMsg {
940    /// Unique order ID of the liquidation.
941    pub order_id: Ustr,
942    /// The instrument symbol being liquidated.
943    pub symbol: Ustr,
944    /// Side of the liquidation ("Buy" or "Sell").
945    pub side: BitmexSide,
946    /// Price of the liquidation order.
947    pub price: f64,
948    /// Remaining quantity to be executed.
949    pub leaves_qty: i64,
950}
951
952#[cfg(test)]
953mod tests {
954    use rstest::rstest;
955
956    use super::*;
957
958    #[rstest]
959    fn test_try_from_instrument_msg_with_full_data_success() {
960        let json_data = r#"{
961            "symbol": "XBTUSD",
962            "rootSymbol": "XBT",
963            "state": "Open",
964            "typ": "FFWCSX",
965            "listing": "2016-05-13T12:00:00.000Z",
966            "front": "2016-05-13T12:00:00.000Z",
967            "positionCurrency": "USD",
968            "underlying": "XBT",
969            "quoteCurrency": "USD",
970            "underlyingSymbol": "XBT=",
971            "reference": "BMEX",
972            "referenceSymbol": ".BXBT",
973            "maxOrderQty": 10000000,
974            "maxPrice": 1000000,
975            "lotSize": 100,
976            "tickSize": 0.1,
977            "multiplier": -100000000,
978            "settlCurrency": "XBt",
979            "underlyingToSettleMultiplier": -100000000,
980            "isQuanto": false,
981            "isInverse": true,
982            "initMargin": 0.01,
983            "maintMargin": 0.005,
984            "riskLimit": 20000000000,
985            "riskStep": 15000000000,
986            "taxed": true,
987            "deleverage": true,
988            "makerFee": 0.0005,
989            "takerFee": 0.0005,
990            "settlementFee": 0,
991            "fundingBaseSymbol": ".XBTBON8H",
992            "fundingQuoteSymbol": ".USDBON8H",
993            "fundingPremiumSymbol": ".XBTUSDPI8H",
994            "fundingTimestamp": "2024-11-25T04:00:00.000Z",
995            "fundingInterval": "2000-01-01T08:00:00.000Z",
996            "fundingRate": 0.00011,
997            "indicativeFundingRate": 0.000125,
998            "prevClosePrice": 97409.63,
999            "limitDownPrice": null,
1000            "limitUpPrice": null,
1001            "prevTotalVolume": 3868480147789,
1002            "totalVolume": 3868507398889,
1003            "volume": 27251100,
1004            "volume24h": 419742700,
1005            "prevTotalTurnover": 37667656761390205,
1006            "totalTurnover": 37667684492745237,
1007            "turnover": 27731355032,
1008            "turnover24h": 431762899194,
1009            "homeNotional24h": 4317.62899194,
1010            "foreignNotional24h": 419742700,
1011            "prevPrice24h": 97655,
1012            "vwap": 97216.6863,
1013            "highPrice": 98743.5,
1014            "lowPrice": 95802.9,
1015            "lastPrice": 97893.7,
1016            "lastPriceProtected": 97912.5054,
1017            "lastTickDirection": "PlusTick",
1018            "lastChangePcnt": 0.0024,
1019            "bidPrice": 97882.5,
1020            "midPrice": 97884.8,
1021            "askPrice": 97887.1,
1022            "impactBidPrice": 97882.7951,
1023            "impactMidPrice": 97884.7,
1024            "impactAskPrice": 97886.6277,
1025            "hasLiquidity": true,
1026            "openInterest": 411647400,
1027            "openValue": 420691293378,
1028            "fairMethod": "FundingRate",
1029            "fairBasisRate": 0.12045,
1030            "fairBasis": 5.99,
1031            "fairPrice": 97849.76,
1032            "markMethod": "FairPrice",
1033            "markPrice": 97849.76,
1034            "indicativeSettlePrice": 97843.77,
1035            "instantPnl": true,
1036            "timestamp": "2024-11-24T23:33:19.034Z",
1037            "minTick": 0.01,
1038            "fundingBaseRate": 0.0003,
1039            "fundingQuoteRate": 0.0006,
1040            "capped": false
1041        }"#;
1042
1043        let ws_msg: BitmexInstrumentMsg =
1044            serde_json::from_str(json_data).expect("Failed to deserialize instrument message");
1045
1046        let result = crate::http::models::BitmexInstrument::try_from(ws_msg);
1047        assert!(
1048            result.is_ok(),
1049            "TryFrom should succeed with full instrument data"
1050        );
1051
1052        let instrument = result.unwrap();
1053        assert_eq!(instrument.symbol.as_str(), "XBTUSD");
1054        assert_eq!(instrument.root_symbol.as_str(), "XBT");
1055        assert_eq!(instrument.quote_currency.as_str(), "USD");
1056        assert_eq!(instrument.tick_size, 0.1);
1057    }
1058
1059    #[rstest]
1060    fn test_try_from_instrument_msg_with_partial_data_fails() {
1061        let json_data = r#"{
1062            "symbol": "XBTUSD",
1063            "lastPrice": 95123.5,
1064            "lastTickDirection": "ZeroPlusTick",
1065            "markPrice": 95125.7,
1066            "indexPrice": 95124.3,
1067            "indicativeSettlePrice": 95126.0,
1068            "openInterest": 123456789,
1069            "openValue": 1234567890,
1070            "fairBasis": 1.4,
1071            "fairBasisRate": 0.00001,
1072            "fairPrice": 95125.0,
1073            "markMethod": "FairPrice",
1074            "indicativeTaxRate": 0.00075,
1075            "timestamp": "2024-11-25T12:00:00.000Z"
1076        }"#;
1077
1078        let ws_msg: BitmexInstrumentMsg =
1079            serde_json::from_str(json_data).expect("Failed to deserialize instrument message");
1080
1081        let result = crate::http::models::BitmexInstrument::try_from(ws_msg);
1082        assert!(
1083            result.is_err(),
1084            "TryFrom should fail with partial instrument data (update action)"
1085        );
1086
1087        let err = result.unwrap_err();
1088        assert!(
1089            err.to_string().contains("Missing"),
1090            "Error should indicate missing required fields"
1091        );
1092    }
1093}