1use nautilus_model::enums::{
19 ContingencyType, LiquiditySide, OrderSide, OrderStatus, OrderType, PositionSide, TimeInForce,
20};
21use serde::{Deserialize, Deserializer, Serialize};
22use strum::{AsRefStr, Display, EnumIter, EnumString};
23
24use crate::error::{BitmexError, BitmexNonRetryableError};
25
26#[derive(
28 Copy,
29 Clone,
30 Debug,
31 Display,
32 PartialEq,
33 Eq,
34 AsRefStr,
35 EnumIter,
36 EnumString,
37 Serialize,
38 Deserialize,
39)]
40#[serde(rename_all = "PascalCase")]
41#[cfg_attr(
42 feature = "python",
43 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.bitmex", eq, eq_int)
44)]
45pub enum BitmexSymbolStatus {
46 Open,
48 Closed,
50 Unlisted,
52}
53
54#[derive(
56 Copy,
57 Clone,
58 Debug,
59 Display,
60 PartialEq,
61 Eq,
62 AsRefStr,
63 EnumIter,
64 EnumString,
65 Serialize,
66 Deserialize,
67)]
68pub enum BitmexSide {
69 #[serde(rename = "Buy", alias = "BUY", alias = "buy")]
71 Buy,
72 #[serde(rename = "Sell", alias = "SELL", alias = "sell")]
74 Sell,
75}
76
77impl TryFrom<OrderSide> for BitmexSide {
78 type Error = BitmexError;
79
80 fn try_from(value: OrderSide) -> Result<Self, Self::Error> {
81 match value {
82 OrderSide::Buy => Ok(Self::Buy),
83 OrderSide::Sell => Ok(Self::Sell),
84 _ => Err(BitmexError::NonRetryable {
85 source: BitmexNonRetryableError::Validation {
86 field: "order_side".to_string(),
87 message: format!("Invalid order side: {value:?}"),
88 },
89 }),
90 }
91 }
92}
93
94impl BitmexSide {
95 pub fn try_from_order_side(value: OrderSide) -> anyhow::Result<Self> {
101 Self::try_from(value).map_err(|e| anyhow::anyhow!("{e}"))
102 }
103}
104
105impl From<BitmexSide> for OrderSide {
106 fn from(side: BitmexSide) -> Self {
107 match side {
108 BitmexSide::Buy => Self::Buy,
109 BitmexSide::Sell => Self::Sell,
110 }
111 }
112}
113
114#[derive(
116 Copy,
117 Clone,
118 Debug,
119 Display,
120 PartialEq,
121 Eq,
122 AsRefStr,
123 EnumIter,
124 EnumString,
125 Serialize,
126 Deserialize,
127)]
128#[cfg_attr(
129 feature = "python",
130 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.bitmex", eq, eq_int)
131)]
132pub enum BitmexPositionSide {
133 #[serde(rename = "LONG", alias = "Long", alias = "long")]
135 Long,
136 #[serde(rename = "SHORT", alias = "Short", alias = "short")]
138 Short,
139 #[serde(rename = "FLAT", alias = "Flat", alias = "flat")]
141 Flat,
142}
143
144impl From<BitmexPositionSide> for PositionSide {
145 fn from(side: BitmexPositionSide) -> Self {
146 match side {
147 BitmexPositionSide::Long => Self::Long,
148 BitmexPositionSide::Short => Self::Short,
149 BitmexPositionSide::Flat => Self::Flat,
150 }
151 }
152}
153
154impl From<PositionSide> for BitmexPositionSide {
155 fn from(side: PositionSide) -> Self {
156 match side {
157 PositionSide::Long => Self::Long,
158 PositionSide::Short => Self::Short,
159 PositionSide::Flat | PositionSide::NoPositionSide => Self::Flat,
160 }
161 }
162}
163
164#[derive(
166 Copy,
167 Clone,
168 Debug,
169 Display,
170 PartialEq,
171 Eq,
172 AsRefStr,
173 EnumIter,
174 EnumString,
175 Serialize,
176 Deserialize,
177)]
178pub enum BitmexOrderType {
179 Market,
181 Limit,
183 Stop,
185 StopLimit,
187 MarketIfTouched,
189 LimitIfTouched,
191 Pegged,
193}
194
195impl TryFrom<OrderType> for BitmexOrderType {
196 type Error = BitmexError;
197
198 fn try_from(value: OrderType) -> Result<Self, Self::Error> {
199 match value {
200 OrderType::Market => Ok(Self::Market),
201 OrderType::Limit => Ok(Self::Limit),
202 OrderType::StopMarket => Ok(Self::Stop),
203 OrderType::StopLimit => Ok(Self::StopLimit),
204 OrderType::MarketIfTouched => Ok(Self::MarketIfTouched),
205 OrderType::LimitIfTouched => Ok(Self::LimitIfTouched),
206 OrderType::TrailingStopMarket => Ok(Self::Pegged),
207 OrderType::TrailingStopLimit => Ok(Self::Pegged),
208 OrderType::MarketToLimit => Err(BitmexError::NonRetryable {
209 source: BitmexNonRetryableError::Validation {
210 field: "order_type".to_string(),
211 message: "MarketToLimit order type is not supported by BitMEX".to_string(),
212 },
213 }),
214 }
215 }
216}
217
218impl BitmexOrderType {
219 pub fn try_from_order_type(value: OrderType) -> anyhow::Result<Self> {
225 Self::try_from(value).map_err(|e| anyhow::anyhow!("{e}"))
226 }
227}
228
229impl From<BitmexOrderType> for OrderType {
230 fn from(value: BitmexOrderType) -> Self {
231 match value {
232 BitmexOrderType::Market => Self::Market,
233 BitmexOrderType::Limit => Self::Limit,
234 BitmexOrderType::Stop => Self::StopMarket,
235 BitmexOrderType::StopLimit => Self::StopLimit,
236 BitmexOrderType::MarketIfTouched => Self::MarketIfTouched,
237 BitmexOrderType::LimitIfTouched => Self::LimitIfTouched,
238 BitmexOrderType::Pegged => Self::Limit,
239 }
240 }
241}
242
243#[derive(
245 Copy,
246 Clone,
247 Debug,
248 Display,
249 PartialEq,
250 Eq,
251 AsRefStr,
252 EnumIter,
253 EnumString,
254 Serialize,
255 Deserialize,
256)]
257pub enum BitmexOrderStatus {
258 New,
260 PartiallyFilled,
262 Filled,
264 PendingCancel,
266 Canceled,
268 Rejected,
270 Expired,
272}
273
274impl From<BitmexOrderStatus> for OrderStatus {
275 fn from(value: BitmexOrderStatus) -> Self {
276 match value {
277 BitmexOrderStatus::New => Self::Accepted,
278 BitmexOrderStatus::PartiallyFilled => Self::PartiallyFilled,
279 BitmexOrderStatus::Filled => Self::Filled,
280 BitmexOrderStatus::PendingCancel => Self::PendingCancel,
281 BitmexOrderStatus::Canceled => Self::Canceled,
282 BitmexOrderStatus::Rejected => Self::Rejected,
283 BitmexOrderStatus::Expired => Self::Expired,
284 }
285 }
286}
287
288#[derive(
290 Copy,
291 Clone,
292 Debug,
293 Display,
294 PartialEq,
295 Eq,
296 AsRefStr,
297 EnumIter,
298 EnumString,
299 Serialize,
300 Deserialize,
301)]
302pub enum BitmexTimeInForce {
303 Day,
304 GoodTillCancel,
305 AtTheOpening,
306 ImmediateOrCancel,
307 FillOrKill,
308 GoodTillCrossing,
309 GoodTillDate,
310 AtTheClose,
311 GoodThroughCrossing,
312 AtCrossing,
313}
314
315impl TryFrom<BitmexTimeInForce> for TimeInForce {
316 type Error = BitmexError;
317
318 fn try_from(value: BitmexTimeInForce) -> Result<Self, Self::Error> {
319 match value {
320 BitmexTimeInForce::Day => Ok(Self::Day),
321 BitmexTimeInForce::GoodTillCancel => Ok(Self::Gtc),
322 BitmexTimeInForce::GoodTillDate => Ok(Self::Gtd),
323 BitmexTimeInForce::ImmediateOrCancel => Ok(Self::Ioc),
324 BitmexTimeInForce::FillOrKill => Ok(Self::Fok),
325 BitmexTimeInForce::AtTheOpening => Ok(Self::AtTheOpen),
326 BitmexTimeInForce::AtTheClose => Ok(Self::AtTheClose),
327 _ => Err(BitmexError::NonRetryable {
328 source: BitmexNonRetryableError::Validation {
329 field: "time_in_force".to_string(),
330 message: format!("Unsupported BitmexTimeInForce: {value}"),
331 },
332 }),
333 }
334 }
335}
336
337impl TryFrom<TimeInForce> for BitmexTimeInForce {
338 type Error = crate::error::BitmexError;
339
340 fn try_from(value: TimeInForce) -> Result<Self, Self::Error> {
341 match value {
342 TimeInForce::Day => Ok(Self::Day),
343 TimeInForce::Gtc => Ok(Self::GoodTillCancel),
344 TimeInForce::Gtd => Ok(Self::GoodTillDate),
345 TimeInForce::Ioc => Ok(Self::ImmediateOrCancel),
346 TimeInForce::Fok => Ok(Self::FillOrKill),
347 TimeInForce::AtTheOpen => Ok(Self::AtTheOpening),
348 TimeInForce::AtTheClose => Ok(Self::AtTheClose),
349 }
350 }
351}
352
353impl BitmexTimeInForce {
354 pub fn try_from_time_in_force(value: TimeInForce) -> anyhow::Result<Self> {
360 Self::try_from(value).map_err(|e| anyhow::anyhow!("{e}"))
361 }
362}
363
364#[derive(
366 Copy,
367 Clone,
368 Debug,
369 Display,
370 PartialEq,
371 Eq,
372 AsRefStr,
373 EnumIter,
374 EnumString,
375 Serialize,
376 Deserialize,
377)]
378pub enum BitmexContingencyType {
379 OneCancelsTheOther,
380 OneTriggersTheOther,
381 OneUpdatesTheOtherAbsolute,
382 OneUpdatesTheOtherProportional,
383 #[serde(rename = "")]
384 Unknown, }
386
387impl From<BitmexContingencyType> for ContingencyType {
388 fn from(value: BitmexContingencyType) -> Self {
389 match value {
390 BitmexContingencyType::OneCancelsTheOther => Self::Oco,
391 BitmexContingencyType::OneTriggersTheOther => Self::Oto,
392 BitmexContingencyType::OneUpdatesTheOtherProportional => Self::Ouo,
393 BitmexContingencyType::OneUpdatesTheOtherAbsolute => Self::Ouo,
394 BitmexContingencyType::Unknown => Self::NoContingency,
395 }
396 }
397}
398
399impl TryFrom<ContingencyType> for BitmexContingencyType {
400 type Error = BitmexError;
401
402 fn try_from(value: ContingencyType) -> Result<Self, Self::Error> {
403 match value {
404 ContingencyType::NoContingency => Ok(Self::Unknown),
405 ContingencyType::Oco => Ok(Self::OneCancelsTheOther),
406 ContingencyType::Oto => Ok(Self::OneTriggersTheOther),
407 ContingencyType::Ouo => Err(BitmexError::NonRetryable {
408 source: BitmexNonRetryableError::Validation {
409 field: "contingency_type".to_string(),
410 message: "OUO contingency type not supported by BitMEX".to_string(),
411 },
412 }),
413 }
414 }
415}
416
417#[derive(
419 Copy,
420 Clone,
421 Debug,
422 Display,
423 PartialEq,
424 Eq,
425 AsRefStr,
426 EnumIter,
427 EnumString,
428 Serialize,
429 Deserialize,
430)]
431pub enum BitmexPegPriceType {
432 LastPeg,
433 OpeningPeg,
434 MidPricePeg,
435 MarketPeg,
436 PrimaryPeg,
437 PegToVWAP,
438 TrailingStopPeg,
439 PegToLimitPrice,
440 ShortSaleMinPricePeg,
441 #[serde(rename = "")]
442 Unknown, }
444
445#[derive(
447 Copy,
448 Clone,
449 Debug,
450 Display,
451 PartialEq,
452 Eq,
453 AsRefStr,
454 EnumIter,
455 EnumString,
456 Serialize,
457 Deserialize,
458)]
459pub enum BitmexExecInstruction {
460 ParticipateDoNotInitiate,
461 AllOrNone,
462 MarkPrice,
463 IndexPrice,
464 LastPrice,
465 Close,
466 ReduceOnly,
467 Fixed,
468 #[serde(rename = "")]
469 Unknown, }
471
472impl BitmexExecInstruction {
473 pub fn join(instructions: &[Self]) -> String {
475 instructions
476 .iter()
477 .map(std::string::ToString::to_string)
478 .collect::<Vec<_>>()
479 .join(",")
480 }
481}
482
483#[derive(
485 Copy,
486 Clone,
487 Debug,
488 Display,
489 PartialEq,
490 Eq,
491 AsRefStr,
492 EnumIter,
493 EnumString,
494 Serialize,
495 Deserialize,
496)]
497pub enum BitmexExecType {
498 New,
500 Trade,
502 Canceled,
504 CancelReject,
506 Replaced,
508 Rejected,
510 AmendReject,
512 Funding,
514 Settlement,
516 Suspended,
518 Released,
520 Insurance,
522 Rebalance,
524 Liquidation,
526 Bankruptcy,
528 TrialFill,
530 TriggeredOrActivatedBySystem,
532}
533
534#[derive(
536 Copy,
537 Clone,
538 Debug,
539 Display,
540 PartialEq,
541 Eq,
542 AsRefStr,
543 EnumIter,
544 EnumString,
545 Serialize,
546 Deserialize,
547)]
548pub enum BitmexLiquidityIndicator {
549 #[serde(rename = "Added")]
552 #[serde(alias = "AddedLiquidity")]
553 Maker,
554 #[serde(rename = "Removed")]
557 #[serde(alias = "RemovedLiquidity")]
558 Taker,
559}
560
561impl From<BitmexLiquidityIndicator> for LiquiditySide {
562 fn from(value: BitmexLiquidityIndicator) -> Self {
563 match value {
564 BitmexLiquidityIndicator::Maker => Self::Maker,
565 BitmexLiquidityIndicator::Taker => Self::Taker,
566 }
567 }
568}
569
570#[derive(
572 Copy,
573 Clone,
574 Debug,
575 Display,
576 PartialEq,
577 Eq,
578 AsRefStr,
579 EnumIter,
580 EnumString,
581 Serialize,
582 Deserialize,
583)]
584#[serde(rename_all = "UPPERCASE")]
585pub enum BitmexInstrumentType {
586 #[serde(rename = "FXXXS")]
587 Unknown1, #[serde(rename = "FMXXS")]
590 Unknown2, #[serde(rename = "FFICSX")]
595 PredictionMarket,
596
597 #[serde(rename = "FFWCSX")]
599 PerpetualContract,
600
601 #[serde(rename = "FFWCSF")]
603 PerpetualContractFx,
604
605 #[serde(rename = "IFXXXP")]
607 Spot,
608
609 #[serde(rename = "FFCCSX")]
611 Futures,
612
613 #[serde(rename = "MRBXXX")]
615 BasketIndex,
616
617 #[serde(rename = "MRCXXX")]
619 CryptoIndex,
620
621 #[serde(rename = "MRFXXX")]
623 FxIndex,
624
625 #[serde(rename = "MRRXXX")]
627 LendingIndex,
628
629 #[serde(rename = "MRIXXX")]
631 VolatilityIndex,
632}
633
634#[derive(Clone, Debug, Display, PartialEq, Eq, AsRefStr, EnumIter, EnumString, Serialize)]
636pub enum BitmexProductType {
637 #[serde(rename = "instrument")]
639 All,
640
641 #[serde(rename = "CONTRACTS")]
643 Contracts,
644
645 #[serde(rename = "INDICES")]
647 Indices,
648
649 #[serde(rename = "DERIVATIVES")]
651 Derivatives,
652
653 #[serde(rename = "SPOT")]
655 Spot,
656
657 #[serde(rename = "instrument")]
659 #[serde(untagged)]
660 Specific(String),
661}
662
663impl BitmexProductType {
664 #[must_use]
666 pub fn to_subscription(&self) -> String {
667 match self {
668 Self::All => "instrument".to_string(),
669 Self::Specific(symbol) => format!("instrument:{symbol}"),
670 Self::Contracts => "CONTRACTS".to_string(),
671 Self::Indices => "INDICES".to_string(),
672 Self::Derivatives => "DERIVATIVES".to_string(),
673 Self::Spot => "SPOT".to_string(),
674 }
675 }
676}
677
678impl<'de> Deserialize<'de> for BitmexProductType {
679 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
680 where
681 D: Deserializer<'de>,
682 {
683 let s = String::deserialize(deserializer)?;
684
685 match s.as_str() {
686 "instrument" => Ok(Self::All),
687 "CONTRACTS" => Ok(Self::Contracts),
688 "INDICES" => Ok(Self::Indices),
689 "DERIVATIVES" => Ok(Self::Derivatives),
690 "SPOT" => Ok(Self::Spot),
691 s if s.starts_with("instrument:") => {
692 let symbol = s.strip_prefix("instrument:").unwrap();
693 Ok(Self::Specific(symbol.to_string()))
694 }
695 _ => Err(serde::de::Error::custom(format!(
696 "Invalid product type: {s}"
697 ))),
698 }
699 }
700}
701
702#[derive(
704 Copy,
705 Clone,
706 Debug,
707 Display,
708 PartialEq,
709 Eq,
710 AsRefStr,
711 EnumIter,
712 EnumString,
713 Serialize,
714 Deserialize,
715)]
716pub enum BitmexTickDirection {
717 PlusTick,
719 MinusTick,
721 ZeroPlusTick,
723 ZeroMinusTick,
725}
726
727#[derive(
729 Clone, Debug, Display, PartialEq, Eq, AsRefStr, EnumIter, EnumString, Serialize, Deserialize,
730)]
731pub enum BitmexInstrumentState {
732 Open,
734 Closed,
736 Unlisted,
738 Settled,
740}
741
742#[derive(
744 Clone, Debug, Display, PartialEq, Eq, AsRefStr, EnumIter, EnumString, Serialize, Deserialize,
745)]
746pub enum BitmexFairMethod {
747 FundingRate,
749 ImpactMidPrice,
751 LastPrice,
753}
754
755#[derive(
757 Clone, Debug, Display, PartialEq, Eq, AsRefStr, EnumIter, EnumString, Serialize, Deserialize,
758)]
759pub enum BitmexMarkMethod {
760 FairPrice,
762 LastPrice,
764 CompositeIndex,
766}
767
768#[cfg(test)]
773mod tests {
774 use rstest::rstest;
775
776 use super::*;
777
778 #[rstest]
779 fn test_bitmex_side_deserialization() {
780 assert_eq!(
782 serde_json::from_str::<BitmexSide>(r#""Buy""#).unwrap(),
783 BitmexSide::Buy
784 );
785 assert_eq!(
786 serde_json::from_str::<BitmexSide>(r#""BUY""#).unwrap(),
787 BitmexSide::Buy
788 );
789 assert_eq!(
790 serde_json::from_str::<BitmexSide>(r#""buy""#).unwrap(),
791 BitmexSide::Buy
792 );
793 assert_eq!(
794 serde_json::from_str::<BitmexSide>(r#""Sell""#).unwrap(),
795 BitmexSide::Sell
796 );
797 assert_eq!(
798 serde_json::from_str::<BitmexSide>(r#""SELL""#).unwrap(),
799 BitmexSide::Sell
800 );
801 assert_eq!(
802 serde_json::from_str::<BitmexSide>(r#""sell""#).unwrap(),
803 BitmexSide::Sell
804 );
805 }
806
807 #[rstest]
808 fn test_bitmex_order_type_deserialization() {
809 assert_eq!(
810 serde_json::from_str::<BitmexOrderType>(r#""Market""#).unwrap(),
811 BitmexOrderType::Market
812 );
813 assert_eq!(
814 serde_json::from_str::<BitmexOrderType>(r#""Limit""#).unwrap(),
815 BitmexOrderType::Limit
816 );
817 assert_eq!(
818 serde_json::from_str::<BitmexOrderType>(r#""Stop""#).unwrap(),
819 BitmexOrderType::Stop
820 );
821 assert_eq!(
822 serde_json::from_str::<BitmexOrderType>(r#""StopLimit""#).unwrap(),
823 BitmexOrderType::StopLimit
824 );
825 assert_eq!(
826 serde_json::from_str::<BitmexOrderType>(r#""MarketIfTouched""#).unwrap(),
827 BitmexOrderType::MarketIfTouched
828 );
829 assert_eq!(
830 serde_json::from_str::<BitmexOrderType>(r#""LimitIfTouched""#).unwrap(),
831 BitmexOrderType::LimitIfTouched
832 );
833 assert_eq!(
834 serde_json::from_str::<BitmexOrderType>(r#""Pegged""#).unwrap(),
835 BitmexOrderType::Pegged
836 );
837 }
838
839 #[rstest]
840 fn test_instrument_type_serialization() {
841 assert_eq!(
842 serde_json::to_string(&BitmexInstrumentType::PerpetualContract).unwrap(),
843 r#""FFWCSX""#
844 );
845 assert_eq!(
846 serde_json::to_string(&BitmexInstrumentType::PerpetualContractFx).unwrap(),
847 r#""FFWCSF""#
848 );
849 assert_eq!(
850 serde_json::to_string(&BitmexInstrumentType::Spot).unwrap(),
851 r#""IFXXXP""#
852 );
853 assert_eq!(
854 serde_json::to_string(&BitmexInstrumentType::Futures).unwrap(),
855 r#""FFCCSX""#
856 );
857 assert_eq!(
858 serde_json::to_string(&BitmexInstrumentType::BasketIndex).unwrap(),
859 r#""MRBXXX""#
860 );
861 assert_eq!(
862 serde_json::to_string(&BitmexInstrumentType::CryptoIndex).unwrap(),
863 r#""MRCXXX""#
864 );
865 assert_eq!(
866 serde_json::to_string(&BitmexInstrumentType::FxIndex).unwrap(),
867 r#""MRFXXX""#
868 );
869 assert_eq!(
870 serde_json::to_string(&BitmexInstrumentType::LendingIndex).unwrap(),
871 r#""MRRXXX""#
872 );
873 assert_eq!(
874 serde_json::to_string(&BitmexInstrumentType::VolatilityIndex).unwrap(),
875 r#""MRIXXX""#
876 );
877 assert_eq!(
878 serde_json::to_string(&BitmexInstrumentType::PredictionMarket).unwrap(),
879 r#""FFICSX""#
880 );
881 }
882
883 #[rstest]
884 fn test_instrument_type_deserialization() {
885 assert_eq!(
886 serde_json::from_str::<BitmexInstrumentType>(r#""FFWCSX""#).unwrap(),
887 BitmexInstrumentType::PerpetualContract
888 );
889 assert_eq!(
890 serde_json::from_str::<BitmexInstrumentType>(r#""FFWCSF""#).unwrap(),
891 BitmexInstrumentType::PerpetualContractFx
892 );
893 assert_eq!(
894 serde_json::from_str::<BitmexInstrumentType>(r#""IFXXXP""#).unwrap(),
895 BitmexInstrumentType::Spot
896 );
897 assert_eq!(
898 serde_json::from_str::<BitmexInstrumentType>(r#""FFCCSX""#).unwrap(),
899 BitmexInstrumentType::Futures
900 );
901 assert_eq!(
902 serde_json::from_str::<BitmexInstrumentType>(r#""MRBXXX""#).unwrap(),
903 BitmexInstrumentType::BasketIndex
904 );
905 assert_eq!(
906 serde_json::from_str::<BitmexInstrumentType>(r#""MRCXXX""#).unwrap(),
907 BitmexInstrumentType::CryptoIndex
908 );
909 assert_eq!(
910 serde_json::from_str::<BitmexInstrumentType>(r#""MRFXXX""#).unwrap(),
911 BitmexInstrumentType::FxIndex
912 );
913 assert_eq!(
914 serde_json::from_str::<BitmexInstrumentType>(r#""MRRXXX""#).unwrap(),
915 BitmexInstrumentType::LendingIndex
916 );
917 assert_eq!(
918 serde_json::from_str::<BitmexInstrumentType>(r#""MRIXXX""#).unwrap(),
919 BitmexInstrumentType::VolatilityIndex
920 );
921 assert_eq!(
922 serde_json::from_str::<BitmexInstrumentType>(r#""FFICSX""#).unwrap(),
923 BitmexInstrumentType::PredictionMarket
924 );
925
926 assert!(serde_json::from_str::<BitmexInstrumentType>(r#""INVALID""#).is_err());
928 }
929
930 #[rstest]
931 fn test_subscription_strings() {
932 assert_eq!(BitmexProductType::All.to_subscription(), "instrument");
933 assert_eq!(
934 BitmexProductType::Specific("XBTUSD".to_string()).to_subscription(),
935 "instrument:XBTUSD"
936 );
937 assert_eq!(BitmexProductType::Contracts.to_subscription(), "CONTRACTS");
938 assert_eq!(BitmexProductType::Indices.to_subscription(), "INDICES");
939 assert_eq!(
940 BitmexProductType::Derivatives.to_subscription(),
941 "DERIVATIVES"
942 );
943 assert_eq!(BitmexProductType::Spot.to_subscription(), "SPOT");
944 }
945
946 #[rstest]
947 fn test_serialization() {
948 assert_eq!(
950 serde_json::to_string(&BitmexProductType::All).unwrap(),
951 r#""instrument""#
952 );
953 assert_eq!(
954 serde_json::to_string(&BitmexProductType::Specific("XBTUSD".to_string())).unwrap(),
955 r#""XBTUSD""#
956 );
957 assert_eq!(
958 serde_json::to_string(&BitmexProductType::Contracts).unwrap(),
959 r#""CONTRACTS""#
960 );
961 }
962
963 #[rstest]
964 fn test_deserialization() {
965 assert_eq!(
966 serde_json::from_str::<BitmexProductType>(r#""instrument""#).unwrap(),
967 BitmexProductType::All
968 );
969 assert_eq!(
970 serde_json::from_str::<BitmexProductType>(r#""instrument:XBTUSD""#).unwrap(),
971 BitmexProductType::Specific("XBTUSD".to_string())
972 );
973 assert_eq!(
974 serde_json::from_str::<BitmexProductType>(r#""CONTRACTS""#).unwrap(),
975 BitmexProductType::Contracts
976 );
977 }
978
979 #[rstest]
980 fn test_error_cases() {
981 assert!(serde_json::from_str::<BitmexProductType>(r#""invalid_type""#).is_err());
982 assert!(serde_json::from_str::<BitmexProductType>(r"123").is_err());
983 assert!(serde_json::from_str::<BitmexProductType>(r"{}").is_err());
984 }
985
986 #[rstest]
987 fn test_order_side_try_from() {
988 assert_eq!(
990 BitmexSide::try_from(OrderSide::Buy).unwrap(),
991 BitmexSide::Buy
992 );
993 assert_eq!(
994 BitmexSide::try_from(OrderSide::Sell).unwrap(),
995 BitmexSide::Sell
996 );
997
998 let result = BitmexSide::try_from(OrderSide::NoOrderSide);
1000 assert!(result.is_err());
1001 match result {
1002 Err(BitmexError::NonRetryable {
1003 source: BitmexNonRetryableError::Validation { field, .. },
1004 ..
1005 }) => {
1006 assert_eq!(field, "order_side");
1007 }
1008 _ => panic!("Expected validation error"),
1009 }
1010 }
1011
1012 #[rstest]
1013 fn test_order_type_try_from() {
1014 assert_eq!(
1016 BitmexOrderType::try_from(OrderType::Market).unwrap(),
1017 BitmexOrderType::Market
1018 );
1019 assert_eq!(
1020 BitmexOrderType::try_from(OrderType::Limit).unwrap(),
1021 BitmexOrderType::Limit
1022 );
1023
1024 let result = BitmexOrderType::try_from(OrderType::MarketToLimit);
1026 assert!(result.is_err());
1027 match result {
1028 Err(BitmexError::NonRetryable {
1029 source: BitmexNonRetryableError::Validation { message, .. },
1030 ..
1031 }) => {
1032 assert!(message.contains("not supported"));
1033 }
1034 _ => panic!("Expected validation error"),
1035 }
1036 }
1037
1038 #[rstest]
1039 fn test_time_in_force_conversions() {
1040 assert_eq!(
1042 TimeInForce::try_from(BitmexTimeInForce::Day).unwrap(),
1043 TimeInForce::Day
1044 );
1045 assert_eq!(
1046 TimeInForce::try_from(BitmexTimeInForce::GoodTillCancel).unwrap(),
1047 TimeInForce::Gtc
1048 );
1049 assert_eq!(
1050 TimeInForce::try_from(BitmexTimeInForce::ImmediateOrCancel).unwrap(),
1051 TimeInForce::Ioc
1052 );
1053
1054 let result = TimeInForce::try_from(BitmexTimeInForce::GoodTillCrossing);
1056 assert!(result.is_err());
1057 match result {
1058 Err(BitmexError::NonRetryable {
1059 source: BitmexNonRetryableError::Validation { field, message },
1060 ..
1061 }) => {
1062 assert_eq!(field, "time_in_force");
1063 assert!(message.contains("Unsupported"));
1064 }
1065 _ => panic!("Expected validation error"),
1066 }
1067
1068 assert_eq!(
1070 BitmexTimeInForce::try_from(TimeInForce::Day).unwrap(),
1071 BitmexTimeInForce::Day
1072 );
1073 assert_eq!(
1074 BitmexTimeInForce::try_from(TimeInForce::Gtc).unwrap(),
1075 BitmexTimeInForce::GoodTillCancel
1076 );
1077 assert_eq!(
1078 BitmexTimeInForce::try_from(TimeInForce::Fok).unwrap(),
1079 BitmexTimeInForce::FillOrKill
1080 );
1081 }
1082
1083 #[rstest]
1084 fn test_helper_methods() {
1085 let result = BitmexSide::try_from_order_side(OrderSide::Buy);
1087 assert!(result.is_ok());
1088 assert_eq!(result.unwrap(), BitmexSide::Buy);
1089
1090 let result = BitmexSide::try_from_order_side(OrderSide::NoOrderSide);
1091 assert!(result.is_err());
1092
1093 let result = BitmexOrderType::try_from_order_type(OrderType::Limit);
1095 assert!(result.is_ok());
1096 assert_eq!(result.unwrap(), BitmexOrderType::Limit);
1097
1098 let result = BitmexOrderType::try_from_order_type(OrderType::MarketToLimit);
1099 assert!(result.is_err());
1100
1101 let result = BitmexTimeInForce::try_from_time_in_force(TimeInForce::Ioc);
1103 assert!(result.is_ok());
1104 assert_eq!(result.unwrap(), BitmexTimeInForce::ImmediateOrCancel);
1105 }
1106}