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