1use std::{borrow::Cow, fmt::Display, marker::PhantomData, str::FromStr};
19
20use serde::{Deserialize, Deserializer, Serializer};
21use strum::{AsRefStr, Display, EnumIter, EnumString, FromRepr};
22
23use crate::enum_strum_serde;
24
25pub trait FromU8 {
27 fn from_u8(value: u8) -> Option<Self>
31 where
32 Self: Sized;
33}
34
35pub trait FromU16 {
37 fn from_u16(value: u16) -> Option<Self>
41 where
42 Self: Sized;
43}
44
45#[repr(C)]
47#[derive(
48 Copy,
49 Clone,
50 Debug,
51 Display,
52 Hash,
53 PartialEq,
54 Eq,
55 PartialOrd,
56 Ord,
57 AsRefStr,
58 FromRepr,
59 EnumIter,
60 EnumString,
61)]
62#[strum(ascii_case_insensitive)]
63#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
64#[cfg_attr(
65 feature = "python",
66 pyo3::pyclass(
67 frozen,
68 eq,
69 eq_int,
70 module = "nautilus_trader.model",
71 from_py_object,
72 rename_all = "SCREAMING_SNAKE_CASE",
73 )
74)]
75#[cfg_attr(
76 feature = "python",
77 pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.model")
78)]
79pub enum AccountType {
80 Cash = 1,
82 Margin = 2,
84 Betting = 3,
86 Wallet = 4,
88}
89
90#[repr(C)]
92#[derive(
93 Copy,
94 Clone,
95 Debug,
96 Display,
97 Hash,
98 PartialEq,
99 Eq,
100 PartialOrd,
101 Ord,
102 AsRefStr,
103 FromRepr,
104 EnumIter,
105 EnumString,
106)]
107#[strum(ascii_case_insensitive)]
108#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
109#[cfg_attr(
110 feature = "python",
111 pyo3::pyclass(
112 frozen,
113 eq,
114 eq_int,
115 module = "nautilus_trader.model",
116 from_py_object,
117 rename_all = "SCREAMING_SNAKE_CASE",
118 )
119)]
120#[cfg_attr(
121 feature = "python",
122 pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.model")
123)]
124pub enum AggregationSource {
125 External = 1,
127 Internal = 2,
129}
130
131#[repr(C)]
133#[derive(
134 Copy,
135 Clone,
136 Debug,
137 Default,
138 Display,
139 Hash,
140 PartialEq,
141 Eq,
142 PartialOrd,
143 Ord,
144 AsRefStr,
145 FromRepr,
146 EnumIter,
147 EnumString,
148)]
149#[strum(ascii_case_insensitive)]
150#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
151#[cfg_attr(
152 feature = "python",
153 pyo3::pyclass(
154 frozen,
155 eq,
156 eq_int,
157 module = "nautilus_trader.model",
158 from_py_object,
159 rename_all = "SCREAMING_SNAKE_CASE",
160 )
161)]
162#[cfg_attr(
163 feature = "python",
164 pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.model")
165)]
166pub enum AggressorSide {
167 #[default]
169 NoAggressor = 0,
170 #[strum(serialize = "BUYER", to_string = "BUY")]
174 Buy = 1,
175 #[strum(serialize = "SELLER", to_string = "SELL")]
179 Sell = 2,
180}
181
182impl FromU8 for AggressorSide {
183 fn from_u8(value: u8) -> Option<Self> {
184 Self::from_repr(usize::from(value))
185 }
186}
187
188#[repr(C)]
190#[derive(
191 Copy,
192 Clone,
193 Debug,
194 Display,
195 Hash,
196 PartialEq,
197 Eq,
198 PartialOrd,
199 Ord,
200 AsRefStr,
201 FromRepr,
202 EnumIter,
203 EnumString,
204)]
205#[strum(ascii_case_insensitive)]
206#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
207#[cfg_attr(
208 feature = "python",
209 pyo3::pyclass(
210 frozen,
211 eq,
212 eq_int,
213 module = "nautilus_trader.model",
214 from_py_object,
215 rename_all = "SCREAMING_SNAKE_CASE",
216 )
217)]
218#[cfg_attr(
219 feature = "python",
220 pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.model")
221)]
222#[allow(non_camel_case_types)]
223pub enum AssetClass {
224 FX = 1,
226 Equity = 2,
228 Commodity = 3,
230 Debt = 4,
232 Index = 5,
234 Cryptocurrency = 6,
236 Alternative = 7,
238}
239
240impl FromU8 for AssetClass {
241 fn from_u8(value: u8) -> Option<Self> {
242 Self::from_repr(usize::from(value))
243 }
244}
245
246#[repr(C)]
248#[derive(
249 Copy,
250 Clone,
251 Debug,
252 Display,
253 Hash,
254 PartialEq,
255 Eq,
256 PartialOrd,
257 Ord,
258 AsRefStr,
259 FromRepr,
260 EnumIter,
261 EnumString,
262)]
263#[strum(ascii_case_insensitive)]
264#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
265#[cfg_attr(
266 feature = "python",
267 pyo3::pyclass(
268 frozen,
269 eq,
270 eq_int,
271 module = "nautilus_trader.model",
272 from_py_object,
273 rename_all = "SCREAMING_SNAKE_CASE",
274 )
275)]
276#[cfg_attr(
277 feature = "python",
278 pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.model")
279)]
280pub enum BarAggregation {
281 Tick = 1,
283 TickImbalance = 2,
285 TickRuns = 3,
287 Volume = 4,
289 VolumeImbalance = 5,
291 VolumeRuns = 6,
293 Value = 7,
295 ValueImbalance = 8,
297 ValueRuns = 9,
299 Millisecond = 10,
301 Second = 11,
303 Minute = 12,
305 Hour = 13,
307 Day = 14,
309 Week = 15,
311 Month = 16,
313 Year = 17,
315 Renko = 18,
317}
318
319#[repr(C)]
321#[derive(
322 Copy,
323 Clone,
324 Debug,
325 Default,
326 Display,
327 Hash,
328 PartialEq,
329 Eq,
330 PartialOrd,
331 Ord,
332 AsRefStr,
333 FromRepr,
334 EnumIter,
335 EnumString,
336)]
337#[strum(ascii_case_insensitive)]
338#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
339#[cfg_attr(
340 feature = "python",
341 pyo3::pyclass(
342 frozen,
343 eq,
344 eq_int,
345 module = "nautilus_trader.model",
346 from_py_object,
347 rename_all = "SCREAMING_SNAKE_CASE",
348 )
349)]
350#[cfg_attr(
351 feature = "python",
352 pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.model")
353)]
354pub enum BarIntervalType {
355 #[default]
357 LeftOpen = 1,
358 RightOpen = 2,
360}
361
362#[repr(C)]
364#[derive(
365 Copy,
366 Clone,
367 Debug,
368 Display,
369 Hash,
370 PartialEq,
371 Eq,
372 PartialOrd,
373 Ord,
374 AsRefStr,
375 FromRepr,
376 EnumIter,
377 EnumString,
378)]
379#[strum(ascii_case_insensitive)]
380#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
381#[cfg_attr(
382 feature = "python",
383 pyo3::pyclass(
384 frozen,
385 eq,
386 eq_int,
387 module = "nautilus_trader.model",
388 from_py_object,
389 rename_all = "SCREAMING_SNAKE_CASE",
390 )
391)]
392#[cfg_attr(
393 feature = "python",
394 pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.model")
395)]
396pub enum BetSide {
397 Back = 1,
399 Lay = 2,
401}
402
403impl BetSide {
404 #[must_use]
406 pub fn opposite(&self) -> Self {
407 match self {
408 Self::Back => Self::Lay,
409 Self::Lay => Self::Back,
410 }
411 }
412}
413
414impl From<OrderSide> for BetSide {
415 fn from(side: OrderSide) -> Self {
416 match side {
417 OrderSide::Buy => Self::Back,
418 OrderSide::Sell => Self::Lay,
419 }
420 }
421}
422
423#[repr(C)]
425#[derive(
426 Copy,
427 Clone,
428 Debug,
429 Display,
430 Hash,
431 PartialEq,
432 Eq,
433 PartialOrd,
434 Ord,
435 AsRefStr,
436 FromRepr,
437 EnumIter,
438 EnumString,
439)]
440#[strum(ascii_case_insensitive)]
441#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
442#[cfg_attr(
443 feature = "python",
444 pyo3::pyclass(
445 frozen,
446 eq,
447 eq_int,
448 module = "nautilus_trader.model",
449 from_py_object,
450 rename_all = "SCREAMING_SNAKE_CASE",
451 )
452)]
453#[cfg_attr(
454 feature = "python",
455 pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.model")
456)]
457pub enum BookAction {
458 Add = 1,
460 Update = 2,
462 Delete = 3,
464 Clear = 4,
466}
467
468impl FromU8 for BookAction {
469 fn from_u8(value: u8) -> Option<Self> {
470 Self::from_repr(usize::from(value))
471 }
472}
473
474#[repr(C)]
476#[derive(
477 Copy,
478 Clone,
479 Debug,
480 Display,
481 Hash,
482 PartialEq,
483 Eq,
484 PartialOrd,
485 Ord,
486 AsRefStr,
487 FromRepr,
488 EnumIter,
489 EnumString,
490)]
491#[strum(ascii_case_insensitive)]
492#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
493#[cfg_attr(
494 feature = "python",
495 pyo3::pyclass(
496 frozen,
497 eq,
498 eq_int,
499 module = "nautilus_trader.model",
500 from_py_object,
501 rename_all = "SCREAMING_SNAKE_CASE",
502 )
503)]
504#[cfg_attr(
505 feature = "python",
506 pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.model")
507)]
508#[allow(non_camel_case_types)]
509pub enum BookType {
510 L1_MBP = 1,
512 L2_MBP = 2,
514 L3_MBO = 3,
516}
517
518impl FromU8 for BookType {
519 fn from_u8(value: u8) -> Option<Self> {
520 Self::from_repr(usize::from(value))
521 }
522}
523
524#[repr(C)]
531#[derive(
532 Copy,
533 Clone,
534 Debug,
535 Display,
536 Hash,
537 PartialEq,
538 Eq,
539 PartialOrd,
540 Ord,
541 AsRefStr,
542 FromRepr,
543 EnumIter,
544 EnumString,
545)]
546#[strum(ascii_case_insensitive)]
547#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
548#[cfg_attr(
549 feature = "python",
550 pyo3::pyclass(
551 frozen,
552 eq,
553 eq_int,
554 module = "nautilus_trader.model",
555 from_py_object,
556 rename_all = "SCREAMING_SNAKE_CASE",
557 )
558)]
559#[cfg_attr(
560 feature = "python",
561 pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.model")
562)]
563pub enum ContingencyType {
564 Oco = 1,
566 Oto = 2,
568 Ouo = 3,
570}
571
572#[repr(C)]
586#[derive(
587 Copy,
588 Clone,
589 Debug,
590 Default,
591 Display,
592 Hash,
593 PartialEq,
594 Eq,
595 PartialOrd,
596 Ord,
597 AsRefStr,
598 FromRepr,
599 EnumIter,
600 EnumString,
601)]
602#[strum(ascii_case_insensitive)]
603#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
604#[cfg_attr(
605 feature = "python",
606 pyo3::pyclass(
607 frozen,
608 eq,
609 eq_int,
610 module = "nautilus_trader.model",
611 from_py_object,
612 rename_all = "SCREAMING_SNAKE_CASE",
613 )
614)]
615#[cfg_attr(
616 feature = "python",
617 pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.model")
618)]
619pub enum ContinuousFutureAdjustmentType {
620 #[default]
622 BackwardSpread = 1,
623 ForwardSpread = 2,
625 BackwardRatio = 3,
627 ForwardRatio = 4,
629}
630
631impl ContinuousFutureAdjustmentType {
632 #[must_use]
634 pub const fn is_ratio(&self) -> bool {
635 matches!(self, Self::BackwardRatio | Self::ForwardRatio)
636 }
637
638 #[must_use]
640 pub const fn is_backward(&self) -> bool {
641 matches!(self, Self::BackwardSpread | Self::BackwardRatio)
642 }
643}
644
645#[repr(C)]
647#[derive(
648 Copy,
649 Clone,
650 Debug,
651 Display,
652 Hash,
653 PartialEq,
654 Eq,
655 PartialOrd,
656 Ord,
657 AsRefStr,
658 FromRepr,
659 EnumIter,
660 EnumString,
661)]
662#[strum(ascii_case_insensitive)]
663#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
664#[cfg_attr(
665 feature = "python",
666 pyo3::pyclass(
667 frozen,
668 eq,
669 eq_int,
670 module = "nautilus_trader.model",
671 from_py_object,
672 rename_all = "SCREAMING_SNAKE_CASE",
673 )
674)]
675#[cfg_attr(
676 feature = "python",
677 pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.model")
678)]
679pub enum CurrencyType {
680 Crypto = 1,
682 Fiat = 2,
684 CommodityBacked = 3,
686}
687
688#[repr(C)]
690#[derive(
691 Copy,
692 Clone,
693 Debug,
694 Display,
695 Hash,
696 PartialEq,
697 Eq,
698 PartialOrd,
699 Ord,
700 AsRefStr,
701 FromRepr,
702 EnumIter,
703 EnumString,
704)]
705#[strum(ascii_case_insensitive)]
706#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
707#[cfg_attr(
708 feature = "python",
709 pyo3::pyclass(
710 frozen,
711 eq,
712 eq_int,
713 module = "nautilus_trader.model",
714 from_py_object,
715 rename_all = "SCREAMING_SNAKE_CASE",
716 )
717)]
718#[cfg_attr(
719 feature = "python",
720 pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.model")
721)]
722pub enum InstrumentClass {
723 Spot = 1,
725 Swap = 2,
727 Future = 3,
729 FuturesSpread = 4,
731 Forward = 5,
733 Cfd = 6,
735 Bond = 7,
737 Option = 8,
739 OptionSpread = 9,
741 Warrant = 10,
743 SportsBetting = 11,
745 BinaryOption = 12,
747}
748
749impl InstrumentClass {
750 #[must_use]
752 pub const fn has_expiration(&self) -> bool {
753 matches!(
754 self,
755 Self::Future | Self::FuturesSpread | Self::Option | Self::OptionSpread
756 )
757 }
758
759 #[must_use]
761 pub const fn allows_negative_price(&self) -> bool {
762 matches!(
763 self,
764 Self::Option | Self::FuturesSpread | Self::OptionSpread
765 )
766 }
767
768 #[must_use]
773 pub fn try_from_parent_suffix(suffix: &str) -> Option<Self> {
774 match suffix {
775 "FUT" | "FUTURE" => Some(Self::Future),
776 "OPT" | "OPTION" => Some(Self::Option),
777 _ => None,
778 }
779 }
780
781 #[must_use]
786 pub const fn parent_suffix(self) -> Option<&'static str> {
787 match self {
788 Self::Future => Some("FUT"),
789 Self::Option => Some("OPT"),
790 _ => None,
791 }
792 }
793}
794
795#[repr(C)]
797#[derive(
798 Copy,
799 Clone,
800 Debug,
801 Display,
802 Hash,
803 PartialEq,
804 Eq,
805 PartialOrd,
806 Ord,
807 AsRefStr,
808 FromRepr,
809 EnumIter,
810 EnumString,
811)]
812#[strum(ascii_case_insensitive)]
813#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
814#[cfg_attr(
815 feature = "python",
816 pyo3::pyclass(
817 frozen,
818 eq,
819 eq_int,
820 module = "nautilus_trader.model",
821 from_py_object,
822 rename_all = "SCREAMING_SNAKE_CASE",
823 )
824)]
825#[cfg_attr(
826 feature = "python",
827 pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.model")
828)]
829pub enum InstrumentCloseType {
830 EndOfSession = 1,
832 ContractExpired = 2,
834}
835
836impl FromU8 for InstrumentCloseType {
838 fn from_u8(value: u8) -> Option<Self> {
839 Self::from_repr(usize::from(value))
840 }
841}
842
843#[repr(C)]
845#[derive(
846 Copy,
847 Clone,
848 Debug,
849 Display,
850 Hash,
851 PartialEq,
852 Eq,
853 PartialOrd,
854 Ord,
855 AsRefStr,
856 FromRepr,
857 EnumIter,
858 EnumString,
859)]
860#[strum(ascii_case_insensitive)]
861#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
862#[cfg_attr(
863 feature = "python",
864 pyo3::pyclass(
865 frozen,
866 eq,
867 eq_int,
868 module = "nautilus_trader.model",
869 from_py_object,
870 rename_all = "SCREAMING_SNAKE_CASE",
871 )
872)]
873#[cfg_attr(
874 feature = "python",
875 pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.model")
876)]
877pub enum LiquiditySide {
878 NoLiquiditySide = 0,
880 Maker = 1,
882 Taker = 2,
884}
885
886#[repr(C)]
888#[derive(
889 Copy,
890 Clone,
891 Debug,
892 Display,
893 Hash,
894 PartialEq,
895 Eq,
896 PartialOrd,
897 Ord,
898 AsRefStr,
899 FromRepr,
900 EnumIter,
901 EnumString,
902)]
903#[strum(ascii_case_insensitive)]
904#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
905#[cfg_attr(
906 feature = "python",
907 pyo3::pyclass(
908 frozen,
909 eq,
910 eq_int,
911 module = "nautilus_trader.model",
912 from_py_object,
913 rename_all = "SCREAMING_SNAKE_CASE",
914 )
915)]
916#[cfg_attr(
917 feature = "python",
918 pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.model")
919)]
920pub enum MarketStatus {
921 Open = 1,
923 Closed = 2,
925 Paused = 3,
927 Halted = 4,
929 Suspended = 5,
931 NotAvailable = 6,
933}
934
935#[repr(C)]
937#[derive(
938 Copy,
939 Clone,
940 Debug,
941 Display,
942 Hash,
943 PartialEq,
944 Eq,
945 PartialOrd,
946 Ord,
947 AsRefStr,
948 FromRepr,
949 EnumIter,
950 EnumString,
951)]
952#[strum(ascii_case_insensitive)]
953#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
954#[cfg_attr(
955 feature = "python",
956 pyo3::pyclass(
957 frozen,
958 eq,
959 eq_int,
960 module = "nautilus_trader.model",
961 from_py_object,
962 rename_all = "SCREAMING_SNAKE_CASE",
963 )
964)]
965#[cfg_attr(
966 feature = "python",
967 pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.model")
968)]
969pub enum MarketStatusAction {
970 None = 0,
972 PreOpen = 1,
974 PreCross = 2,
976 Quoting = 3,
978 Cross = 4,
980 Rotation = 5,
982 NewPriceIndication = 6,
984 Trading = 7,
986 Halt = 8,
988 Pause = 9,
990 Suspend = 10,
992 PreClose = 11,
994 Close = 12,
996 PostClose = 13,
998 ShortSellRestrictionChange = 14,
1000 NotAvailableForTrading = 15,
1002}
1003
1004impl FromU16 for MarketStatusAction {
1006 fn from_u16(value: u16) -> Option<Self> {
1007 Self::from_repr(usize::from(value))
1008 }
1009}
1010
1011#[repr(C)]
1013#[derive(
1014 Copy,
1015 Clone,
1016 Debug,
1017 Default,
1018 Display,
1019 Hash,
1020 PartialEq,
1021 Eq,
1022 PartialOrd,
1023 Ord,
1024 AsRefStr,
1025 FromRepr,
1026 EnumIter,
1027 EnumString,
1028)]
1029#[strum(ascii_case_insensitive)]
1030#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
1031#[cfg_attr(
1032 feature = "python",
1033 pyo3::pyclass(
1034 frozen,
1035 eq,
1036 eq_int,
1037 module = "nautilus_trader.model",
1038 from_py_object,
1039 rename_all = "SCREAMING_SNAKE_CASE",
1040 )
1041)]
1042#[cfg_attr(
1043 feature = "python",
1044 pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.model")
1045)]
1046pub enum OmsType {
1047 #[default]
1049 Unspecified = 0,
1050 Netting = 1,
1052 Hedging = 2,
1056}
1057
1058#[repr(C)]
1060#[derive(
1061 Copy,
1062 Clone,
1063 Debug,
1064 Display,
1065 Hash,
1066 PartialEq,
1067 Eq,
1068 PartialOrd,
1069 Ord,
1070 AsRefStr,
1071 FromRepr,
1072 EnumIter,
1073 EnumString,
1074)]
1075#[strum(ascii_case_insensitive)]
1076#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
1077#[cfg_attr(
1078 feature = "python",
1079 pyo3::pyclass(
1080 frozen,
1081 eq,
1082 eq_int,
1083 module = "nautilus_trader.model",
1084 from_py_object,
1085 rename_all = "SCREAMING_SNAKE_CASE",
1086 )
1087)]
1088#[cfg_attr(
1089 feature = "python",
1090 pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.model")
1091)]
1092pub enum OptionKind {
1093 Call = 1,
1095 Put = 2,
1097}
1098
1099#[repr(C)]
1111#[derive(
1112 Copy,
1113 Clone,
1114 Debug,
1115 Default,
1116 Display,
1117 Hash,
1118 PartialEq,
1119 Eq,
1120 PartialOrd,
1121 Ord,
1122 AsRefStr,
1123 FromRepr,
1124 EnumIter,
1125 EnumString,
1126)]
1127#[strum(ascii_case_insensitive)]
1128#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
1129#[cfg_attr(
1130 feature = "python",
1131 pyo3::pyclass(
1132 frozen,
1133 eq,
1134 eq_int,
1135 module = "nautilus_trader.model",
1136 from_py_object,
1137 rename_all = "SCREAMING_SNAKE_CASE",
1138 )
1139)]
1140#[cfg_attr(
1141 feature = "python",
1142 pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.model")
1143)]
1144pub enum GreeksConvention {
1145 #[default]
1147 BlackScholes = 1,
1148 PriceAdjusted = 2,
1150}
1151
1152#[repr(C)]
1154#[derive(
1155 Copy,
1156 Clone,
1157 Debug,
1158 Default,
1159 Display,
1160 Hash,
1161 PartialEq,
1162 Eq,
1163 PartialOrd,
1164 Ord,
1165 AsRefStr,
1166 FromRepr,
1167 EnumIter,
1168 EnumString,
1169)]
1170#[strum(ascii_case_insensitive)]
1171#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
1172#[cfg_attr(
1173 feature = "python",
1174 pyo3::pyclass(
1175 frozen,
1176 eq,
1177 eq_int,
1178 module = "nautilus_trader.model",
1179 from_py_object,
1180 rename_all = "SCREAMING_SNAKE_CASE",
1181 )
1182)]
1183#[cfg_attr(
1184 feature = "python",
1185 pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.model")
1186)]
1187pub enum OtoTriggerMode {
1188 #[default]
1190 Partial = 0,
1191 Full = 1,
1193}
1194
1195#[repr(C)]
1200#[derive(
1201 Copy,
1202 Clone,
1203 Debug,
1204 Display,
1205 Hash,
1206 PartialEq,
1207 Eq,
1208 PartialOrd,
1209 Ord,
1210 AsRefStr,
1211 FromRepr,
1212 EnumIter,
1213 EnumString,
1214)]
1215#[strum(ascii_case_insensitive)]
1216#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
1217#[cfg_attr(
1218 feature = "python",
1219 pyo3::pyclass(
1220 frozen,
1221 eq,
1222 eq_int,
1223 module = "nautilus_trader.model",
1224 from_py_object,
1225 rename_all = "SCREAMING_SNAKE_CASE",
1226 )
1227)]
1228#[cfg_attr(
1229 feature = "python",
1230 pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.model")
1231)]
1232pub enum OrderSide {
1233 Buy = 1,
1235 Sell = 2,
1237}
1238
1239impl OrderSide {
1240 #[must_use]
1242 pub fn opposite(&self) -> Self {
1243 match &self {
1244 Self::Buy => Self::Sell,
1245 Self::Sell => Self::Buy,
1246 }
1247 }
1248}
1249
1250#[repr(C)]
1272#[derive(
1273 Copy,
1274 Clone,
1275 Debug,
1276 Display,
1277 Hash,
1278 PartialEq,
1279 Eq,
1280 PartialOrd,
1281 Ord,
1282 AsRefStr,
1283 FromRepr,
1284 EnumIter,
1285 EnumString,
1286)]
1287#[strum(ascii_case_insensitive)]
1288#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
1289#[cfg_attr(
1290 feature = "python",
1291 pyo3::pyclass(
1292 frozen,
1293 eq,
1294 eq_int,
1295 module = "nautilus_trader.model",
1296 from_py_object,
1297 rename_all = "SCREAMING_SNAKE_CASE",
1298 )
1299)]
1300#[cfg_attr(
1301 feature = "python",
1302 pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.model")
1303)]
1304pub enum OrderStatus {
1305 Initialized = 1,
1307 Denied = 2,
1309 Emulated = 3,
1311 Released = 4,
1313 Submitted = 5,
1315 Accepted = 6,
1317 Rejected = 7,
1319 Canceled = 8,
1321 Expired = 9,
1323 Triggered = 10,
1325 PendingUpdate = 11,
1327 PendingCancel = 12,
1329 PartiallyFilled = 13,
1331 Filled = 14,
1333 Voided = 15,
1335}
1336
1337impl OrderStatus {
1338 #[must_use]
1345 pub const fn is_open(self) -> bool {
1346 matches!(
1347 self,
1348 Self::Accepted
1349 | Self::Triggered
1350 | Self::PendingUpdate
1351 | Self::PendingCancel
1352 | Self::PartiallyFilled
1353 )
1354 }
1355
1356 #[must_use]
1358 pub const fn is_closed(self) -> bool {
1359 matches!(
1360 self,
1361 Self::Denied
1362 | Self::Rejected
1363 | Self::Canceled
1364 | Self::Expired
1365 | Self::Filled
1366 | Self::Voided
1367 )
1368 }
1369
1370 #[must_use]
1375 pub const fn is_inflight(self) -> bool {
1376 matches!(
1377 self,
1378 Self::Submitted | Self::PendingUpdate | Self::PendingCancel
1379 )
1380 }
1381
1382 #[must_use]
1384 pub const fn is_cancellable(self) -> bool {
1385 matches!(
1386 self,
1387 Self::Accepted | Self::Triggered | Self::PendingUpdate | Self::PartiallyFilled
1388 )
1389 }
1390}
1391
1392#[repr(C)]
1394#[derive(
1395 Copy,
1396 Clone,
1397 Debug,
1398 Display,
1399 Hash,
1400 PartialEq,
1401 Eq,
1402 PartialOrd,
1403 Ord,
1404 AsRefStr,
1405 FromRepr,
1406 EnumIter,
1407 EnumString,
1408)]
1409#[strum(ascii_case_insensitive)]
1410#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
1411#[cfg_attr(
1412 feature = "python",
1413 pyo3::pyclass(
1414 frozen,
1415 eq,
1416 eq_int,
1417 module = "nautilus_trader.model",
1418 from_py_object,
1419 rename_all = "SCREAMING_SNAKE_CASE",
1420 )
1421)]
1422#[cfg_attr(
1423 feature = "python",
1424 pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.model")
1425)]
1426pub enum OrderType {
1427 Market = 1,
1429 Limit = 2,
1431 StopMarket = 3,
1433 StopLimit = 4,
1435 MarketToLimit = 5,
1437 MarketIfTouched = 6,
1439 LimitIfTouched = 7,
1441 TrailingStopMarket = 8,
1443 TrailingStopLimit = 9,
1445}
1446
1447#[repr(C)]
1449#[derive(
1450 Copy,
1451 Clone,
1452 Debug,
1453 Display,
1454 Hash,
1455 PartialEq,
1456 Eq,
1457 PartialOrd,
1458 Ord,
1459 AsRefStr,
1460 FromRepr,
1461 EnumIter,
1462 EnumString,
1463)]
1464#[strum(ascii_case_insensitive)]
1465#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
1466#[cfg_attr(
1467 feature = "python",
1468 pyo3::pyclass(
1469 frozen,
1470 eq,
1471 eq_int,
1472 module = "nautilus_trader.model",
1473 from_py_object,
1474 rename_all = "SCREAMING_SNAKE_CASE",
1475 )
1476)]
1477#[cfg_attr(
1478 feature = "python",
1479 pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.model")
1480)]
1481pub enum PositionAdjustmentType {
1482 Commission = 1,
1484 Funding = 2,
1486}
1487
1488impl FromU8 for PositionAdjustmentType {
1489 fn from_u8(value: u8) -> Option<Self> {
1490 Self::from_repr(usize::from(value))
1491 }
1492}
1493
1494#[repr(C)]
1499#[derive(
1500 Copy,
1501 Clone,
1502 Debug,
1503 Display,
1504 Hash,
1505 PartialEq,
1506 Eq,
1507 PartialOrd,
1508 Ord,
1509 AsRefStr,
1510 FromRepr,
1511 EnumIter,
1512 EnumString,
1513)]
1514#[strum(ascii_case_insensitive)]
1515#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
1516#[cfg_attr(
1517 feature = "python",
1518 pyo3::pyclass(
1519 frozen,
1520 eq,
1521 eq_int,
1522 module = "nautilus_trader.model",
1523 from_py_object,
1524 rename_all = "SCREAMING_SNAKE_CASE",
1525 )
1526)]
1527#[cfg_attr(
1528 feature = "python",
1529 pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.model")
1530)]
1531pub enum PositionSide {
1532 Flat = 1,
1534 Long = 2,
1536 Short = 3,
1538}
1539
1540pub mod serde_option_order_side {
1542 use serde::{Deserializer, Serializer};
1543
1544 use super::{OrderSide, deserialize_optional_enum, serialize_optional_enum};
1545
1546 pub fn serialize<S>(value: &Option<OrderSide>, serializer: S) -> Result<S::Ok, S::Error>
1552 where
1553 S: Serializer,
1554 {
1555 serialize_optional_enum(value.as_ref(), serializer, "NO_ORDER_SIDE")
1556 }
1557
1558 pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<OrderSide>, D::Error>
1564 where
1565 D: Deserializer<'de>,
1566 {
1567 deserialize_optional_enum(
1568 deserializer,
1569 "NO_ORDER_SIDE",
1570 "BUY, SELL, NO_ORDER_SIDE, or null",
1571 )
1572 }
1573}
1574
1575pub mod serde_option_position_side {
1577 use serde::{Deserializer, Serializer};
1578
1579 use super::{PositionSide, deserialize_optional_enum, serialize_optional_enum};
1580
1581 pub fn serialize<S>(value: &Option<PositionSide>, serializer: S) -> Result<S::Ok, S::Error>
1587 where
1588 S: Serializer,
1589 {
1590 serialize_optional_enum(value.as_ref(), serializer, "NO_POSITION_SIDE")
1591 }
1592
1593 pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<PositionSide>, D::Error>
1599 where
1600 D: Deserializer<'de>,
1601 {
1602 deserialize_optional_enum(
1603 deserializer,
1604 "NO_POSITION_SIDE",
1605 "FLAT, LONG, SHORT, NO_POSITION_SIDE, or null",
1606 )
1607 }
1608}
1609
1610pub mod serde_option_contingency_type {
1612 use serde::{Deserializer, Serializer};
1613
1614 use super::{ContingencyType, deserialize_optional_enum, serialize_optional_enum};
1615
1616 pub fn serialize<S>(value: &Option<ContingencyType>, serializer: S) -> Result<S::Ok, S::Error>
1622 where
1623 S: Serializer,
1624 {
1625 serialize_optional_enum(value.as_ref(), serializer, "NO_CONTINGENCY")
1626 }
1627
1628 pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<ContingencyType>, D::Error>
1634 where
1635 D: Deserializer<'de>,
1636 {
1637 deserialize_optional_enum(
1638 deserializer,
1639 "NO_CONTINGENCY",
1640 "OCO, OTO, OUO, NO_CONTINGENCY, or null",
1641 )
1642 }
1643}
1644
1645pub mod serde_option_trailing_offset_type {
1648 use serde::{Deserializer, Serializer};
1649
1650 use super::{TrailingOffsetType, deserialize_optional_enum, serialize_optional_enum};
1651
1652 pub fn serialize<S>(
1658 value: &Option<TrailingOffsetType>,
1659 serializer: S,
1660 ) -> Result<S::Ok, S::Error>
1661 where
1662 S: Serializer,
1663 {
1664 serialize_optional_enum(value.as_ref(), serializer, "NO_TRAILING_OFFSET")
1665 }
1666
1667 pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<TrailingOffsetType>, D::Error>
1673 where
1674 D: Deserializer<'de>,
1675 {
1676 deserialize_optional_enum(
1677 deserializer,
1678 "NO_TRAILING_OFFSET",
1679 "PRICE, BASIS_POINTS, TICKS, PRICE_TIER, NO_TRAILING_OFFSET, or null",
1680 )
1681 }
1682}
1683
1684pub mod serde_option_trigger_type {
1686 use serde::{Deserializer, Serializer};
1687
1688 use super::{TriggerType, deserialize_optional_enum, serialize_optional_enum};
1689
1690 pub fn serialize<S>(value: &Option<TriggerType>, serializer: S) -> Result<S::Ok, S::Error>
1696 where
1697 S: Serializer,
1698 {
1699 serialize_optional_enum(value.as_ref(), serializer, "NO_TRIGGER")
1700 }
1701
1702 pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<TriggerType>, D::Error>
1708 where
1709 D: Deserializer<'de>,
1710 {
1711 deserialize_optional_enum(
1712 deserializer,
1713 "NO_TRIGGER",
1714 "a trigger type, NO_TRIGGER, or null",
1715 )
1716 }
1717}
1718
1719fn serialize_optional_enum<S, T>(
1720 value: Option<&T>,
1721 serializer: S,
1722 none_token: &'static str,
1723) -> Result<S::Ok, S::Error>
1724where
1725 S: Serializer,
1726 T: AsRef<str>,
1727{
1728 serializer.serialize_str(value.map_or(none_token, AsRef::as_ref))
1729}
1730
1731fn deserialize_optional_enum<'de, D, T>(
1732 deserializer: D,
1733 none_token: &'static str,
1734 expected: &'static str,
1735) -> Result<Option<T>, D::Error>
1736where
1737 D: Deserializer<'de>,
1738 T: FromStr,
1739 T::Err: Display,
1740{
1741 struct OptionalEnumVisitor<T> {
1742 none_token: &'static str,
1743 expected: &'static str,
1744 marker: PhantomData<T>,
1745 }
1746
1747 impl<'de, T> serde::de::Visitor<'de> for OptionalEnumVisitor<T>
1748 where
1749 T: FromStr,
1750 T::Err: Display,
1751 {
1752 type Value = Option<T>;
1753
1754 fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1755 formatter.write_str(self.expected)
1756 }
1757
1758 fn visit_none<E>(self) -> Result<Self::Value, E> {
1759 Ok(None)
1760 }
1761
1762 fn visit_unit<E>(self) -> Result<Self::Value, E> {
1763 Ok(None)
1764 }
1765
1766 fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
1767 where
1768 D: Deserializer<'de>,
1769 {
1770 let value = Cow::<'de, str>::deserialize(deserializer)?;
1771 if value.eq_ignore_ascii_case(self.none_token) {
1772 Ok(None)
1773 } else {
1774 T::from_str(&value)
1775 .map(Some)
1776 .map_err(serde::de::Error::custom)
1777 }
1778 }
1779 }
1780
1781 deserializer.deserialize_option(OptionalEnumVisitor {
1782 none_token,
1783 expected,
1784 marker: PhantomData,
1785 })
1786}
1787
1788#[repr(C)]
1790#[derive(
1791 Copy,
1792 Clone,
1793 Debug,
1794 Display,
1795 Hash,
1796 PartialEq,
1797 Eq,
1798 PartialOrd,
1799 Ord,
1800 AsRefStr,
1801 FromRepr,
1802 EnumIter,
1803 EnumString,
1804)]
1805#[strum(ascii_case_insensitive)]
1806#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
1807#[cfg_attr(
1808 feature = "python",
1809 pyo3::pyclass(
1810 frozen,
1811 eq,
1812 eq_int,
1813 module = "nautilus_trader.model",
1814 from_py_object,
1815 rename_all = "SCREAMING_SNAKE_CASE",
1816 )
1817)]
1818#[cfg_attr(
1819 feature = "python",
1820 pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.model")
1821)]
1822pub enum PriceType {
1823 Bid = 1,
1829 Ask = 2,
1832 Mid = 3,
1834 Last = 4,
1836 Mark = 5,
1839}
1840
1841#[repr(C)]
1843#[derive(
1844 Copy,
1845 Clone,
1846 Debug,
1847 Display,
1848 Hash,
1849 PartialEq,
1850 Eq,
1851 PartialOrd,
1852 Ord,
1853 AsRefStr,
1854 FromRepr,
1855 EnumIter,
1856 EnumString,
1857)]
1858#[strum(ascii_case_insensitive)]
1859#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
1860#[cfg_attr(
1861 feature = "python",
1862 pyo3::pyclass(
1863 frozen,
1864 eq,
1865 eq_int,
1866 module = "nautilus_trader.model",
1867 from_py_object,
1868 rename_all = "SCREAMING_SNAKE_CASE",
1869 )
1870)]
1871#[cfg_attr(
1872 feature = "python",
1873 pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.model")
1874)]
1875#[allow(non_camel_case_types)]
1876pub enum RecordFlag {
1877 F_LAST = 1 << 7, F_TOB = 1 << 6, F_SNAPSHOT = 1 << 5, F_MBP = 1 << 4, RESERVED_2 = 1 << 3, RESERVED_1 = 1 << 2, }
1890
1891impl RecordFlag {
1892 #[must_use]
1894 pub fn matches(self, value: u8) -> bool {
1895 (self as u8) & value != 0
1896 }
1897}
1898
1899#[repr(C)]
1901#[derive(
1902 Copy,
1903 Clone,
1904 Debug,
1905 Display,
1906 Hash,
1907 PartialEq,
1908 Eq,
1909 PartialOrd,
1910 Ord,
1911 AsRefStr,
1912 FromRepr,
1913 EnumIter,
1914 EnumString,
1915)]
1916#[strum(ascii_case_insensitive)]
1917#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
1918#[cfg_attr(
1919 feature = "python",
1920 pyo3::pyclass(
1921 frozen,
1922 eq,
1923 eq_int,
1924 module = "nautilus_trader.model",
1925 from_py_object,
1926 rename_all = "SCREAMING_SNAKE_CASE",
1927 )
1928)]
1929#[cfg_attr(
1930 feature = "python",
1931 pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.model")
1932)]
1933pub enum TimeInForce {
1934 Gtc = 1,
1936 Ioc = 2,
1938 Fok = 3,
1940 Gtd = 4,
1942 Day = 5,
1944 AtTheOpen = 6,
1946 AtTheClose = 7,
1948}
1949
1950#[repr(C)]
1952#[derive(
1953 Copy,
1954 Clone,
1955 Debug,
1956 Display,
1957 Hash,
1958 PartialEq,
1959 Eq,
1960 PartialOrd,
1961 Ord,
1962 AsRefStr,
1963 FromRepr,
1964 EnumIter,
1965 EnumString,
1966)]
1967#[strum(ascii_case_insensitive)]
1968#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
1969#[cfg_attr(
1970 feature = "python",
1971 pyo3::pyclass(
1972 frozen,
1973 eq,
1974 eq_int,
1975 module = "nautilus_trader.model",
1976 from_py_object,
1977 rename_all = "SCREAMING_SNAKE_CASE",
1978 )
1979)]
1980#[cfg_attr(
1981 feature = "python",
1982 pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.model")
1983)]
1984pub enum TradingState {
1985 Active = 1,
1987 Reducing = 2,
1989 Halted = 3,
1991}
1992
1993#[repr(C)]
1998#[derive(
1999 Copy,
2000 Clone,
2001 Debug,
2002 Display,
2003 Hash,
2004 PartialEq,
2005 Eq,
2006 PartialOrd,
2007 Ord,
2008 AsRefStr,
2009 FromRepr,
2010 EnumIter,
2011 EnumString,
2012)]
2013#[strum(ascii_case_insensitive)]
2014#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
2015#[cfg_attr(
2016 feature = "python",
2017 pyo3::pyclass(
2018 frozen,
2019 eq,
2020 eq_int,
2021 module = "nautilus_trader.model",
2022 from_py_object,
2023 rename_all = "SCREAMING_SNAKE_CASE",
2024 )
2025)]
2026#[cfg_attr(
2027 feature = "python",
2028 pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.model")
2029)]
2030pub enum TrailingOffsetType {
2031 Price = 1,
2033 BasisPoints = 2,
2035 Ticks = 3,
2037 PriceTier = 4,
2039}
2040
2041#[repr(C)]
2046#[derive(
2047 Copy,
2048 Clone,
2049 Debug,
2050 Display,
2051 Hash,
2052 PartialEq,
2053 Eq,
2054 PartialOrd,
2055 Ord,
2056 AsRefStr,
2057 FromRepr,
2058 EnumIter,
2059 EnumString,
2060)]
2061#[strum(ascii_case_insensitive)]
2062#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
2063#[cfg_attr(
2064 feature = "python",
2065 pyo3::pyclass(
2066 frozen,
2067 eq,
2068 eq_int,
2069 module = "nautilus_trader.model",
2070 from_py_object,
2071 rename_all = "SCREAMING_SNAKE_CASE",
2072 )
2073)]
2074#[cfg_attr(
2075 feature = "python",
2076 pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.model")
2077)]
2078pub enum TriggerType {
2079 Default = 1,
2081 LastPrice = 2,
2083 MarkPrice = 3,
2085 IndexPrice = 4,
2087 BidAsk = 5,
2089 DoubleLast = 6,
2091 DoubleBidAsk = 7,
2093 LastOrBidAsk = 8,
2095 MidPoint = 9,
2097}
2098
2099enum_strum_serde!(AccountType);
2100enum_strum_serde!(AggregationSource);
2101enum_strum_serde!(AggressorSide);
2102enum_strum_serde!(AssetClass);
2103enum_strum_serde!(BarAggregation);
2104enum_strum_serde!(BarIntervalType);
2105enum_strum_serde!(BetSide);
2106enum_strum_serde!(BookAction);
2107enum_strum_serde!(BookType);
2108enum_strum_serde!(ContingencyType);
2109enum_strum_serde!(ContinuousFutureAdjustmentType);
2110enum_strum_serde!(CurrencyType);
2111enum_strum_serde!(GreeksConvention);
2112enum_strum_serde!(InstrumentClass);
2113enum_strum_serde!(InstrumentCloseType);
2114enum_strum_serde!(LiquiditySide);
2115enum_strum_serde!(MarketStatus);
2116enum_strum_serde!(MarketStatusAction);
2117enum_strum_serde!(OmsType);
2118enum_strum_serde!(OptionKind);
2119enum_strum_serde!(OrderSide);
2120enum_strum_serde!(OrderStatus);
2121enum_strum_serde!(OrderType);
2122enum_strum_serde!(OtoTriggerMode);
2123enum_strum_serde!(PositionAdjustmentType);
2124enum_strum_serde!(PositionSide);
2125enum_strum_serde!(PriceType);
2126enum_strum_serde!(RecordFlag);
2127enum_strum_serde!(TimeInForce);
2128enum_strum_serde!(TradingState);
2129enum_strum_serde!(TrailingOffsetType);
2130enum_strum_serde!(TriggerType);
2131
2132#[cfg(test)]
2133mod tests {
2134 use rstest::rstest;
2135 use serde::{Serialize, de::DeserializeOwned};
2136 use strum::IntoEnumIterator;
2137
2138 use super::*;
2139
2140 #[derive(Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
2141 struct OptionalSides {
2142 #[serde(with = "serde_option_order_side")]
2143 order: Option<OrderSide>,
2144 #[serde(with = "serde_option_position_side")]
2145 position: Option<PositionSide>,
2146 }
2147
2148 #[derive(Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
2149 struct OptionalOrderTypes {
2150 #[serde(with = "serde_option_contingency_type")]
2151 contingency: Option<ContingencyType>,
2152 #[serde(with = "serde_option_trailing_offset_type")]
2153 trailing_offset: Option<TrailingOffsetType>,
2154 #[serde(with = "serde_option_trigger_type")]
2155 trigger: Option<TriggerType>,
2156 }
2157
2158 #[rstest]
2159 fn test_optional_sides_serde_preserves_legacy_none_tokens() {
2160 let value = OptionalSides {
2161 order: None,
2162 position: None,
2163 };
2164
2165 let json = serde_json::to_string(&value).unwrap();
2166 let decoded: OptionalSides = serde_json::from_str(&json).unwrap();
2167
2168 assert_eq!(
2169 json,
2170 r#"{"order":"NO_ORDER_SIDE","position":"NO_POSITION_SIDE"}"#
2171 );
2172 assert_eq!(decoded, value);
2173 }
2174
2175 #[rstest]
2176 fn test_optional_sides_serde_accepts_null_and_valid_sides() {
2177 let json = r#"{"order":null,"position":"LONG"}"#;
2178 let decoded: OptionalSides = serde_json::from_str(json).unwrap();
2179
2180 assert_eq!(
2181 decoded,
2182 OptionalSides {
2183 order: None,
2184 position: Some(PositionSide::Long),
2185 }
2186 );
2187 }
2188
2189 #[rstest]
2190 fn test_optional_order_types_serde_preserves_legacy_none_tokens() {
2191 let value = OptionalOrderTypes {
2192 contingency: None,
2193 trailing_offset: None,
2194 trigger: None,
2195 };
2196
2197 let json = serde_json::to_string(&value).unwrap();
2198 let decoded: OptionalOrderTypes = serde_json::from_str(&json).unwrap();
2199
2200 assert_eq!(
2201 json,
2202 r#"{"contingency":"NO_CONTINGENCY","trailing_offset":"NO_TRAILING_OFFSET","trigger":"NO_TRIGGER"}"#,
2203 );
2204 assert_eq!(decoded, value);
2205 }
2206
2207 #[rstest]
2208 fn test_optional_order_types_serde_accepts_null_and_valid_values() {
2209 let json = r#"{"contingency":null,"trailing_offset":"PRICE","trigger":"LAST_PRICE"}"#;
2210 let decoded: OptionalOrderTypes = serde_json::from_str(json).unwrap();
2211
2212 assert_eq!(
2213 decoded,
2214 OptionalOrderTypes {
2215 contingency: None,
2216 trailing_offset: Some(TrailingOffsetType::Price),
2217 trigger: Some(TriggerType::LastPrice),
2218 },
2219 );
2220 }
2221
2222 #[rstest]
2223 #[case(r#"{"contingency":"INVALID","trailing_offset":"NO_TRAILING_OFFSET","trigger":"NO_TRIGGER"}"#)]
2224 #[case(
2225 r#"{"contingency":"NO_CONTINGENCY","trailing_offset":"INVALID","trigger":"NO_TRIGGER"}"#
2226 )]
2227 #[case(r#"{"contingency":"NO_CONTINGENCY","trailing_offset":"NO_TRAILING_OFFSET","trigger":"INVALID"}"#)]
2228 fn test_optional_order_types_serde_rejects_invalid_values(#[case] json: &str) {
2229 assert!(serde_json::from_str::<OptionalOrderTypes>(json).is_err());
2230 }
2231
2232 #[rstest]
2233 #[case::bet_side_back(BetSide::Back, r#""BACK""#)]
2234 #[case::bet_side_lay(BetSide::Lay, r#""LAY""#)]
2235 fn test_bet_side_serde_uses_canonical_label(#[case] value: BetSide, #[case] expected: &str) {
2236 let json = serde_json::to_string(&value).unwrap();
2237
2238 assert_eq!(json, expected);
2239 assert_eq!(serde_json::from_str::<BetSide>(&json).unwrap(), value);
2240 }
2241
2242 #[rstest]
2243 #[case::oto_trigger_mode_partial(OtoTriggerMode::Partial, r#""PARTIAL""#)]
2244 #[case::oto_trigger_mode_full(OtoTriggerMode::Full, r#""FULL""#)]
2245 fn test_oto_trigger_mode_serde_uses_canonical_label(
2246 #[case] value: OtoTriggerMode,
2247 #[case] expected: &str,
2248 ) {
2249 let json = serde_json::to_string(&value).unwrap();
2250
2251 assert_eq!(json, expected);
2252 assert_eq!(
2253 serde_json::from_str::<OtoTriggerMode>(&json).unwrap(),
2254 value
2255 );
2256 }
2257
2258 #[rstest]
2259 #[case::pascal(r#""Back""#, BetSide::Back)]
2260 #[case::lower(r#""lay""#, BetSide::Lay)]
2261 fn test_bet_side_serde_is_case_insensitive(#[case] json: &str, #[case] expected: BetSide) {
2262 assert_eq!(serde_json::from_str::<BetSide>(json).unwrap(), expected);
2263 }
2264
2265 #[rstest]
2266 #[case::wrong_domain(r#""BUY""#)]
2267 #[case::empty(r#""""#)]
2268 fn test_bet_side_serde_rejects_unknown_label(#[case] json: &str) {
2269 assert!(serde_json::from_str::<BetSide>(json).is_err());
2270 }
2271
2272 #[rstest]
2273 #[case::no_aggressor(0, Some(AggressorSide::NoAggressor))]
2274 #[case::buy(1, Some(AggressorSide::Buy))]
2275 #[case::sell(2, Some(AggressorSide::Sell))]
2276 #[case::invalid(3, None)]
2277 #[case::max_u8(255, None)]
2278 fn test_aggressor_side_from_u8(#[case] value: u8, #[case] expected: Option<AggressorSide>) {
2279 assert_eq!(AggressorSide::from_u8(value), expected);
2280 }
2281
2282 #[rstest]
2283 #[case::active(TradingState::Active, 1)]
2284 #[case::reducing(TradingState::Reducing, 2)]
2285 #[case::halted(TradingState::Halted, 3)]
2286 fn test_trading_state_discriminants(#[case] state: TradingState, #[case] value: usize) {
2287 assert_eq!(state as usize, value);
2288 assert_eq!(TradingState::from_repr(value), Some(state));
2289 }
2290
2291 #[rstest]
2292 #[case(AggressorSide::NoAggressor, "NO_AGGRESSOR")]
2293 #[case(AggressorSide::Buy, "BUY")]
2294 #[case(AggressorSide::Sell, "SELL")]
2295 fn test_aggressor_side_to_string(#[case] value: AggressorSide, #[case] expected: &str) {
2296 assert_eq!(value.to_string(), expected);
2297 assert_eq!(value.as_ref(), expected);
2298 }
2299
2300 #[rstest]
2301 #[case(AggressorSide::NoAggressor, "NO_AGGRESSOR")]
2302 #[case(AggressorSide::Buy, "BUY")]
2303 #[case(AggressorSide::Sell, "SELL")]
2304 #[case(AggressorSide::Buy, "BUYER")]
2305 #[case(AggressorSide::Sell, "SELLER")]
2306 #[case(AggressorSide::Buy, "buy")]
2307 #[case(AggressorSide::Sell, "seller")]
2308 fn test_aggressor_side_from_str(#[case] expected: AggressorSide, #[case] value: &str) {
2309 assert_eq!(AggressorSide::from_str(value), Ok(expected));
2310 }
2311
2312 #[rstest]
2313 #[case(AggressorSide::Buy, "\"BUY\"")]
2314 #[case(AggressorSide::Sell, "\"SELL\"")]
2315 #[case(AggressorSide::NoAggressor, "\"NO_AGGRESSOR\"")]
2316 fn test_aggressor_side_serde_roundtrip(#[case] input: AggressorSide, #[case] expected: &str) {
2317 let json = serde_json::to_string(&input).unwrap();
2318 assert_eq!(json, expected);
2319 let parsed: AggressorSide = serde_json::from_str(expected).unwrap();
2320 assert_eq!(parsed, input);
2321 }
2322
2323 #[rstest]
2324 #[case("BUYER", AggressorSide::Buy)]
2325 #[case("SELLER", AggressorSide::Sell)]
2326 fn test_aggressor_side_serde_accepts_historical(
2327 #[case] value: &str,
2328 #[case] expected: AggressorSide,
2329 ) {
2330 let parsed: AggressorSide = serde_json::from_str(&format!("\"{value}\"")).unwrap();
2331 assert_eq!(parsed, expected);
2332 }
2333
2334 #[rstest]
2335 #[case(GreeksConvention::BlackScholes, "\"BLACK_SCHOLES\"")]
2336 #[case(GreeksConvention::PriceAdjusted, "\"PRICE_ADJUSTED\"")]
2337 fn test_greeks_convention_serde_roundtrip(
2338 #[case] input: GreeksConvention,
2339 #[case] expected: &str,
2340 ) {
2341 let json = serde_json::to_string(&input).unwrap();
2342 assert_eq!(json, expected);
2343 let parsed: GreeksConvention = serde_json::from_str(expected).unwrap();
2344 assert_eq!(parsed, input);
2345 }
2346
2347 #[rstest]
2348 fn test_greeks_convention_default_is_black_scholes() {
2349 assert_eq!(GreeksConvention::default(), GreeksConvention::BlackScholes);
2350 }
2351
2352 #[rstest]
2353 #[case(ContinuousFutureAdjustmentType::BackwardSpread, false, true)]
2354 #[case(ContinuousFutureAdjustmentType::ForwardSpread, false, false)]
2355 #[case(ContinuousFutureAdjustmentType::BackwardRatio, true, true)]
2356 #[case(ContinuousFutureAdjustmentType::ForwardRatio, true, false)]
2357 fn test_continuous_future_adjustment_type_predicates(
2358 #[case] mode: ContinuousFutureAdjustmentType,
2359 #[case] expected_is_ratio: bool,
2360 #[case] expected_is_backward: bool,
2361 ) {
2362 assert_eq!(mode.is_ratio(), expected_is_ratio);
2363 assert_eq!(mode.is_backward(), expected_is_backward);
2364 }
2365
2366 #[rstest]
2367 #[case(ContinuousFutureAdjustmentType::BackwardSpread, "\"BACKWARD_SPREAD\"")]
2368 #[case(ContinuousFutureAdjustmentType::ForwardSpread, "\"FORWARD_SPREAD\"")]
2369 #[case(ContinuousFutureAdjustmentType::BackwardRatio, "\"BACKWARD_RATIO\"")]
2370 #[case(ContinuousFutureAdjustmentType::ForwardRatio, "\"FORWARD_RATIO\"")]
2371 fn test_continuous_future_adjustment_type_serde_roundtrip(
2372 #[case] input: ContinuousFutureAdjustmentType,
2373 #[case] expected: &str,
2374 ) {
2375 let json = serde_json::to_string(&input).unwrap();
2376 assert_eq!(json, expected);
2377 let parsed: ContinuousFutureAdjustmentType = serde_json::from_str(expected).unwrap();
2378 assert_eq!(parsed, input);
2379 }
2380
2381 #[rstest]
2382 fn test_continuous_future_adjustment_type_default_is_backward_spread() {
2383 assert_eq!(
2384 ContinuousFutureAdjustmentType::default(),
2385 ContinuousFutureAdjustmentType::BackwardSpread,
2386 );
2387 }
2388
2389 #[rstest]
2390 #[case(InstrumentClass::Option, true)]
2391 #[case(InstrumentClass::FuturesSpread, true)]
2392 #[case(InstrumentClass::OptionSpread, true)]
2393 #[case(InstrumentClass::Spot, false)]
2394 #[case(InstrumentClass::Swap, false)]
2395 #[case(InstrumentClass::Future, false)]
2396 #[case(InstrumentClass::Forward, false)]
2397 #[case(InstrumentClass::Cfd, false)]
2398 #[case(InstrumentClass::Bond, false)]
2399 #[case(InstrumentClass::Warrant, false)]
2400 #[case(InstrumentClass::SportsBetting, false)]
2401 #[case(InstrumentClass::BinaryOption, false)]
2402 fn test_instrument_class_allows_negative_price(
2403 #[case] class: InstrumentClass,
2404 #[case] expected: bool,
2405 ) {
2406 assert_eq!(class.allows_negative_price(), expected);
2407 }
2408
2409 #[rstest]
2410 #[case("FUT", Some(InstrumentClass::Future))]
2411 #[case("FUTURE", Some(InstrumentClass::Future))]
2412 #[case("OPT", Some(InstrumentClass::Option))]
2413 #[case("OPTION", Some(InstrumentClass::Option))]
2414 #[case("fut", None)]
2415 #[case("Fut", None)]
2416 #[case("option", None)]
2417 #[case("Option", None)]
2418 #[case("SPREAD", None)]
2419 #[case("UNKNOWN", None)]
2420 #[case("", None)]
2421 fn test_instrument_class_try_from_parent_suffix(
2422 #[case] suffix: &str,
2423 #[case] expected: Option<InstrumentClass>,
2424 ) {
2425 assert_eq!(InstrumentClass::try_from_parent_suffix(suffix), expected);
2426 }
2427
2428 #[rstest]
2429 #[case(InstrumentClass::Future, Some("FUT"))]
2430 #[case(InstrumentClass::Option, Some("OPT"))]
2431 #[case(InstrumentClass::Spot, None)]
2432 #[case(InstrumentClass::Swap, None)]
2433 #[case(InstrumentClass::FuturesSpread, None)]
2434 #[case(InstrumentClass::Forward, None)]
2435 #[case(InstrumentClass::Cfd, None)]
2436 #[case(InstrumentClass::Bond, None)]
2437 #[case(InstrumentClass::OptionSpread, None)]
2438 #[case(InstrumentClass::Warrant, None)]
2439 #[case(InstrumentClass::SportsBetting, None)]
2440 #[case(InstrumentClass::BinaryOption, None)]
2441 fn test_instrument_class_parent_suffix(
2442 #[case] class: InstrumentClass,
2443 #[case] expected: Option<&'static str>,
2444 ) {
2445 assert_eq!(class.parent_suffix(), expected);
2446 }
2447
2448 #[rstest]
2449 #[case(InstrumentClass::Future)]
2450 #[case(InstrumentClass::Option)]
2451 fn test_instrument_class_parent_suffix_roundtrip(#[case] class: InstrumentClass) {
2452 let suffix = class.parent_suffix().unwrap();
2453 assert_eq!(InstrumentClass::try_from_parent_suffix(suffix), Some(class));
2454 }
2455
2456 fn assert_enum_string_contract<T>(type_name: &str)
2460 where
2461 T: IntoEnumIterator
2462 + Copy
2463 + std::fmt::Debug
2464 + PartialEq
2465 + Display
2466 + AsRef<str>
2467 + FromStr
2468 + Serialize
2469 + DeserializeOwned,
2470 {
2471 for variant in T::iter() {
2472 let wire = variant.to_string();
2473 assert_eq!(
2474 variant.as_ref(),
2475 wire,
2476 "{type_name}::{variant:?} must expose the same name through `AsRef` and `Display`",
2477 );
2478 assert_eq!(
2479 T::from_str(&wire).ok(),
2480 Some(variant),
2481 "{type_name} must parse `{wire}` back to {variant:?}",
2482 );
2483 assert_eq!(
2484 T::from_str(&wire.to_ascii_lowercase()).ok(),
2485 Some(variant),
2486 "{type_name} must parse `{wire}` case-insensitively",
2487 );
2488 let json = serde_json::to_string(&variant).unwrap();
2489 assert_eq!(
2490 json,
2491 format!("\"{wire}\""),
2492 "{type_name}::{variant:?} must serialize as its display name",
2493 );
2494 assert_eq!(
2495 serde_json::from_str::<T>(&json).unwrap(),
2496 variant,
2497 "{type_name}::{variant:?} must round-trip through JSON",
2498 );
2499 }
2500 }
2501
2502 #[rstest]
2503 fn test_enum_string_and_serde_contract() {
2504 macro_rules! assert_contract {
2505 ($($t:ty),+ $(,)?) => {
2506 $(assert_enum_string_contract::<$t>(stringify!($t));)+
2507 };
2508 }
2509
2510 assert_contract!(
2511 AccountType,
2512 AggregationSource,
2513 AggressorSide,
2514 AssetClass,
2515 BarAggregation,
2516 BarIntervalType,
2517 BetSide,
2518 BookAction,
2519 BookType,
2520 ContingencyType,
2521 ContinuousFutureAdjustmentType,
2522 CurrencyType,
2523 GreeksConvention,
2524 InstrumentClass,
2525 InstrumentCloseType,
2526 LiquiditySide,
2527 MarketStatus,
2528 MarketStatusAction,
2529 OmsType,
2530 OptionKind,
2531 OrderSide,
2532 OrderStatus,
2533 OrderType,
2534 OtoTriggerMode,
2535 PositionAdjustmentType,
2536 PositionSide,
2537 PriceType,
2538 RecordFlag,
2539 TimeInForce,
2540 TradingState,
2541 TrailingOffsetType,
2542 TriggerType,
2543 );
2544 }
2545
2546 const EXPECTED_WIRE_NAMES: &[&str] = &[
2551 "AccountType::Betting=BETTING",
2552 "AccountType::Cash=CASH",
2553 "AccountType::Margin=MARGIN",
2554 "AccountType::Wallet=WALLET",
2555 "AggregationSource::External=EXTERNAL",
2556 "AggregationSource::Internal=INTERNAL",
2557 "AggressorSide::Buy=BUY",
2558 "AggressorSide::NoAggressor=NO_AGGRESSOR",
2559 "AggressorSide::Sell=SELL",
2560 "AssetClass::Alternative=ALTERNATIVE",
2561 "AssetClass::Commodity=COMMODITY",
2562 "AssetClass::Cryptocurrency=CRYPTOCURRENCY",
2563 "AssetClass::Debt=DEBT",
2564 "AssetClass::Equity=EQUITY",
2565 "AssetClass::FX=FX",
2566 "AssetClass::Index=INDEX",
2567 "BarAggregation::Day=DAY",
2568 "BarAggregation::Hour=HOUR",
2569 "BarAggregation::Millisecond=MILLISECOND",
2570 "BarAggregation::Minute=MINUTE",
2571 "BarAggregation::Month=MONTH",
2572 "BarAggregation::Renko=RENKO",
2573 "BarAggregation::Second=SECOND",
2574 "BarAggregation::Tick=TICK",
2575 "BarAggregation::TickImbalance=TICK_IMBALANCE",
2576 "BarAggregation::TickRuns=TICK_RUNS",
2577 "BarAggregation::Value=VALUE",
2578 "BarAggregation::ValueImbalance=VALUE_IMBALANCE",
2579 "BarAggregation::ValueRuns=VALUE_RUNS",
2580 "BarAggregation::Volume=VOLUME",
2581 "BarAggregation::VolumeImbalance=VOLUME_IMBALANCE",
2582 "BarAggregation::VolumeRuns=VOLUME_RUNS",
2583 "BarAggregation::Week=WEEK",
2584 "BarAggregation::Year=YEAR",
2585 "BarIntervalType::LeftOpen=LEFT_OPEN",
2586 "BarIntervalType::RightOpen=RIGHT_OPEN",
2587 "BetSide::Back=BACK",
2588 "BetSide::Lay=LAY",
2589 "BookAction::Add=ADD",
2590 "BookAction::Clear=CLEAR",
2591 "BookAction::Delete=DELETE",
2592 "BookAction::Update=UPDATE",
2593 "BookType::L1_MBP=L1_MBP",
2594 "BookType::L2_MBP=L2_MBP",
2595 "BookType::L3_MBO=L3_MBO",
2596 "ContingencyType::Oco=OCO",
2597 "ContingencyType::Oto=OTO",
2598 "ContingencyType::Ouo=OUO",
2599 "ContinuousFutureAdjustmentType::BackwardRatio=BACKWARD_RATIO",
2600 "ContinuousFutureAdjustmentType::BackwardSpread=BACKWARD_SPREAD",
2601 "ContinuousFutureAdjustmentType::ForwardRatio=FORWARD_RATIO",
2602 "ContinuousFutureAdjustmentType::ForwardSpread=FORWARD_SPREAD",
2603 "CurrencyType::CommodityBacked=COMMODITY_BACKED",
2604 "CurrencyType::Crypto=CRYPTO",
2605 "CurrencyType::Fiat=FIAT",
2606 "GreeksConvention::BlackScholes=BLACK_SCHOLES",
2607 "GreeksConvention::PriceAdjusted=PRICE_ADJUSTED",
2608 "InstrumentClass::BinaryOption=BINARY_OPTION",
2609 "InstrumentClass::Bond=BOND",
2610 "InstrumentClass::Cfd=CFD",
2611 "InstrumentClass::Forward=FORWARD",
2612 "InstrumentClass::Future=FUTURE",
2613 "InstrumentClass::FuturesSpread=FUTURES_SPREAD",
2614 "InstrumentClass::Option=OPTION",
2615 "InstrumentClass::OptionSpread=OPTION_SPREAD",
2616 "InstrumentClass::SportsBetting=SPORTS_BETTING",
2617 "InstrumentClass::Spot=SPOT",
2618 "InstrumentClass::Swap=SWAP",
2619 "InstrumentClass::Warrant=WARRANT",
2620 "InstrumentCloseType::ContractExpired=CONTRACT_EXPIRED",
2621 "InstrumentCloseType::EndOfSession=END_OF_SESSION",
2622 "LiquiditySide::Maker=MAKER",
2623 "LiquiditySide::NoLiquiditySide=NO_LIQUIDITY_SIDE",
2624 "LiquiditySide::Taker=TAKER",
2625 "MarketStatus::Closed=CLOSED",
2626 "MarketStatus::Halted=HALTED",
2627 "MarketStatus::NotAvailable=NOT_AVAILABLE",
2628 "MarketStatus::Open=OPEN",
2629 "MarketStatus::Paused=PAUSED",
2630 "MarketStatus::Suspended=SUSPENDED",
2631 "MarketStatusAction::Close=CLOSE",
2632 "MarketStatusAction::Cross=CROSS",
2633 "MarketStatusAction::Halt=HALT",
2634 "MarketStatusAction::NewPriceIndication=NEW_PRICE_INDICATION",
2635 "MarketStatusAction::None=NONE",
2636 "MarketStatusAction::NotAvailableForTrading=NOT_AVAILABLE_FOR_TRADING",
2637 "MarketStatusAction::Pause=PAUSE",
2638 "MarketStatusAction::PostClose=POST_CLOSE",
2639 "MarketStatusAction::PreClose=PRE_CLOSE",
2640 "MarketStatusAction::PreCross=PRE_CROSS",
2641 "MarketStatusAction::PreOpen=PRE_OPEN",
2642 "MarketStatusAction::Quoting=QUOTING",
2643 "MarketStatusAction::Rotation=ROTATION",
2644 "MarketStatusAction::ShortSellRestrictionChange=SHORT_SELL_RESTRICTION_CHANGE",
2645 "MarketStatusAction::Suspend=SUSPEND",
2646 "MarketStatusAction::Trading=TRADING",
2647 "OmsType::Hedging=HEDGING",
2648 "OmsType::Netting=NETTING",
2649 "OmsType::Unspecified=UNSPECIFIED",
2650 "OptionKind::Call=CALL",
2651 "OptionKind::Put=PUT",
2652 "OrderSide::Buy=BUY",
2653 "OrderSide::Sell=SELL",
2654 "OrderStatus::Accepted=ACCEPTED",
2655 "OrderStatus::Canceled=CANCELED",
2656 "OrderStatus::Denied=DENIED",
2657 "OrderStatus::Emulated=EMULATED",
2658 "OrderStatus::Expired=EXPIRED",
2659 "OrderStatus::Filled=FILLED",
2660 "OrderStatus::Initialized=INITIALIZED",
2661 "OrderStatus::PartiallyFilled=PARTIALLY_FILLED",
2662 "OrderStatus::PendingCancel=PENDING_CANCEL",
2663 "OrderStatus::PendingUpdate=PENDING_UPDATE",
2664 "OrderStatus::Rejected=REJECTED",
2665 "OrderStatus::Released=RELEASED",
2666 "OrderStatus::Submitted=SUBMITTED",
2667 "OrderStatus::Triggered=TRIGGERED",
2668 "OrderStatus::Voided=VOIDED",
2669 "OrderType::Limit=LIMIT",
2670 "OrderType::LimitIfTouched=LIMIT_IF_TOUCHED",
2671 "OrderType::Market=MARKET",
2672 "OrderType::MarketIfTouched=MARKET_IF_TOUCHED",
2673 "OrderType::MarketToLimit=MARKET_TO_LIMIT",
2674 "OrderType::StopLimit=STOP_LIMIT",
2675 "OrderType::StopMarket=STOP_MARKET",
2676 "OrderType::TrailingStopLimit=TRAILING_STOP_LIMIT",
2677 "OrderType::TrailingStopMarket=TRAILING_STOP_MARKET",
2678 "OtoTriggerMode::Full=FULL",
2679 "OtoTriggerMode::Partial=PARTIAL",
2680 "PositionAdjustmentType::Commission=COMMISSION",
2681 "PositionAdjustmentType::Funding=FUNDING",
2682 "PositionSide::Flat=FLAT",
2683 "PositionSide::Long=LONG",
2684 "PositionSide::Short=SHORT",
2685 "PriceType::Ask=ASK",
2686 "PriceType::Bid=BID",
2687 "PriceType::Last=LAST",
2688 "PriceType::Mark=MARK",
2689 "PriceType::Mid=MID",
2690 "RecordFlag::F_LAST=F_LAST",
2691 "RecordFlag::F_MBP=F_MBP",
2692 "RecordFlag::F_SNAPSHOT=F_SNAPSHOT",
2693 "RecordFlag::F_TOB=F_TOB",
2694 "RecordFlag::RESERVED_1=RESERVED_1",
2695 "RecordFlag::RESERVED_2=RESERVED_2",
2696 "TimeInForce::AtTheClose=AT_THE_CLOSE",
2697 "TimeInForce::AtTheOpen=AT_THE_OPEN",
2698 "TimeInForce::Day=DAY",
2699 "TimeInForce::Fok=FOK",
2700 "TimeInForce::Gtc=GTC",
2701 "TimeInForce::Gtd=GTD",
2702 "TimeInForce::Ioc=IOC",
2703 "TradingState::Active=ACTIVE",
2704 "TradingState::Halted=HALTED",
2705 "TradingState::Reducing=REDUCING",
2706 "TrailingOffsetType::BasisPoints=BASIS_POINTS",
2707 "TrailingOffsetType::Price=PRICE",
2708 "TrailingOffsetType::PriceTier=PRICE_TIER",
2709 "TrailingOffsetType::Ticks=TICKS",
2710 "TriggerType::BidAsk=BID_ASK",
2711 "TriggerType::Default=DEFAULT",
2712 "TriggerType::DoubleBidAsk=DOUBLE_BID_ASK",
2713 "TriggerType::DoubleLast=DOUBLE_LAST",
2714 "TriggerType::IndexPrice=INDEX_PRICE",
2715 "TriggerType::LastOrBidAsk=LAST_OR_BID_ASK",
2716 "TriggerType::LastPrice=LAST_PRICE",
2717 "TriggerType::MarkPrice=MARK_PRICE",
2718 "TriggerType::MidPoint=MID_POINT",
2719 ];
2720
2721 #[rstest]
2722 fn test_enum_wire_names_are_stable() {
2723 let mut actual = Vec::new();
2724 macro_rules! collect_wire_names {
2725 ($($t:ty),+ $(,)?) => {
2726 $(for variant in <$t>::iter() {
2727 actual.push(format!("{}::{variant:?}={variant}", stringify!($t)));
2728 })+
2729 };
2730 }
2731
2732 collect_wire_names!(
2733 AccountType,
2734 AggregationSource,
2735 AggressorSide,
2736 AssetClass,
2737 BarAggregation,
2738 BarIntervalType,
2739 BetSide,
2740 BookAction,
2741 BookType,
2742 ContingencyType,
2743 ContinuousFutureAdjustmentType,
2744 CurrencyType,
2745 GreeksConvention,
2746 InstrumentClass,
2747 InstrumentCloseType,
2748 LiquiditySide,
2749 MarketStatus,
2750 MarketStatusAction,
2751 OmsType,
2752 OptionKind,
2753 OrderSide,
2754 OrderStatus,
2755 OrderType,
2756 OtoTriggerMode,
2757 PositionAdjustmentType,
2758 PositionSide,
2759 PriceType,
2760 RecordFlag,
2761 TimeInForce,
2762 TradingState,
2763 TrailingOffsetType,
2764 TriggerType,
2765 );
2766
2767 actual.sort();
2768
2769 assert_eq!(
2770 actual.len(),
2771 EXPECTED_WIRE_NAMES.len(),
2772 "variant count changed; update `EXPECTED_WIRE_NAMES`",
2773 );
2774
2775 for (got, expected) in actual.iter().zip(EXPECTED_WIRE_NAMES) {
2776 assert_eq!(got, expected);
2777 }
2778 }
2779
2780 macro_rules! assert_numeric_mapping {
2788 ($t:ty, $from:ident, $width:ty, $($value:literal => $variant:expr),+ $(,)?) => {{
2789 let valid: &[$width] = &[$($value),+];
2790 $(
2791 assert_eq!(
2792 <$t>::$from($value),
2793 Some($variant),
2794 "{}::{}({}) must map to the pinned variant",
2795 stringify!($t),
2796 stringify!($from),
2797 $value,
2798 );
2799 )+
2800
2801 for value in <$width>::MIN..=<$width>::MAX {
2802 if !valid.contains(&value) {
2803 assert_eq!(
2804 <$t>::$from(value),
2805 None,
2806 "{} must reject {value}",
2807 stringify!($t),
2808 );
2809 }
2810 }
2811 }};
2812 }
2813
2814 #[rstest]
2815 fn test_from_u8_pins_numeric_discriminants() {
2816 assert_numeric_mapping!(
2817 AggressorSide, from_u8, u8,
2818 0 => AggressorSide::NoAggressor,
2819 1 => AggressorSide::Buy,
2820 2 => AggressorSide::Sell,
2821 );
2822 assert_numeric_mapping!(
2823 AssetClass, from_u8, u8,
2824 1 => AssetClass::FX,
2825 2 => AssetClass::Equity,
2826 3 => AssetClass::Commodity,
2827 4 => AssetClass::Debt,
2828 5 => AssetClass::Index,
2829 6 => AssetClass::Cryptocurrency,
2830 7 => AssetClass::Alternative,
2831 );
2832 assert_numeric_mapping!(
2833 BookAction, from_u8, u8,
2834 1 => BookAction::Add,
2835 2 => BookAction::Update,
2836 3 => BookAction::Delete,
2837 4 => BookAction::Clear,
2838 );
2839 assert_numeric_mapping!(
2840 BookType, from_u8, u8,
2841 1 => BookType::L1_MBP,
2842 2 => BookType::L2_MBP,
2843 3 => BookType::L3_MBO,
2844 );
2845 assert_numeric_mapping!(
2846 InstrumentCloseType, from_u8, u8,
2847 1 => InstrumentCloseType::EndOfSession,
2848 2 => InstrumentCloseType::ContractExpired,
2849 );
2850 assert_numeric_mapping!(
2851 PositionAdjustmentType, from_u8, u8,
2852 1 => PositionAdjustmentType::Commission,
2853 2 => PositionAdjustmentType::Funding,
2854 );
2855 }
2856
2857 #[rstest]
2858 fn test_market_status_action_from_u16_pins_numeric_discriminants() {
2859 assert_numeric_mapping!(
2860 MarketStatusAction, from_u16, u16,
2861 0 => MarketStatusAction::None,
2862 1 => MarketStatusAction::PreOpen,
2863 2 => MarketStatusAction::PreCross,
2864 3 => MarketStatusAction::Quoting,
2865 4 => MarketStatusAction::Cross,
2866 5 => MarketStatusAction::Rotation,
2867 6 => MarketStatusAction::NewPriceIndication,
2868 7 => MarketStatusAction::Trading,
2869 8 => MarketStatusAction::Halt,
2870 9 => MarketStatusAction::Pause,
2871 10 => MarketStatusAction::Suspend,
2872 11 => MarketStatusAction::PreClose,
2873 12 => MarketStatusAction::Close,
2874 13 => MarketStatusAction::PostClose,
2875 14 => MarketStatusAction::ShortSellRestrictionChange,
2876 15 => MarketStatusAction::NotAvailableForTrading,
2877 );
2878 }
2879
2880 #[rstest]
2881 #[case(OrderStatus::Initialized, false, false, false, false)]
2882 #[case(OrderStatus::Denied, false, true, false, false)]
2883 #[case(OrderStatus::Emulated, false, false, false, false)]
2884 #[case(OrderStatus::Released, false, false, false, false)]
2885 #[case(OrderStatus::Submitted, false, false, false, true)]
2886 #[case(OrderStatus::Accepted, true, false, true, false)]
2887 #[case(OrderStatus::Rejected, false, true, false, false)]
2888 #[case(OrderStatus::Canceled, false, true, false, false)]
2889 #[case(OrderStatus::Expired, false, true, false, false)]
2890 #[case(OrderStatus::Triggered, true, false, true, false)]
2891 #[case(OrderStatus::PendingUpdate, true, false, true, true)]
2892 #[case(OrderStatus::PendingCancel, true, false, false, true)]
2893 #[case(OrderStatus::PartiallyFilled, true, false, true, false)]
2894 #[case(OrderStatus::Filled, false, true, false, false)]
2895 #[case(OrderStatus::Voided, false, true, false, false)]
2896 fn test_order_status_predicates(
2897 #[case] status: OrderStatus,
2898 #[case] expected_open: bool,
2899 #[case] expected_closed: bool,
2900 #[case] expected_cancellable: bool,
2901 #[case] expected_inflight: bool,
2902 ) {
2903 assert_eq!(status.is_open(), expected_open, "{status} is_open");
2904 assert_eq!(status.is_closed(), expected_closed, "{status} is_closed");
2905 assert_eq!(
2906 status.is_cancellable(),
2907 expected_cancellable,
2908 "{status} is_cancellable",
2909 );
2910 assert_eq!(
2911 status.is_inflight(),
2912 expected_inflight,
2913 "{status} is_inflight"
2914 );
2915 }
2916
2917 #[rstest]
2918 fn test_order_status_predicates_hold_across_all_variants() {
2919 for status in OrderStatus::iter() {
2920 assert!(
2921 !(status.is_open() && status.is_closed()),
2922 "{status} cannot be both open and closed",
2923 );
2924 assert!(
2925 !status.is_cancellable() || status.is_open(),
2926 "{status} must be open to be cancellable",
2927 );
2928 assert!(
2929 !(status.is_inflight() && status.is_closed()),
2930 "{status} cannot be both in-flight and closed",
2931 );
2932 }
2933 }
2934
2935 #[rstest]
2936 #[case(OrderSide::Buy, OrderSide::Sell)]
2937 #[case(OrderSide::Sell, OrderSide::Buy)]
2938 fn test_order_side_opposite(#[case] side: OrderSide, #[case] expected: OrderSide) {
2939 assert_eq!(side.opposite(), expected);
2940 assert_eq!(side.opposite().opposite(), side);
2941 }
2942
2943 #[rstest]
2944 #[case(BetSide::Back, BetSide::Lay)]
2945 #[case(BetSide::Lay, BetSide::Back)]
2946 fn test_bet_side_opposite(#[case] side: BetSide, #[case] expected: BetSide) {
2947 assert_eq!(side.opposite(), expected);
2948 assert_eq!(side.opposite().opposite(), side);
2949 }
2950
2951 #[rstest]
2952 #[case(OrderSide::Buy, BetSide::Back)]
2953 #[case(OrderSide::Sell, BetSide::Lay)]
2954 fn test_bet_side_from_order_side(#[case] side: OrderSide, #[case] expected: BetSide) {
2955 assert_eq!(BetSide::from(side), expected);
2956 }
2957
2958 #[rstest]
2959 #[case(RecordFlag::F_LAST, 0b0000_0000, false)]
2960 #[case(RecordFlag::F_LAST, 0b1000_0000, true)]
2961 #[case(RecordFlag::F_LAST, 0b0111_1111, false)]
2962 #[case(RecordFlag::F_TOB, 0b1100_0000, true)]
2963 #[case(RecordFlag::F_TOB, 0b1000_0000, false)]
2964 #[case(RecordFlag::F_SNAPSHOT, 0b0010_0000, true)]
2965 #[case(RecordFlag::F_SNAPSHOT, 0b1101_1111, false)]
2966 #[case(RecordFlag::F_MBP, 0b0001_0000, true)]
2967 #[case(RecordFlag::RESERVED_2, 0b0000_1000, true)]
2968 #[case(RecordFlag::RESERVED_1, 0b0000_0100, true)]
2969 #[case(RecordFlag::RESERVED_1, 0b0000_1000, false)]
2970 #[case(RecordFlag::F_LAST, u8::MAX, true)]
2971 fn test_record_flag_matches(
2972 #[case] flag: RecordFlag,
2973 #[case] value: u8,
2974 #[case] expected: bool,
2975 ) {
2976 assert_eq!(flag.matches(value), expected);
2977 }
2978
2979 #[rstest]
2980 fn test_record_flag_matches_only_its_own_bit() {
2981 for flag in RecordFlag::iter() {
2982 let bit = flag as u8;
2983 assert_eq!(bit.count_ones(), 1, "{flag} must occupy exactly one bit");
2984 assert!(flag.matches(bit), "{flag} must match its own bit");
2985 assert!(
2986 !flag.matches(!bit),
2987 "{flag} must not match the inverse mask"
2988 );
2989 }
2990 }
2991
2992 #[rstest]
2993 #[case(InstrumentClass::Future, true)]
2994 #[case(InstrumentClass::FuturesSpread, true)]
2995 #[case(InstrumentClass::Option, true)]
2996 #[case(InstrumentClass::OptionSpread, true)]
2997 #[case(InstrumentClass::Spot, false)]
2998 #[case(InstrumentClass::Swap, false)]
2999 #[case(InstrumentClass::Forward, false)]
3000 #[case(InstrumentClass::Cfd, false)]
3001 #[case(InstrumentClass::Bond, false)]
3002 #[case(InstrumentClass::Warrant, false)]
3003 #[case(InstrumentClass::SportsBetting, false)]
3004 #[case(InstrumentClass::BinaryOption, false)]
3005 fn test_instrument_class_has_expiration(
3006 #[case] class: InstrumentClass,
3007 #[case] expected: bool,
3008 ) {
3009 assert_eq!(class.has_expiration(), expected);
3010 }
3011
3012 #[rstest]
3013 fn test_optional_sides_serde_round_trips_present_values() {
3014 let value = OptionalSides {
3015 order: Some(OrderSide::Sell),
3016 position: Some(PositionSide::Short),
3017 };
3018
3019 let json = serde_json::to_string(&value).unwrap();
3020
3021 assert_eq!(json, r#"{"order":"SELL","position":"SHORT"}"#);
3022 assert_eq!(serde_json::from_str::<OptionalSides>(&json).unwrap(), value);
3023 }
3024
3025 #[rstest]
3026 #[case(r#"{"order":"no_order_side","position":"FLAT"}"#)]
3027 #[case(r#"{"order":"No_Order_Side","position":"FLAT"}"#)]
3028 fn test_optional_sides_serde_accepts_none_token_in_any_case(#[case] json: &str) {
3029 let decoded: OptionalSides = serde_json::from_str(json).unwrap();
3030
3031 assert_eq!(
3032 decoded,
3033 OptionalSides {
3034 order: None,
3035 position: Some(PositionSide::Flat),
3036 },
3037 );
3038 }
3039
3040 #[rstest]
3041 #[case(r#"{"order":"INVALID","position":"FLAT"}"#)]
3042 #[case(r#"{"order":5,"position":"FLAT"}"#)]
3043 #[case(r#"{"order":"BUY","position":"INVALID"}"#)]
3044 #[case(r#"{"order":"BUY","position":true}"#)]
3045 fn test_optional_sides_serde_rejects_invalid_values(#[case] json: &str) {
3046 assert!(serde_json::from_str::<OptionalSides>(json).is_err());
3047 }
3048}