nautilus_bitmex/websocket/
messages.rs

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