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