1use 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
42fn 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#[derive(Debug, Clone, Serialize, Deserialize)]
84pub struct BitmexAuthentication {
85 pub op: BitmexWsAuthAction,
86 pub args: (String, i64, String),
87}
88
89#[derive(Debug, Clone, Serialize, Deserialize)]
91pub struct BitmexSubscription {
92 pub op: BitmexWsOperation,
93 pub args: Vec<Ustr>,
94}
95
96#[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#[derive(Debug, Display, Deserialize)]
114#[serde(untagged)]
115pub enum BitmexWsMessage {
116 Table(BitmexTableMessage),
118 Welcome {
120 info: String,
122 version: String,
124 timestamp: DateTime<Utc>,
126 docs: String,
128 #[serde(rename = "heartbeatEnabled")]
130 heartbeat_enabled: bool,
131 limit: BitmexRateLimit,
133 #[serde(rename = "appName")]
135 app_name: Option<String>,
136 },
137 Subscription {
139 success: bool,
141 subscribe: Option<String>,
143 request: Option<BitmexHttpRequest>,
145 error: Option<String>,
147 },
148 Error {
150 status: u16,
151 error: String,
152 meta: HashMap<String, String>,
153 request: BitmexHttpRequest,
154 },
155 #[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#[derive(Debug, Deserialize)]
168pub struct BitmexRateLimit {
169 pub remaining: Option<i32>,
171}
172
173#[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#[derive(Clone, Debug, Deserialize)]
255#[serde(rename_all = "camelCase")]
256pub struct BitmexOrderBookMsg {
257 pub symbol: Ustr,
259 pub id: u64,
261 pub side: BitmexSide,
263 pub size: Option<u64>,
265 pub price: f64,
267 pub timestamp: DateTime<Utc>,
269 pub transact_time: DateTime<Utc>,
271 pub pool: Option<Ustr>,
272}
273
274#[derive(Clone, Debug, Deserialize)]
276#[serde(rename_all = "camelCase")]
277pub struct BitmexOrderBook10Msg {
278 pub symbol: Ustr,
280 pub bids: Vec<[f64; 2]>,
282 pub asks: Vec<[f64; 2]>,
284 pub timestamp: DateTime<Utc>,
286 pub pool: Option<Ustr>,
287}
288
289#[derive(Clone, Debug, Deserialize)]
291#[serde(rename_all = "camelCase")]
292pub struct BitmexQuoteMsg {
293 pub symbol: Ustr,
295 pub bid_price: Option<f64>,
297 pub bid_size: Option<u64>,
299 pub ask_price: Option<f64>,
301 pub ask_size: Option<u64>,
303 pub timestamp: DateTime<Utc>,
305 pub pool: Option<Ustr>,
306}
307
308#[derive(Clone, Debug, Deserialize)]
310#[serde(rename_all = "camelCase")]
311pub struct BitmexTradeMsg {
312 pub timestamp: DateTime<Utc>,
314 pub symbol: Ustr,
316 pub side: BitmexSide,
318 pub size: u64,
320 pub price: f64,
322 pub tick_direction: BitmexTickDirection,
324 #[serde(rename = "trdMatchID")]
326 pub trd_match_id: Option<Uuid>,
327 pub gross_value: Option<i64>,
329 pub home_notional: Option<f64>,
331 pub foreign_notional: Option<f64>,
333 #[serde(rename = "trdType")]
335 pub trade_type: Ustr, pub pool: Option<Ustr>,
337}
338
339#[derive(Clone, Debug, Deserialize)]
340#[serde(rename_all = "camelCase")]
341pub struct BitmexTradeBinMsg {
342 pub timestamp: DateTime<Utc>,
344 pub symbol: Ustr,
346 pub open: f64,
348 pub high: f64,
350 pub low: f64,
352 pub close: f64,
354 pub trades: i64,
356 pub volume: i64,
358 pub vwap: Option<f64>,
360 pub last_size: Option<i64>,
362 pub turnover: i64,
364 pub home_notional: f64,
366 pub foreign_notional: f64,
368 pub pool: Option<Ustr>,
369}
370
371#[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 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 let state = msg
465 .state
466 .and_then(|s| serde_json::from_str::<BitmexInstrumentState>(&format!("\"{s}\"")).ok())
467 .unwrap_or(BitmexInstrumentState::Open);
468
469 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, 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#[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#[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#[derive(Clone, Debug)]
641pub enum OrderData {
642 Full(BitmexOrderMsg),
643 Update(BitmexOrderUpdateMsg),
644}
645
646fn 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 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#[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#[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#[derive(Clone, Debug, Deserialize)]
855#[serde(rename_all = "camelCase")]
856pub struct BitmexMarginMsg {
857 pub account: i64,
859 pub currency: Ustr,
861 pub risk_limit: Option<i64>,
863 pub amount: Option<i64>,
865 pub prev_realised_pnl: Option<i64>,
867 pub gross_comm: Option<i64>,
869 pub gross_open_cost: Option<i64>,
871 pub gross_open_premium: Option<i64>,
873 pub gross_exec_cost: Option<i64>,
875 pub gross_mark_value: Option<i64>,
877 pub risk_value: Option<i64>,
879 pub init_margin: Option<i64>,
881 pub maint_margin: Option<i64>,
883 pub target_excess_margin: Option<i64>,
885 pub realised_pnl: Option<i64>,
887 pub unrealised_pnl: Option<i64>,
889 pub wallet_balance: Option<i64>,
891 pub margin_balance: Option<i64>,
893 pub margin_leverage: Option<f64>,
895 pub margin_used_pcnt: Option<f64>,
897 pub excess_margin: Option<i64>,
899 pub available_margin: Option<i64>,
901 pub withdrawable_margin: Option<i64>,
903 pub maker_fee_discount: Option<f64>,
905 pub taker_fee_discount: Option<f64>,
907 pub timestamp: DateTime<Utc>,
909 pub foreign_margin_balance: Option<i64>,
911 pub foreign_requirement: Option<i64>,
913}
914
915#[derive(Clone, Debug, Deserialize)]
917#[serde(rename_all = "camelCase")]
918pub struct BitmexFundingMsg {
919 pub timestamp: DateTime<Utc>,
921 pub symbol: Ustr,
923 #[serde(with = "rust_decimal::serde::float")]
925 pub funding_rate: Decimal,
926 #[serde(with = "rust_decimal::serde::float")]
928 pub funding_rate_daily: Decimal,
929}
930
931#[derive(Clone, Debug, Deserialize)]
933#[serde(rename_all = "camelCase")]
934pub struct BitmexInsuranceMsg {
935 pub currency: Ustr,
937 pub timestamp: DateTime<Utc>,
939 pub wallet_balance: i64,
941}
942
943#[derive(Clone, Debug, Deserialize)]
945#[serde(rename_all = "camelCase")]
946pub struct BitmexLiquidationMsg {
947 pub order_id: Ustr,
949 pub symbol: Ustr,
951 pub side: BitmexSide,
953 pub price: f64,
955 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}