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 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
41fn 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#[derive(Debug, Clone, Serialize, Deserialize)]
83pub struct BitmexAuthentication {
84 pub op: BitmexWsAuthAction,
85 pub args: (String, i64, String),
86}
87
88#[derive(Debug, Clone, Serialize, Deserialize)]
90pub struct BitmexSubscription {
91 pub op: BitmexWsOperation,
92 pub args: Vec<Ustr>,
93}
94
95#[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#[derive(Debug, Display, Deserialize)]
112#[serde(untagged)]
113pub enum BitmexWsMessage {
114 Table(BitmexTableMessage),
116 Welcome {
118 info: String,
120 version: String,
122 timestamp: DateTime<Utc>,
124 docs: String,
126 #[serde(rename = "heartbeatEnabled")]
128 heartbeat_enabled: bool,
129 limit: BitmexRateLimit,
131 #[serde(rename = "appName")]
133 app_name: Option<String>,
134 },
135 Subscription {
137 success: bool,
139 subscribe: Option<String>,
141 request: Option<BitmexHttpRequest>,
143 error: Option<String>,
145 },
146 Error {
148 status: u16,
149 error: String,
150 meta: HashMap<String, String>,
151 request: BitmexHttpRequest,
152 },
153 #[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#[derive(Debug, Deserialize)]
166pub struct BitmexRateLimit {
167 pub remaining: Option<i32>,
169}
170
171#[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#[derive(Clone, Debug, Deserialize)]
253#[serde(rename_all = "camelCase")]
254pub struct BitmexOrderBookMsg {
255 pub symbol: Ustr,
257 pub id: u64,
259 pub side: BitmexSide,
261 pub size: Option<u64>,
263 pub price: f64,
265 pub timestamp: DateTime<Utc>,
267 pub transact_time: DateTime<Utc>,
269}
270
271#[derive(Clone, Debug, Deserialize)]
273#[serde(rename_all = "camelCase")]
274pub struct BitmexOrderBook10Msg {
275 pub symbol: Ustr,
277 pub bids: Vec<[f64; 2]>,
279 pub asks: Vec<[f64; 2]>,
281 pub timestamp: DateTime<Utc>,
283}
284
285#[derive(Clone, Debug, Deserialize)]
287#[serde(rename_all = "camelCase")]
288pub struct BitmexQuoteMsg {
289 pub symbol: Ustr,
291 pub bid_price: Option<f64>,
293 pub bid_size: Option<u64>,
295 pub ask_price: Option<f64>,
297 pub ask_size: Option<u64>,
299 pub timestamp: DateTime<Utc>,
301}
302
303#[derive(Clone, Debug, Deserialize)]
305#[serde(rename_all = "camelCase")]
306pub struct BitmexTradeMsg {
307 pub timestamp: DateTime<Utc>,
309 pub symbol: Ustr,
311 pub side: BitmexSide,
313 pub size: u64,
315 pub price: f64,
317 pub tick_direction: BitmexTickDirection,
319 #[serde(rename = "trdMatchID")]
321 pub trd_match_id: Option<Uuid>,
322 pub gross_value: Option<i64>,
324 pub home_notional: Option<f64>,
326 pub foreign_notional: Option<f64>,
328 #[serde(rename = "trdType")]
330 pub trade_type: Ustr, }
332
333#[derive(Clone, Debug, Deserialize)]
334#[serde(rename_all = "camelCase")]
335pub struct BitmexTradeBinMsg {
336 pub timestamp: DateTime<Utc>,
338 pub symbol: Ustr,
340 pub open: f64,
342 pub high: f64,
344 pub low: f64,
346 pub close: f64,
348 pub trades: i64,
350 pub volume: i64,
352 pub vwap: f64,
354 pub last_size: i64,
356 pub turnover: i64,
358 pub home_notional: f64,
360 pub foreign_notional: f64,
362}
363
364#[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 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 let state = msg
455 .state
456 .and_then(|s| serde_json::from_str::<BitmexInstrumentState>(&format!("\"{s}\"")).ok())
457 .unwrap_or(BitmexInstrumentState::Open);
458
459 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, 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#[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#[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#[derive(Clone, Debug)]
628pub enum OrderData {
629 Full(BitmexOrderMsg),
630 Update(BitmexOrderUpdateMsg),
631}
632
633fn 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 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#[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#[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#[derive(Clone, Debug, Deserialize)]
838#[serde(rename_all = "camelCase")]
839pub struct BitmexMarginMsg {
840 pub account: i64,
842 pub currency: Ustr,
844 pub risk_limit: Option<i64>,
846 pub amount: Option<i64>,
848 pub prev_realised_pnl: Option<i64>,
850 pub gross_comm: Option<i64>,
852 pub gross_open_cost: Option<i64>,
854 pub gross_open_premium: Option<i64>,
856 pub gross_exec_cost: Option<i64>,
858 pub gross_mark_value: Option<i64>,
860 pub risk_value: Option<i64>,
862 pub init_margin: Option<i64>,
864 pub maint_margin: Option<i64>,
866 pub target_excess_margin: Option<i64>,
868 pub realised_pnl: Option<i64>,
870 pub unrealised_pnl: Option<i64>,
872 pub wallet_balance: Option<i64>,
874 pub margin_balance: Option<i64>,
876 pub margin_leverage: Option<f64>,
878 pub margin_used_pcnt: Option<f64>,
880 pub excess_margin: Option<i64>,
882 pub available_margin: Option<i64>,
884 pub withdrawable_margin: Option<i64>,
886 pub maker_fee_discount: Option<f64>,
888 pub taker_fee_discount: Option<f64>,
890 pub timestamp: DateTime<Utc>,
892 pub foreign_margin_balance: Option<i64>,
894 pub foreign_requirement: Option<i64>,
896}
897
898#[derive(Clone, Debug, Deserialize)]
900#[serde(rename_all = "camelCase")]
901pub struct BitmexFundingMsg {
902 pub timestamp: DateTime<Utc>,
904 pub symbol: Ustr,
906 pub funding_rate: f64,
908 pub funding_rate_daily: f64,
910}
911
912#[derive(Clone, Debug, Deserialize)]
914#[serde(rename_all = "camelCase")]
915pub struct BitmexInsuranceMsg {
916 pub currency: Ustr,
918 pub timestamp: DateTime<Utc>,
920 pub wallet_balance: i64,
922}
923
924#[derive(Clone, Debug, Deserialize)]
926#[serde(rename_all = "camelCase")]
927pub struct BitmexLiquidationMsg {
928 pub order_id: Ustr,
930 pub symbol: Ustr,
932 pub side: BitmexSide,
934 pub price: f64,
936 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}