1use nautilus_model::{
19 data::BarSpecification,
20 enums::{
21 AggressorSide, AssetClass, BarAggregation, MarketStatusAction, OrderSide, OrderStatus,
22 PositionSide, TimeInForce,
23 },
24};
25use serde::{Deserialize, Serialize};
26use strum::{AsRefStr, Display, EnumIter, EnumString};
27
28use super::consts::{
29 AX_HTTP_SANDBOX_URL, AX_HTTP_URL, AX_ORDERS_SANDBOX_URL, AX_ORDERS_URL, AX_WS_PRIVATE_URL,
30 AX_WS_PUBLIC_URL, AX_WS_SANDBOX_PRIVATE_URL, AX_WS_SANDBOX_PUBLIC_URL,
31};
32
33#[derive(
35 Clone,
36 Copy,
37 Debug,
38 Default,
39 Display,
40 Eq,
41 PartialEq,
42 Hash,
43 AsRefStr,
44 EnumIter,
45 EnumString,
46 Serialize,
47 Deserialize,
48)]
49#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
50#[strum(ascii_case_insensitive)]
51#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
52#[cfg_attr(
53 feature = "python",
54 pyo3::pyclass(
55 eq,
56 eq_int,
57 frozen,
58 hash,
59 module = "nautilus_trader.adapters.architect_ax",
60 from_py_object,
61 rename_all = "SCREAMING_SNAKE_CASE",
62 )
63)]
64#[cfg_attr(
65 feature = "python",
66 pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.adapters.architect_ax")
67)]
68pub enum AxEnvironment {
69 #[default]
71 Sandbox,
72 Production,
74}
75
76impl AxEnvironment {
77 #[must_use]
79 pub const fn http_url(&self) -> &'static str {
80 match self {
81 Self::Sandbox => AX_HTTP_SANDBOX_URL,
82 Self::Production => AX_HTTP_URL,
83 }
84 }
85
86 #[must_use]
88 pub const fn orders_url(&self) -> &'static str {
89 match self {
90 Self::Sandbox => AX_ORDERS_SANDBOX_URL,
91 Self::Production => AX_ORDERS_URL,
92 }
93 }
94
95 #[must_use]
97 pub const fn ws_md_url(&self) -> &'static str {
98 match self {
99 Self::Sandbox => AX_WS_SANDBOX_PUBLIC_URL,
100 Self::Production => AX_WS_PUBLIC_URL,
101 }
102 }
103
104 #[must_use]
106 pub const fn ws_orders_url(&self) -> &'static str {
107 match self {
108 Self::Sandbox => AX_WS_SANDBOX_PRIVATE_URL,
109 Self::Production => AX_WS_PRIVATE_URL,
110 }
111 }
112}
113
114#[derive(
119 Clone,
120 Copy,
121 Debug,
122 Display,
123 Eq,
124 PartialEq,
125 Hash,
126 AsRefStr,
127 EnumIter,
128 EnumString,
129 Serialize,
130 Deserialize,
131)]
132#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
133#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
134#[cfg_attr(
135 feature = "python",
136 pyo3::pyclass(
137 eq,
138 eq_int,
139 frozen,
140 hash,
141 module = "nautilus_trader.adapters.architect_ax",
142 from_py_object,
143 rename_all = "SCREAMING_SNAKE_CASE",
144 )
145)]
146pub enum AxInstrumentState {
147 PreOpen,
149 Open,
151 Closed,
153 ClosedFrozen,
155 Halted,
157 MatchAndCloseAuction,
159 Suspended,
161 Delisted,
163 #[serde(other)]
165 Unknown,
166}
167
168impl AxInstrumentState {
169 #[must_use]
171 pub fn is_tradeable(self) -> bool {
172 matches!(self, Self::Open | Self::PreOpen)
173 }
174}
175
176impl From<AxInstrumentState> for MarketStatusAction {
177 fn from(state: AxInstrumentState) -> Self {
178 match state {
179 AxInstrumentState::PreOpen => Self::PreOpen,
180 AxInstrumentState::Open => Self::Trading,
181 AxInstrumentState::Closed | AxInstrumentState::ClosedFrozen => Self::Close,
182 AxInstrumentState::Halted => Self::Halt,
183 AxInstrumentState::MatchAndCloseAuction => Self::Cross,
184 AxInstrumentState::Suspended => Self::Suspend,
185 AxInstrumentState::Delisted | AxInstrumentState::Unknown => {
186 Self::NotAvailableForTrading
187 }
188 }
189 }
190}
191
192#[derive(
199 Clone,
200 Copy,
201 Debug,
202 Display,
203 Eq,
204 PartialEq,
205 Hash,
206 AsRefStr,
207 EnumIter,
208 EnumString,
209 Serialize,
210 Deserialize,
211)]
212#[serde(rename_all = "snake_case")]
213#[strum(serialize_all = "snake_case")]
214pub enum AxCategory {
215 Fx,
216 Equities,
217 Metals,
218 Energy,
219 EnergyEtfs,
220 Treasuries,
221 Compute,
222 Crypto,
223 #[serde(other)]
224 Unknown,
225}
226
227impl From<AxCategory> for AssetClass {
228 fn from(category: AxCategory) -> Self {
229 match category {
230 AxCategory::Fx => Self::FX,
231 AxCategory::Equities | AxCategory::EnergyEtfs => Self::Equity,
232 AxCategory::Metals | AxCategory::Energy => Self::Commodity,
233 AxCategory::Crypto => Self::Cryptocurrency,
234 AxCategory::Treasuries => Self::Debt,
235 AxCategory::Compute | AxCategory::Unknown => Self::Alternative,
236 }
237 }
238}
239
240#[derive(
245 Clone,
246 Copy,
247 Debug,
248 Display,
249 Eq,
250 PartialEq,
251 Hash,
252 AsRefStr,
253 EnumIter,
254 EnumString,
255 Serialize,
256 Deserialize,
257)]
258#[cfg_attr(
259 feature = "python",
260 pyo3::pyclass(
261 eq,
262 eq_int,
263 frozen,
264 hash,
265 module = "nautilus_trader.adapters.architect_ax",
266 from_py_object,
267 rename_all = "SCREAMING_SNAKE_CASE",
268 )
269)]
270pub enum AxOrderSide {
271 #[serde(rename = "B")]
273 #[strum(serialize = "B")]
274 Buy,
275 #[serde(rename = "S")]
277 #[strum(serialize = "S")]
278 Sell,
279}
280
281impl From<AxOrderSide> for AggressorSide {
282 fn from(side: AxOrderSide) -> Self {
283 match side {
284 AxOrderSide::Buy => Self::Buy,
285 AxOrderSide::Sell => Self::Sell,
286 }
287 }
288}
289
290impl From<AxOrderSide> for OrderSide {
291 fn from(side: AxOrderSide) -> Self {
292 match side {
293 AxOrderSide::Buy => Self::Buy,
294 AxOrderSide::Sell => Self::Sell,
295 }
296 }
297}
298
299impl From<AxOrderSide> for PositionSide {
300 fn from(side: AxOrderSide) -> Self {
301 match side {
302 AxOrderSide::Buy => Self::Long,
303 AxOrderSide::Sell => Self::Short,
304 }
305 }
306}
307
308impl From<OrderSide> for AxOrderSide {
309 fn from(side: OrderSide) -> Self {
310 match side {
311 OrderSide::Buy => Self::Buy,
312 OrderSide::Sell => Self::Sell,
313 }
314 }
315}
316
317#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
322#[serde(rename_all = "snake_case")]
323pub enum AxFundingVariant {
324 DailyClose,
326 IntradayTwap,
328 #[serde(other)]
330 Unknown,
331}
332
333#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
338#[serde(rename_all = "snake_case")]
339pub enum AxFundingSlotStatus {
340 Realized,
342 Projected,
344 Skipped,
346 Pending,
348 #[serde(other)]
350 Unknown,
351}
352
353#[derive(
358 Clone,
359 Copy,
360 Debug,
361 Display,
362 Eq,
363 PartialEq,
364 Hash,
365 AsRefStr,
366 EnumIter,
367 EnumString,
368 Serialize,
369 Deserialize,
370)]
371#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
372#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
373#[cfg_attr(
374 feature = "python",
375 pyo3::pyclass(
376 eq,
377 eq_int,
378 frozen,
379 hash,
380 module = "nautilus_trader.adapters.architect_ax",
381 from_py_object,
382 rename_all = "SCREAMING_SNAKE_CASE",
383 )
384)]
385pub enum AxOrderStatus {
386 Pending,
388 Accepted,
390 PartiallyFilled,
392 Filled,
394 Canceling,
396 Canceled,
398 Rejected,
400 Expired,
402 Replaced,
404 DoneForDay,
406 Out,
408 ReconciledOut,
410 Stale,
412 #[serde(other)]
414 Unknown,
415}
416
417impl TryFrom<AxOrderStatus> for OrderStatus {
418 type Error = anyhow::Error;
419
420 fn try_from(status: AxOrderStatus) -> Result<Self, Self::Error> {
421 Ok(match status {
422 AxOrderStatus::Pending => Self::Submitted,
423 AxOrderStatus::Accepted => Self::Accepted,
424 AxOrderStatus::PartiallyFilled => Self::PartiallyFilled,
425 AxOrderStatus::Filled => Self::Filled,
426 AxOrderStatus::Canceling => Self::PendingCancel,
427 AxOrderStatus::Canceled => Self::Canceled,
428 AxOrderStatus::Rejected => Self::Rejected,
429 AxOrderStatus::Expired => Self::Expired,
430 AxOrderStatus::Replaced => Self::Canceled,
431 AxOrderStatus::DoneForDay => Self::Expired,
432 AxOrderStatus::Out => Self::Canceled,
433 AxOrderStatus::ReconciledOut => Self::Canceled,
434 AxOrderStatus::Stale => Self::Accepted,
435 AxOrderStatus::Unknown => anyhow::bail!("Unmapped AX order status"),
436 })
437 }
438}
439
440#[derive(
445 Clone,
446 Copy,
447 Debug,
448 Display,
449 Eq,
450 PartialEq,
451 Hash,
452 AsRefStr,
453 EnumIter,
454 EnumString,
455 Serialize,
456 Deserialize,
457)]
458#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
459#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
460#[cfg_attr(
461 feature = "python",
462 pyo3::pyclass(
463 eq,
464 eq_int,
465 frozen,
466 hash,
467 module = "nautilus_trader.adapters.architect_ax",
468 from_py_object,
469 rename_all = "SCREAMING_SNAKE_CASE",
470 )
471)]
472pub enum AxTimeInForce {
473 Gtc,
475 Gtd,
477 Day,
479 Ioc,
481 Fok,
483 Ato,
485 Atc,
487 #[serde(other)]
489 Unknown,
490}
491
492impl TryFrom<AxTimeInForce> for TimeInForce {
493 type Error = anyhow::Error;
494
495 fn try_from(tif: AxTimeInForce) -> Result<Self, Self::Error> {
496 Ok(match tif {
497 AxTimeInForce::Gtc => Self::Gtc,
498 AxTimeInForce::Gtd => Self::Gtd,
499 AxTimeInForce::Day => Self::Day,
500 AxTimeInForce::Ioc => Self::Ioc,
501 AxTimeInForce::Fok => Self::Fok,
502 AxTimeInForce::Ato => Self::AtTheOpen,
503 AxTimeInForce::Atc => Self::AtTheClose,
504 AxTimeInForce::Unknown => anyhow::bail!("Unmapped AX time in force"),
505 })
506 }
507}
508
509impl TryFrom<TimeInForce> for AxTimeInForce {
510 type Error = &'static str;
511
512 fn try_from(tif: TimeInForce) -> Result<Self, Self::Error> {
513 match tif {
514 TimeInForce::Gtc => Ok(Self::Gtc),
515 TimeInForce::Gtd => Ok(Self::Gtd),
516 TimeInForce::Day => Ok(Self::Day),
517 TimeInForce::Ioc => Ok(Self::Ioc),
518 TimeInForce::Fok => Ok(Self::Fok),
519 TimeInForce::AtTheOpen => Ok(Self::Ato),
520 TimeInForce::AtTheClose => Ok(Self::Atc),
521 }
522 }
523}
524
525#[derive(
536 Clone,
537 Copy,
538 Debug,
539 Display,
540 Eq,
541 PartialEq,
542 Hash,
543 AsRefStr,
544 EnumIter,
545 EnumString,
546 Serialize,
547 Deserialize,
548)]
549#[strum(ascii_case_insensitive)]
550#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
551#[cfg_attr(
552 feature = "python",
553 pyo3::pyclass(
554 eq,
555 eq_int,
556 frozen,
557 hash,
558 module = "nautilus_trader.adapters.architect_ax",
559 from_py_object,
560 rename_all = "SCREAMING_SNAKE_CASE",
561 )
562)]
563#[cfg_attr(
564 feature = "python",
565 pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.adapters.architect_ax")
566)]
567pub enum AxMarketDataLevel {
568 #[serde(rename = "LEVEL_1")]
570 #[strum(serialize = "LEVEL_1")]
571 Level1,
572 #[serde(rename = "LEVEL_2")]
574 #[strum(serialize = "LEVEL_2")]
575 Level2,
576 #[serde(rename = "LEVEL_3")]
578 #[strum(serialize = "LEVEL_3")]
579 Level3,
580 #[serde(rename = "TRADES")]
582 #[strum(serialize = "TRADES")]
583 Trades,
584}
585
586#[derive(
591 Clone,
592 Copy,
593 Debug,
594 Display,
595 Eq,
596 PartialEq,
597 Hash,
598 AsRefStr,
599 EnumIter,
600 EnumString,
601 Serialize,
602 Deserialize,
603)]
604pub enum AxCandleWidth {
605 #[serde(rename = "1s")]
607 #[strum(serialize = "1s")]
608 Seconds1,
609 #[serde(rename = "5s")]
611 #[strum(serialize = "5s")]
612 Seconds5,
613 #[serde(rename = "1m")]
615 #[strum(serialize = "1m")]
616 Minutes1,
617 #[serde(rename = "5m")]
619 #[strum(serialize = "5m")]
620 Minutes5,
621 #[serde(rename = "15m")]
623 #[strum(serialize = "15m")]
624 Minutes15,
625 #[serde(rename = "1h")]
627 #[strum(serialize = "1h")]
628 Hours1,
629 #[serde(rename = "1d")]
631 #[strum(serialize = "1d")]
632 Days1,
633}
634
635impl TryFrom<&BarSpecification> for AxCandleWidth {
636 type Error = anyhow::Error;
637
638 fn try_from(spec: &BarSpecification) -> Result<Self, Self::Error> {
639 let step = spec.step.get();
640 match (step, spec.aggregation) {
641 (1, BarAggregation::Second) => Ok(Self::Seconds1),
642 (5, BarAggregation::Second) => Ok(Self::Seconds5),
643 (1, BarAggregation::Minute) => Ok(Self::Minutes1),
644 (5, BarAggregation::Minute) => Ok(Self::Minutes5),
645 (15, BarAggregation::Minute) => Ok(Self::Minutes15),
646 (1, BarAggregation::Hour) => Ok(Self::Hours1),
647 (1, BarAggregation::Day) => Ok(Self::Days1),
648 _ => anyhow::bail!(
649 "Unsupported bar specification for AX: {step}-{:?}",
650 spec.aggregation,
651 ),
652 }
653 }
654}
655
656#[derive(
661 Clone,
662 Copy,
663 Debug,
664 Display,
665 Eq,
666 PartialEq,
667 Hash,
668 AsRefStr,
669 EnumIter,
670 EnumString,
671 Serialize,
672 Deserialize,
673)]
674#[serde(rename_all = "snake_case")]
675#[strum(serialize_all = "snake_case")]
676pub enum AxMdRequestType {
677 Subscribe,
679 Unsubscribe,
681 SubscribeCandles,
683 UnsubscribeCandles,
685}
686
687#[derive(
692 Clone,
693 Copy,
694 Debug,
695 Display,
696 Eq,
697 PartialEq,
698 Hash,
699 AsRefStr,
700 EnumIter,
701 EnumString,
702 Serialize,
703 Deserialize,
704)]
705pub enum AxOrderRequestType {
706 #[serde(rename = "p")]
708 #[strum(serialize = "p")]
709 PlaceOrder,
710 #[serde(rename = "x")]
712 #[strum(serialize = "x")]
713 CancelOrder,
714 #[serde(rename = "o")]
716 #[strum(serialize = "o")]
717 GetOpenOrders,
718}
719
720#[derive(
725 Clone,
726 Copy,
727 Debug,
728 Display,
729 Eq,
730 PartialEq,
731 Hash,
732 AsRefStr,
733 EnumIter,
734 EnumString,
735 Serialize,
736 Deserialize,
737)]
738#[cfg_attr(
739 feature = "python",
740 pyo3::pyclass(
741 eq,
742 eq_int,
743 frozen,
744 hash,
745 module = "nautilus_trader.adapters.architect_ax",
746 from_py_object,
747 rename_all = "SCREAMING_SNAKE_CASE",
748 )
749)]
750pub enum AxMdWsMessageType {
751 #[serde(rename = "h")]
753 #[strum(serialize = "h")]
754 Heartbeat,
755 #[serde(rename = "s")]
757 #[strum(serialize = "s")]
758 Ticker,
759 #[serde(rename = "t")]
761 #[strum(serialize = "t")]
762 Trade,
763 #[serde(rename = "c")]
765 #[strum(serialize = "c")]
766 Candle,
767 #[serde(rename = "1")]
769 #[strum(serialize = "1")]
770 BookLevel1,
771 #[serde(rename = "2")]
773 #[strum(serialize = "2")]
774 BookLevel2,
775 #[serde(rename = "3")]
777 #[strum(serialize = "3")]
778 BookLevel3,
779}
780
781#[derive(
786 Clone,
787 Copy,
788 Debug,
789 Display,
790 Eq,
791 PartialEq,
792 Hash,
793 AsRefStr,
794 EnumIter,
795 EnumString,
796 Serialize,
797 Deserialize,
798)]
799#[cfg_attr(
800 feature = "python",
801 pyo3::pyclass(
802 eq,
803 eq_int,
804 frozen,
805 hash,
806 module = "nautilus_trader.adapters.architect_ax",
807 from_py_object,
808 rename_all = "SCREAMING_SNAKE_CASE",
809 )
810)]
811pub enum AxOrderWsMessageType {
812 #[serde(rename = "h")]
814 #[strum(serialize = "h")]
815 Heartbeat,
816 #[serde(rename = "e")]
818 #[strum(serialize = "e")]
819 CancelRejected,
820 #[serde(rename = "n")]
822 #[strum(serialize = "n")]
823 OrderAcknowledged,
824 #[serde(rename = "c")]
826 #[strum(serialize = "c")]
827 OrderCanceled,
828 #[serde(rename = "r")]
830 #[strum(serialize = "r")]
831 OrderReplaced,
832 #[serde(rename = "j")]
834 #[strum(serialize = "j")]
835 OrderRejected,
836 #[serde(rename = "x")]
838 #[strum(serialize = "x")]
839 OrderExpired,
840 #[serde(rename = "d")]
842 #[strum(serialize = "d")]
843 OrderDoneForDay,
844 #[serde(rename = "p")]
846 #[strum(serialize = "p")]
847 OrderPartiallyFilled,
848 #[serde(rename = "f")]
850 #[strum(serialize = "f")]
851 OrderFilled,
852}
853
854#[derive(
859 Clone,
860 Copy,
861 Debug,
862 Display,
863 Eq,
864 PartialEq,
865 Hash,
866 AsRefStr,
867 EnumIter,
868 EnumString,
869 Serialize,
870 Deserialize,
871)]
872#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
873#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
874#[cfg_attr(
875 feature = "python",
876 pyo3::pyclass(
877 eq,
878 eq_int,
879 frozen,
880 hash,
881 module = "nautilus_trader.adapters.architect_ax",
882 from_py_object,
883 rename_all = "SCREAMING_SNAKE_CASE",
884 )
885)]
886pub enum AxCancelReason {
887 UserRequested,
889 #[serde(other)]
891 Unknown,
892}
893
894#[derive(
899 Clone,
900 Copy,
901 Debug,
902 Display,
903 Eq,
904 PartialEq,
905 Hash,
906 AsRefStr,
907 EnumIter,
908 EnumString,
909 Serialize,
910 Deserialize,
911)]
912#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
913#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
914#[cfg_attr(
915 feature = "python",
916 pyo3::pyclass(
917 eq,
918 eq_int,
919 frozen,
920 hash,
921 module = "nautilus_trader.adapters.architect_ax",
922 from_py_object,
923 rename_all = "SCREAMING_SNAKE_CASE",
924 )
925)]
926pub enum AxCancelRejectionReason {
927 OrderNotFound,
929 #[serde(other)]
931 Unknown,
932}
933
934#[cfg(test)]
935mod tests {
936 use rstest::rstest;
937
938 use super::*;
939
940 #[rstest]
941 #[case(AxInstrumentState::Open, "\"OPEN\"")]
942 #[case(AxInstrumentState::PreOpen, "\"PRE_OPEN\"")]
943 #[case(AxInstrumentState::Closed, "\"CLOSED\"")]
944 #[case(AxInstrumentState::ClosedFrozen, "\"CLOSED_FROZEN\"")]
945 #[case(AxInstrumentState::Halted, "\"HALTED\"")]
946 #[case(AxInstrumentState::MatchAndCloseAuction, "\"MATCH_AND_CLOSE_AUCTION\"")]
947 #[case(AxInstrumentState::Suspended, "\"SUSPENDED\"")]
948 #[case(AxInstrumentState::Delisted, "\"DELISTED\"")]
949 fn test_instrument_state_serialization(
950 #[case] state: AxInstrumentState,
951 #[case] expected: &str,
952 ) {
953 let json = serde_json::to_string(&state).unwrap();
954 assert_eq!(json, expected);
955
956 let parsed: AxInstrumentState = serde_json::from_str(&json).unwrap();
957 assert_eq!(parsed, state);
958 }
959
960 #[rstest]
961 fn test_instrument_state_unknown_string_deserializes_as_unknown() {
962 let parsed: AxInstrumentState = serde_json::from_str("\"SOME_FUTURE_STATE\"").unwrap();
963 assert_eq!(parsed, AxInstrumentState::Unknown);
964 }
965
966 #[rstest]
967 #[case(AxInstrumentState::PreOpen, true)]
968 #[case(AxInstrumentState::Open, true)]
969 #[case(AxInstrumentState::Closed, false)]
970 #[case(AxInstrumentState::ClosedFrozen, false)]
971 #[case(AxInstrumentState::Halted, false)]
972 #[case(AxInstrumentState::MatchAndCloseAuction, false)]
973 #[case(AxInstrumentState::Suspended, false)]
974 #[case(AxInstrumentState::Delisted, false)]
975 #[case(AxInstrumentState::Unknown, false)]
976 fn test_instrument_state_is_tradeable(
977 #[case] state: AxInstrumentState,
978 #[case] expected: bool,
979 ) {
980 assert_eq!(state.is_tradeable(), expected);
981 }
982
983 #[rstest]
984 #[case(AxInstrumentState::PreOpen, MarketStatusAction::PreOpen)]
985 #[case(AxInstrumentState::Open, MarketStatusAction::Trading)]
986 #[case(AxInstrumentState::Closed, MarketStatusAction::Close)]
987 #[case(AxInstrumentState::ClosedFrozen, MarketStatusAction::Close)]
988 #[case(AxInstrumentState::Halted, MarketStatusAction::Halt)]
989 #[case(AxInstrumentState::MatchAndCloseAuction, MarketStatusAction::Cross)]
990 #[case(AxInstrumentState::Suspended, MarketStatusAction::Suspend)]
991 #[case(
992 AxInstrumentState::Delisted,
993 MarketStatusAction::NotAvailableForTrading
994 )]
995 #[case(AxInstrumentState::Unknown, MarketStatusAction::NotAvailableForTrading)]
996 fn test_instrument_state_to_market_status_action(
997 #[case] state: AxInstrumentState,
998 #[case] expected: MarketStatusAction,
999 ) {
1000 assert_eq!(MarketStatusAction::from(state), expected);
1001 }
1002
1003 #[rstest]
1004 #[case(AxOrderSide::Buy, "\"B\"")]
1005 #[case(AxOrderSide::Sell, "\"S\"")]
1006 fn test_order_side_serialization(#[case] side: AxOrderSide, #[case] expected: &str) {
1007 let json = serde_json::to_string(&side).unwrap();
1008 assert_eq!(json, expected);
1009
1010 let parsed: AxOrderSide = serde_json::from_str(&json).unwrap();
1011 assert_eq!(parsed, side);
1012 }
1013
1014 #[rstest]
1015 #[case("\"Buy\"")]
1016 #[case("\"Sell\"")]
1017 fn test_order_side_rejects_long_form(#[case] json: &str) {
1018 let error = serde_json::from_str::<AxOrderSide>(json).unwrap_err();
1019 assert_eq!(error.classify(), serde_json::error::Category::Data);
1020 }
1021
1022 #[rstest]
1023 #[case(AxOrderStatus::Pending, "\"PENDING\"")]
1024 #[case(AxOrderStatus::Accepted, "\"ACCEPTED\"")]
1025 #[case(AxOrderStatus::PartiallyFilled, "\"PARTIALLY_FILLED\"")]
1026 #[case(AxOrderStatus::Filled, "\"FILLED\"")]
1027 #[case(AxOrderStatus::Canceling, "\"CANCELING\"")]
1028 #[case(AxOrderStatus::Canceled, "\"CANCELED\"")]
1029 #[case(AxOrderStatus::Out, "\"OUT\"")]
1030 #[case(AxOrderStatus::ReconciledOut, "\"RECONCILED_OUT\"")]
1031 #[case(AxOrderStatus::Stale, "\"STALE\"")]
1032 fn test_order_status_serialization(#[case] status: AxOrderStatus, #[case] expected: &str) {
1033 let json = serde_json::to_string(&status).unwrap();
1034 assert_eq!(json, expected);
1035
1036 let parsed: AxOrderStatus = serde_json::from_str(&json).unwrap();
1037 assert_eq!(parsed, status);
1038 }
1039
1040 #[rstest]
1041 #[case(AxTimeInForce::Gtc, "\"GTC\"")]
1042 #[case(AxTimeInForce::Ioc, "\"IOC\"")]
1043 #[case(AxTimeInForce::Day, "\"DAY\"")]
1044 #[case(AxTimeInForce::Gtd, "\"GTD\"")]
1045 #[case(AxTimeInForce::Fok, "\"FOK\"")]
1046 #[case(AxTimeInForce::Ato, "\"ATO\"")]
1047 #[case(AxTimeInForce::Atc, "\"ATC\"")]
1048 fn test_time_in_force_serialization(#[case] tif: AxTimeInForce, #[case] expected: &str) {
1049 let json = serde_json::to_string(&tif).unwrap();
1050 assert_eq!(json, expected);
1051
1052 let parsed: AxTimeInForce = serde_json::from_str(&json).unwrap();
1053 assert_eq!(parsed, tif);
1054 }
1055
1056 #[rstest]
1057 #[case(AxMarketDataLevel::Level1, "\"LEVEL_1\"")]
1058 #[case(AxMarketDataLevel::Level2, "\"LEVEL_2\"")]
1059 #[case(AxMarketDataLevel::Level3, "\"LEVEL_3\"")]
1060 #[case(AxMarketDataLevel::Trades, "\"TRADES\"")]
1061 fn test_market_data_level_serialization(
1062 #[case] level: AxMarketDataLevel,
1063 #[case] expected: &str,
1064 ) {
1065 let json = serde_json::to_string(&level).unwrap();
1066 assert_eq!(json, expected);
1067
1068 let parsed: AxMarketDataLevel = serde_json::from_str(&json).unwrap();
1069 assert_eq!(parsed, level);
1070 }
1071
1072 #[rstest]
1073 #[case(AxCandleWidth::Seconds1, "\"1s\"")]
1074 #[case(AxCandleWidth::Minutes1, "\"1m\"")]
1075 #[case(AxCandleWidth::Minutes5, "\"5m\"")]
1076 #[case(AxCandleWidth::Hours1, "\"1h\"")]
1077 #[case(AxCandleWidth::Days1, "\"1d\"")]
1078 fn test_candle_width_serialization(#[case] width: AxCandleWidth, #[case] expected: &str) {
1079 let json = serde_json::to_string(&width).unwrap();
1080 assert_eq!(json, expected);
1081
1082 let parsed: AxCandleWidth = serde_json::from_str(&json).unwrap();
1083 assert_eq!(parsed, width);
1084 }
1085
1086 #[rstest]
1087 #[case(AxMdWsMessageType::Heartbeat, "\"h\"")]
1088 #[case(AxMdWsMessageType::Ticker, "\"s\"")]
1089 #[case(AxMdWsMessageType::Trade, "\"t\"")]
1090 #[case(AxMdWsMessageType::Candle, "\"c\"")]
1091 #[case(AxMdWsMessageType::BookLevel1, "\"1\"")]
1092 #[case(AxMdWsMessageType::BookLevel2, "\"2\"")]
1093 #[case(AxMdWsMessageType::BookLevel3, "\"3\"")]
1094 fn test_md_ws_message_type_serialization(
1095 #[case] msg_type: AxMdWsMessageType,
1096 #[case] expected: &str,
1097 ) {
1098 let json = serde_json::to_string(&msg_type).unwrap();
1099 assert_eq!(json, expected);
1100
1101 let parsed: AxMdWsMessageType = serde_json::from_str(&json).unwrap();
1102 assert_eq!(parsed, msg_type);
1103 }
1104
1105 #[rstest]
1106 #[case(AxOrderWsMessageType::Heartbeat, "\"h\"")]
1107 #[case(AxOrderWsMessageType::OrderAcknowledged, "\"n\"")]
1108 #[case(AxOrderWsMessageType::OrderCanceled, "\"c\"")]
1109 #[case(AxOrderWsMessageType::OrderFilled, "\"f\"")]
1110 #[case(AxOrderWsMessageType::OrderPartiallyFilled, "\"p\"")]
1111 fn test_order_ws_message_type_serialization(
1112 #[case] msg_type: AxOrderWsMessageType,
1113 #[case] expected: &str,
1114 ) {
1115 let json = serde_json::to_string(&msg_type).unwrap();
1116 assert_eq!(json, expected);
1117
1118 let parsed: AxOrderWsMessageType = serde_json::from_str(&json).unwrap();
1119 assert_eq!(parsed, msg_type);
1120 }
1121
1122 #[rstest]
1123 #[case(AxMdRequestType::Subscribe, "\"subscribe\"")]
1124 #[case(AxMdRequestType::Unsubscribe, "\"unsubscribe\"")]
1125 #[case(AxMdRequestType::SubscribeCandles, "\"subscribe_candles\"")]
1126 #[case(AxMdRequestType::UnsubscribeCandles, "\"unsubscribe_candles\"")]
1127 fn test_md_request_type_serialization(
1128 #[case] request_type: AxMdRequestType,
1129 #[case] expected: &str,
1130 ) {
1131 let json = serde_json::to_string(&request_type).unwrap();
1132 assert_eq!(json, expected);
1133
1134 let parsed: AxMdRequestType = serde_json::from_str(&json).unwrap();
1135 assert_eq!(parsed, request_type);
1136 }
1137
1138 #[rstest]
1139 #[case(AxOrderRequestType::PlaceOrder, "\"p\"")]
1140 #[case(AxOrderRequestType::CancelOrder, "\"x\"")]
1141 #[case(AxOrderRequestType::GetOpenOrders, "\"o\"")]
1142 fn test_order_request_type_serialization(
1143 #[case] request_type: AxOrderRequestType,
1144 #[case] expected: &str,
1145 ) {
1146 let json = serde_json::to_string(&request_type).unwrap();
1147 assert_eq!(json, expected);
1148
1149 let parsed: AxOrderRequestType = serde_json::from_str(&json).unwrap();
1150 assert_eq!(parsed, request_type);
1151 }
1152
1153 #[rstest]
1154 #[case("\"fx\"", AxCategory::Fx)]
1155 #[case("\"equities\"", AxCategory::Equities)]
1156 #[case("\"metals\"", AxCategory::Metals)]
1157 #[case("\"energy\"", AxCategory::Energy)]
1158 #[case("\"energy_etfs\"", AxCategory::EnergyEtfs)]
1159 #[case("\"treasuries\"", AxCategory::Treasuries)]
1160 #[case("\"compute\"", AxCategory::Compute)]
1161 #[case("\"crypto\"", AxCategory::Crypto)]
1162 #[case("\"something_new\"", AxCategory::Unknown)]
1163 fn test_category_deserialization(#[case] json: &str, #[case] expected: AxCategory) {
1164 let parsed: AxCategory = serde_json::from_str(json).unwrap();
1165 assert_eq!(parsed, expected);
1166 }
1167
1168 #[rstest]
1169 #[case(AxCategory::Fx, AssetClass::FX)]
1170 #[case(AxCategory::Equities, AssetClass::Equity)]
1171 #[case(AxCategory::EnergyEtfs, AssetClass::Equity)]
1172 #[case(AxCategory::Metals, AssetClass::Commodity)]
1173 #[case(AxCategory::Energy, AssetClass::Commodity)]
1174 #[case(AxCategory::Crypto, AssetClass::Cryptocurrency)]
1175 #[case(AxCategory::Treasuries, AssetClass::Debt)]
1176 #[case(AxCategory::Compute, AssetClass::Alternative)]
1177 #[case(AxCategory::Unknown, AssetClass::Alternative)]
1178 fn test_category_asset_class_mapping(
1179 #[case] category: AxCategory,
1180 #[case] expected: AssetClass,
1181 ) {
1182 assert_eq!(AssetClass::from(category), expected);
1183 }
1184
1185 #[rstest]
1186 fn test_future_order_state_is_preserved_but_not_mapped() {
1187 let value = serde_json::Value::String("FUTURE_STATE".into());
1188 let status: AxOrderStatus = serde_json::from_value(value).unwrap();
1189 assert_eq!(status, AxOrderStatus::Unknown);
1190 assert_eq!(
1191 OrderStatus::try_from(status).unwrap_err().to_string(),
1192 "Unmapped AX order status"
1193 );
1194 }
1195
1196 #[rstest]
1197 fn test_future_funding_classifications() {
1198 let value = serde_json::Value::String("future_value".into());
1199 assert_eq!(
1200 serde_json::from_value::<AxFundingVariant>(value.clone()).unwrap(),
1201 AxFundingVariant::Unknown
1202 );
1203 assert_eq!(
1204 serde_json::from_value::<AxFundingSlotStatus>(value).unwrap(),
1205 AxFundingSlotStatus::Unknown
1206 );
1207 }
1208
1209 #[rstest]
1210 fn test_energy_etfs_category_text_matches_wire_name() {
1211 assert_eq!(AxCategory::EnergyEtfs.to_string(), "energy_etfs");
1212 assert_eq!(
1213 "energy_etfs".parse::<AxCategory>().unwrap(),
1214 AxCategory::EnergyEtfs
1215 );
1216 }
1217
1218 #[rstest]
1219 #[case("CandleWidth", enum_wire_roundtrip::<AxCandleWidth>)]
1220 #[case("EstimatedFundingRateStatus", enum_wire_roundtrip::<crate::http::models::AxEstimatedFundingStatus>)]
1221 #[case("FundingSlotStatus", enum_wire_roundtrip::<AxFundingSlotStatus>)]
1222 #[case("FundingVariant", enum_wire_roundtrip::<AxFundingVariant>)]
1223 #[case("InstrumentCategory", enum_wire_roundtrip::<AxCategory>)]
1224 #[case("InstrumentState", enum_wire_roundtrip::<AxInstrumentState>)]
1225 #[case("Side", enum_wire_roundtrip::<AxOrderSide>)]
1226 #[case("OrderRejectReason", enum_wire_roundtrip::<crate::http::models::AxOrderRejectReason>)]
1227 #[case("OrderState", enum_wire_roundtrip::<AxOrderStatus>)]
1228 #[case("RepriceBehavior", enum_wire_roundtrip::<crate::http::models::AxRepriceBehavior>)]
1229 #[case("TimeInForce", enum_wire_roundtrip::<AxTimeInForce>)]
1230 #[case("Environment", enum_wire_roundtrip::<AxEnvironment>)]
1231 #[case("MarketDataLevel", enum_wire_roundtrip::<AxMarketDataLevel>)]
1232 #[case("MdRequestType", enum_wire_roundtrip::<AxMdRequestType>)]
1233 #[case("OrderRequestType", enum_wire_roundtrip::<AxOrderRequestType>)]
1234 #[case("MdWsMessageType", enum_wire_roundtrip::<AxMdWsMessageType>)]
1235 #[case("OrderWsMessageType", enum_wire_roundtrip::<AxOrderWsMessageType>)]
1236 #[case("CancelReason", enum_wire_roundtrip::<AxCancelReason>)]
1237 #[case("CancelRejectionReason", enum_wire_roundtrip::<AxCancelRejectionReason>)]
1238 fn test_documented_enum_values(
1239 #[case] name: &str,
1240 #[case] roundtrip: fn(serde_json::Value) -> serde_json::Value,
1241 ) {
1242 let fixtures: serde_json::Value =
1243 serde_json::from_str(include_str!("../../test_data/wire_enum_values.json")).unwrap();
1244
1245 for value in fixtures[name].as_array().unwrap() {
1246 assert_eq!(roundtrip(value.clone()), *value, "{name}: {value}");
1247 }
1248 }
1249
1250 fn enum_wire_roundtrip<T: serde::de::DeserializeOwned + Serialize>(
1251 value: serde_json::Value,
1252 ) -> serde_json::Value {
1253 serde_json::to_value(serde_json::from_value::<T>(value).unwrap()).unwrap()
1254 }
1255 #[rstest]
1256 #[case(AxOrderStatus::Pending, OrderStatus::Submitted)]
1257 #[case(AxOrderStatus::Accepted, OrderStatus::Accepted)]
1258 #[case(AxOrderStatus::PartiallyFilled, OrderStatus::PartiallyFilled)]
1259 #[case(AxOrderStatus::Filled, OrderStatus::Filled)]
1260 #[case(AxOrderStatus::Canceled, OrderStatus::Canceled)]
1261 #[case(AxOrderStatus::Rejected, OrderStatus::Rejected)]
1262 #[case(AxOrderStatus::Expired, OrderStatus::Expired)]
1263 #[case(AxOrderStatus::Replaced, OrderStatus::Canceled)]
1264 #[case(AxOrderStatus::DoneForDay, OrderStatus::Expired)]
1265 fn test_documented_order_status_mapping(
1266 #[case] wire: AxOrderStatus,
1267 #[case] expected: OrderStatus,
1268 ) {
1269 assert_eq!(OrderStatus::try_from(wire).unwrap(), expected);
1270 }
1271 #[rstest]
1272 fn test_future_time_in_force_is_not_mapped() {
1273 let value = serde_json::Value::String("FUTURE_TIF".into());
1274 let tif: AxTimeInForce = serde_json::from_value(value).unwrap();
1275 assert_eq!(tif, AxTimeInForce::Unknown);
1276 assert_eq!(
1277 TimeInForce::try_from(tif).unwrap_err().to_string(),
1278 "Unmapped AX time in force"
1279 );
1280 }
1281}