Skip to main content

nautilus_architect_ax/common/
enums.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Enumerations that model Ax string enums across HTTP and WebSocket payloads.
17
18use 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/// AX Exchange API environment.
34#[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    /// Sandbox/test environment.
70    #[default]
71    Sandbox,
72    /// Production/live environment.
73    Production,
74}
75
76impl AxEnvironment {
77    /// Returns the HTTP API base URL for this environment.
78    #[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    /// Returns the Orders API base URL for this environment.
87    #[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    /// Returns the market data WebSocket URL for this environment.
96    #[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    /// Returns the orders WebSocket URL for this environment.
105    #[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/// Instrument state as returned by the AX Exchange API.
115///
116/// # References
117/// - <https://docs.architect.exchange/api-reference/symbols-instruments/get-instruments>
118#[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    /// Instrument is in pre-open state.
148    PreOpen,
149    /// Instrument is open for trading.
150    Open,
151    /// Instrument trading is closed.
152    Closed,
153    /// Instrument trading is closed and frozen.
154    ClosedFrozen,
155    /// Instrument trading is halted.
156    Halted,
157    /// Instrument is in a match-and-close auction.
158    MatchAndCloseAuction,
159    /// Instrument trading is suspended.
160    Suspended,
161    /// Instrument has been delisted.
162    Delisted,
163    /// Instrument state is unknown.
164    #[serde(other)]
165    Unknown,
166}
167
168impl AxInstrumentState {
169    /// Returns whether the instrument is in a tradeable state.
170    #[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/// Instrument category as returned by the AX Exchange API.
193///
194/// Unrecognized values map to `Unknown`.
195///
196/// # References
197/// - <https://docs.architect.exchange/api-reference/symbols-instruments/get-instruments>
198#[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 = "lowercase")]
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/// Order side for trading operations.
241///
242/// # References
243/// - <https://docs.architect.exchange/api-reference/order-management/place-order>
244#[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    /// Buy order.
272    #[serde(rename = "B")]
273    #[strum(serialize = "B")]
274    Buy,
275    /// Sell order.
276    #[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/// How a perpetual symbol's funding accrues over a trading day.
318///
319/// # References
320/// - <https://docs.architect.exchange/api-reference/marketdata/get-funding-slots>
321#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
322#[serde(rename_all = "snake_case")]
323pub enum AxFundingVariant {
324    /// A single settlement at the trading-day close.
325    DailyClose,
326    /// A fixed number of intraday slots, each charging its share of the day's TWAP premium.
327    IntradayTwap,
328}
329
330/// Status of one funding slot within a `GET /funding-slots` trading day.
331///
332/// # References
333/// - <https://docs.architect.exchange/api-reference/marketdata/get-funding-slots>
334#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
335#[serde(rename_all = "snake_case")]
336pub enum AxFundingSlotStatus {
337    /// Slot funding has settled.
338    Realized,
339    /// Slot funding is forecast from current mark and underlying TWAPs.
340    Projected,
341    /// Slot did not settle (for example a holiday or suspension); see the slot `reason`.
342    Skipped,
343    /// Slot is scheduled but not yet realized or projected.
344    Pending,
345}
346
347/// Order status as returned by the AX Exchange API.
348///
349/// # References
350/// - <https://docs.architect.exchange/api-reference/order-management/get-open-orders>
351#[derive(
352    Clone,
353    Copy,
354    Debug,
355    Display,
356    Eq,
357    PartialEq,
358    Hash,
359    AsRefStr,
360    EnumIter,
361    EnumString,
362    Serialize,
363    Deserialize,
364)]
365#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
366#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
367#[cfg_attr(
368    feature = "python",
369    pyo3::pyclass(
370        eq,
371        eq_int,
372        frozen,
373        hash,
374        module = "nautilus_trader.adapters.architect_ax",
375        from_py_object,
376        rename_all = "SCREAMING_SNAKE_CASE",
377    )
378)]
379pub enum AxOrderStatus {
380    /// Order is pending submission.
381    Pending,
382    /// Order has been accepted by the exchange (OPEN state).
383    Accepted,
384    /// Order has been partially filled.
385    PartiallyFilled,
386    /// Order has been completely filled.
387    Filled,
388    /// Order cancellation is in progress.
389    Canceling,
390    /// Order has been canceled.
391    Canceled,
392    /// Order has been rejected.
393    Rejected,
394    /// Order has expired.
395    Expired,
396    /// Order has been replaced.
397    Replaced,
398    /// Order is done for the day.
399    DoneForDay,
400    /// Order is no longer on the orderbook (terminal state).
401    Out,
402    /// Order was reconciled out asynchronously.
403    ReconciledOut,
404    /// Order is in a stale state (expected transitions not occurring).
405    Stale,
406    /// Order status is unknown.
407    Unknown,
408}
409
410impl From<AxOrderStatus> for OrderStatus {
411    fn from(status: AxOrderStatus) -> Self {
412        match status {
413            AxOrderStatus::Pending => Self::Submitted,
414            AxOrderStatus::Accepted => Self::Accepted,
415            AxOrderStatus::PartiallyFilled => Self::PartiallyFilled,
416            AxOrderStatus::Filled => Self::Filled,
417            AxOrderStatus::Canceling => Self::PendingCancel,
418            AxOrderStatus::Canceled => Self::Canceled,
419            AxOrderStatus::Rejected => Self::Rejected,
420            AxOrderStatus::Expired => Self::Expired,
421            AxOrderStatus::Replaced => Self::Accepted,
422            AxOrderStatus::DoneForDay => Self::Canceled,
423            AxOrderStatus::Out => Self::Canceled,
424            AxOrderStatus::ReconciledOut => Self::Canceled,
425            AxOrderStatus::Stale => Self::Accepted,
426            AxOrderStatus::Unknown => Self::Initialized,
427        }
428    }
429}
430
431/// Time in force for order validity.
432///
433/// # References
434/// - <https://docs.architect.exchange/api-reference/order-management/place-order>
435#[derive(
436    Clone,
437    Copy,
438    Debug,
439    Display,
440    Eq,
441    PartialEq,
442    Hash,
443    AsRefStr,
444    EnumIter,
445    EnumString,
446    Serialize,
447    Deserialize,
448)]
449#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
450#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
451#[cfg_attr(
452    feature = "python",
453    pyo3::pyclass(
454        eq,
455        eq_int,
456        frozen,
457        hash,
458        module = "nautilus_trader.adapters.architect_ax",
459        from_py_object,
460        rename_all = "SCREAMING_SNAKE_CASE",
461    )
462)]
463pub enum AxTimeInForce {
464    /// Good-Till-Canceled: order remains active until filled or canceled.
465    Gtc,
466    /// Good-Till-Date: order remains active until specified datetime.
467    Gtd,
468    /// Day order: valid until end of trading day.
469    Day,
470    /// Immediate-Or-Cancel: fill immediately or cancel unfilled portion.
471    Ioc,
472    /// Fill-Or-Kill: execute entire order immediately or cancel.
473    Fok,
474    /// At-the-Open: execute at market opening or expire.
475    Ato,
476    /// At-the-Close: execute at market close or expire.
477    Atc,
478}
479
480impl From<AxTimeInForce> for TimeInForce {
481    fn from(tif: AxTimeInForce) -> Self {
482        match tif {
483            AxTimeInForce::Gtc => Self::Gtc,
484            AxTimeInForce::Gtd => Self::Gtd,
485            AxTimeInForce::Day => Self::Day,
486            AxTimeInForce::Ioc => Self::Ioc,
487            AxTimeInForce::Fok => Self::Fok,
488            AxTimeInForce::Ato => Self::AtTheOpen,
489            AxTimeInForce::Atc => Self::AtTheClose,
490        }
491    }
492}
493
494impl TryFrom<TimeInForce> for AxTimeInForce {
495    type Error = &'static str;
496
497    fn try_from(tif: TimeInForce) -> Result<Self, Self::Error> {
498        match tif {
499            TimeInForce::Gtc => Ok(Self::Gtc),
500            TimeInForce::Gtd => Ok(Self::Gtd),
501            TimeInForce::Day => Ok(Self::Day),
502            TimeInForce::Ioc => Ok(Self::Ioc),
503            TimeInForce::Fok => Ok(Self::Fok),
504            TimeInForce::AtTheOpen => Ok(Self::Ato),
505            TimeInForce::AtTheClose => Ok(Self::Atc),
506        }
507    }
508}
509
510/// Market data subscription level.
511///
512/// The AX API uses `LEVEL_1`, `LEVEL_2`, `LEVEL_3` on the wire (with underscore
513/// before the digit). Serde and strum per-variant renames handle the wire and
514/// string formats correctly, however PyO3's `rename_all` does not insert an
515/// underscore at letter-digit boundaries, so the Python variant names are
516/// `LEVEL1`, `LEVEL2`, `LEVEL3` (without underscore).
517///
518/// # References
519/// - <https://docs.architect.exchange/api-reference/marketdata/md-ws>
520#[derive(
521    Clone,
522    Copy,
523    Debug,
524    Display,
525    Eq,
526    PartialEq,
527    Hash,
528    AsRefStr,
529    EnumIter,
530    EnumString,
531    Serialize,
532    Deserialize,
533)]
534#[strum(ascii_case_insensitive)]
535#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
536#[cfg_attr(
537    feature = "python",
538    pyo3::pyclass(
539        eq,
540        eq_int,
541        frozen,
542        hash,
543        module = "nautilus_trader.adapters.architect_ax",
544        from_py_object,
545        rename_all = "SCREAMING_SNAKE_CASE",
546    )
547)]
548#[cfg_attr(
549    feature = "python",
550    pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.adapters.architect_ax")
551)]
552pub enum AxMarketDataLevel {
553    /// Level 1: best bid/ask only.
554    #[serde(rename = "LEVEL_1")]
555    #[strum(serialize = "LEVEL_1")]
556    Level1,
557    /// Level 2: aggregated price levels.
558    #[serde(rename = "LEVEL_2")]
559    #[strum(serialize = "LEVEL_2")]
560    Level2,
561    /// Level 3: individual order quantities.
562    #[serde(rename = "LEVEL_3")]
563    #[strum(serialize = "LEVEL_3")]
564    Level3,
565    /// Trade prints only.
566    #[serde(rename = "TRADES")]
567    #[strum(serialize = "TRADES")]
568    Trades,
569}
570
571/// Candle/bar width for market data subscriptions.
572///
573/// # References
574/// - <https://docs.architect.exchange/api-reference/marketdata/md-ws>
575#[derive(
576    Clone,
577    Copy,
578    Debug,
579    Display,
580    Eq,
581    PartialEq,
582    Hash,
583    AsRefStr,
584    EnumIter,
585    EnumString,
586    Serialize,
587    Deserialize,
588)]
589pub enum AxCandleWidth {
590    /// 1-second candles.
591    #[serde(rename = "1s")]
592    #[strum(serialize = "1s")]
593    Seconds1,
594    /// 5-second candles.
595    #[serde(rename = "5s")]
596    #[strum(serialize = "5s")]
597    Seconds5,
598    /// 1-minute candles.
599    #[serde(rename = "1m")]
600    #[strum(serialize = "1m")]
601    Minutes1,
602    /// 5-minute candles.
603    #[serde(rename = "5m")]
604    #[strum(serialize = "5m")]
605    Minutes5,
606    /// 15-minute candles.
607    #[serde(rename = "15m")]
608    #[strum(serialize = "15m")]
609    Minutes15,
610    /// 1-hour candles.
611    #[serde(rename = "1h")]
612    #[strum(serialize = "1h")]
613    Hours1,
614    /// 1-day candles.
615    #[serde(rename = "1d")]
616    #[strum(serialize = "1d")]
617    Days1,
618}
619
620impl TryFrom<&BarSpecification> for AxCandleWidth {
621    type Error = anyhow::Error;
622
623    fn try_from(spec: &BarSpecification) -> Result<Self, Self::Error> {
624        let step = spec.step.get();
625        match (step, spec.aggregation) {
626            (1, BarAggregation::Second) => Ok(Self::Seconds1),
627            (5, BarAggregation::Second) => Ok(Self::Seconds5),
628            (1, BarAggregation::Minute) => Ok(Self::Minutes1),
629            (5, BarAggregation::Minute) => Ok(Self::Minutes5),
630            (15, BarAggregation::Minute) => Ok(Self::Minutes15),
631            (1, BarAggregation::Hour) => Ok(Self::Hours1),
632            (1, BarAggregation::Day) => Ok(Self::Days1),
633            _ => anyhow::bail!(
634                "Unsupported bar specification for AX: {step}-{:?}",
635                spec.aggregation,
636            ),
637        }
638    }
639}
640
641/// WebSocket market data request type (client to server).
642///
643/// # References
644/// - <https://docs.architect.exchange/api-reference/marketdata/md-ws>
645#[derive(
646    Clone,
647    Copy,
648    Debug,
649    Display,
650    Eq,
651    PartialEq,
652    Hash,
653    AsRefStr,
654    EnumIter,
655    EnumString,
656    Serialize,
657    Deserialize,
658)]
659#[serde(rename_all = "snake_case")]
660#[strum(serialize_all = "snake_case")]
661pub enum AxMdRequestType {
662    /// Subscribe to market data for a symbol.
663    Subscribe,
664    /// Unsubscribe from market data for a symbol.
665    Unsubscribe,
666    /// Subscribe to candle data for a symbol.
667    SubscribeCandles,
668    /// Unsubscribe from candle data for a symbol.
669    UnsubscribeCandles,
670}
671
672/// WebSocket order request type (client to server).
673///
674/// # References
675/// - <https://docs.architect.exchange/api-reference/order-management/orders-ws>
676#[derive(
677    Clone,
678    Copy,
679    Debug,
680    Display,
681    Eq,
682    PartialEq,
683    Hash,
684    AsRefStr,
685    EnumIter,
686    EnumString,
687    Serialize,
688    Deserialize,
689)]
690pub enum AxOrderRequestType {
691    /// Place a new order.
692    #[serde(rename = "p")]
693    #[strum(serialize = "p")]
694    PlaceOrder,
695    /// Cancel an existing order.
696    #[serde(rename = "x")]
697    #[strum(serialize = "x")]
698    CancelOrder,
699    /// Get open orders.
700    #[serde(rename = "o")]
701    #[strum(serialize = "o")]
702    GetOpenOrders,
703}
704
705/// WebSocket market data message type (server to client).
706///
707/// # References
708/// - <https://docs.architect.exchange/api-reference/marketdata/md-ws>
709#[derive(
710    Clone,
711    Copy,
712    Debug,
713    Display,
714    Eq,
715    PartialEq,
716    Hash,
717    AsRefStr,
718    EnumIter,
719    EnumString,
720    Serialize,
721    Deserialize,
722)]
723#[cfg_attr(
724    feature = "python",
725    pyo3::pyclass(
726        eq,
727        eq_int,
728        frozen,
729        hash,
730        module = "nautilus_trader.adapters.architect_ax",
731        from_py_object,
732        rename_all = "SCREAMING_SNAKE_CASE",
733    )
734)]
735pub enum AxMdWsMessageType {
736    /// Heartbeat event.
737    #[serde(rename = "h")]
738    #[strum(serialize = "h")]
739    Heartbeat,
740    /// Ticker statistics update.
741    #[serde(rename = "s")]
742    #[strum(serialize = "s")]
743    Ticker,
744    /// Trade event.
745    #[serde(rename = "t")]
746    #[strum(serialize = "t")]
747    Trade,
748    /// Candle/OHLCV update.
749    #[serde(rename = "c")]
750    #[strum(serialize = "c")]
751    Candle,
752    /// Level 1 book update (best bid/ask).
753    #[serde(rename = "1")]
754    #[strum(serialize = "1")]
755    BookLevel1,
756    /// Level 2 book update (aggregated levels).
757    #[serde(rename = "2")]
758    #[strum(serialize = "2")]
759    BookLevel2,
760    /// Level 3 book update (individual orders).
761    #[serde(rename = "3")]
762    #[strum(serialize = "3")]
763    BookLevel3,
764}
765
766/// WebSocket order message type (server to client).
767///
768/// # References
769/// - <https://docs.architect.exchange/api-reference/order-management/orders-ws>
770#[derive(
771    Clone,
772    Copy,
773    Debug,
774    Display,
775    Eq,
776    PartialEq,
777    Hash,
778    AsRefStr,
779    EnumIter,
780    EnumString,
781    Serialize,
782    Deserialize,
783)]
784#[cfg_attr(
785    feature = "python",
786    pyo3::pyclass(
787        eq,
788        eq_int,
789        frozen,
790        hash,
791        module = "nautilus_trader.adapters.architect_ax",
792        from_py_object,
793        rename_all = "SCREAMING_SNAKE_CASE",
794    )
795)]
796pub enum AxOrderWsMessageType {
797    /// Heartbeat event.
798    #[serde(rename = "h")]
799    #[strum(serialize = "h")]
800    Heartbeat,
801    /// Cancel rejected event.
802    #[serde(rename = "e")]
803    #[strum(serialize = "e")]
804    CancelRejected,
805    /// Order acknowledged event.
806    #[serde(rename = "n")]
807    #[strum(serialize = "n")]
808    OrderAcknowledged,
809    /// Order canceled event.
810    #[serde(rename = "c")]
811    #[strum(serialize = "c")]
812    OrderCanceled,
813    /// Order replaced/amended event.
814    #[serde(rename = "r")]
815    #[strum(serialize = "r")]
816    OrderReplaced,
817    /// Order rejected event.
818    #[serde(rename = "j")]
819    #[strum(serialize = "j")]
820    OrderRejected,
821    /// Order expired event.
822    #[serde(rename = "x")]
823    #[strum(serialize = "x")]
824    OrderExpired,
825    /// Order done for day event.
826    #[serde(rename = "d")]
827    #[strum(serialize = "d")]
828    OrderDoneForDay,
829    /// Order partially filled event.
830    #[serde(rename = "p")]
831    #[strum(serialize = "p")]
832    OrderPartiallyFilled,
833    /// Order filled event.
834    #[serde(rename = "f")]
835    #[strum(serialize = "f")]
836    OrderFilled,
837}
838
839/// Reason for order cancellation.
840///
841/// # References
842/// - <https://docs.architect.exchange/api-reference/order-management/orders-ws>
843#[derive(
844    Clone,
845    Copy,
846    Debug,
847    Display,
848    Eq,
849    PartialEq,
850    Hash,
851    AsRefStr,
852    EnumIter,
853    EnumString,
854    Serialize,
855    Deserialize,
856)]
857#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
858#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
859#[cfg_attr(
860    feature = "python",
861    pyo3::pyclass(
862        eq,
863        eq_int,
864        frozen,
865        hash,
866        module = "nautilus_trader.adapters.architect_ax",
867        from_py_object,
868        rename_all = "SCREAMING_SNAKE_CASE",
869    )
870)]
871pub enum AxCancelReason {
872    /// User requested cancellation.
873    UserRequested,
874    /// Unrecognized or empty reason from the server.
875    #[serde(other)]
876    Unknown,
877}
878
879/// Reason for cancel rejection.
880///
881/// # References
882/// - <https://docs.architect.exchange/api-reference/order-management/orders-ws>
883#[derive(
884    Clone,
885    Copy,
886    Debug,
887    Display,
888    Eq,
889    PartialEq,
890    Hash,
891    AsRefStr,
892    EnumIter,
893    EnumString,
894    Serialize,
895    Deserialize,
896)]
897#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
898#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
899#[cfg_attr(
900    feature = "python",
901    pyo3::pyclass(
902        eq,
903        eq_int,
904        frozen,
905        hash,
906        module = "nautilus_trader.adapters.architect_ax",
907        from_py_object,
908        rename_all = "SCREAMING_SNAKE_CASE",
909    )
910)]
911pub enum AxCancelRejectionReason {
912    /// Order not found or already canceled.
913    OrderNotFound,
914    /// Unrecognized reason from the server.
915    #[serde(other)]
916    Unknown,
917}
918
919#[cfg(test)]
920mod tests {
921    use rstest::rstest;
922
923    use super::*;
924
925    #[rstest]
926    #[case(AxInstrumentState::Open, "\"OPEN\"")]
927    #[case(AxInstrumentState::PreOpen, "\"PRE_OPEN\"")]
928    #[case(AxInstrumentState::Closed, "\"CLOSED\"")]
929    #[case(AxInstrumentState::ClosedFrozen, "\"CLOSED_FROZEN\"")]
930    #[case(AxInstrumentState::Halted, "\"HALTED\"")]
931    #[case(AxInstrumentState::MatchAndCloseAuction, "\"MATCH_AND_CLOSE_AUCTION\"")]
932    #[case(AxInstrumentState::Suspended, "\"SUSPENDED\"")]
933    #[case(AxInstrumentState::Delisted, "\"DELISTED\"")]
934    fn test_instrument_state_serialization(
935        #[case] state: AxInstrumentState,
936        #[case] expected: &str,
937    ) {
938        let json = serde_json::to_string(&state).unwrap();
939        assert_eq!(json, expected);
940
941        let parsed: AxInstrumentState = serde_json::from_str(&json).unwrap();
942        assert_eq!(parsed, state);
943    }
944
945    #[rstest]
946    fn test_instrument_state_unknown_string_deserializes_as_unknown() {
947        let parsed: AxInstrumentState = serde_json::from_str("\"SOME_FUTURE_STATE\"").unwrap();
948        assert_eq!(parsed, AxInstrumentState::Unknown);
949    }
950
951    #[rstest]
952    #[case(AxInstrumentState::PreOpen, true)]
953    #[case(AxInstrumentState::Open, true)]
954    #[case(AxInstrumentState::Closed, false)]
955    #[case(AxInstrumentState::ClosedFrozen, false)]
956    #[case(AxInstrumentState::Halted, false)]
957    #[case(AxInstrumentState::MatchAndCloseAuction, false)]
958    #[case(AxInstrumentState::Suspended, false)]
959    #[case(AxInstrumentState::Delisted, false)]
960    #[case(AxInstrumentState::Unknown, false)]
961    fn test_instrument_state_is_tradeable(
962        #[case] state: AxInstrumentState,
963        #[case] expected: bool,
964    ) {
965        assert_eq!(state.is_tradeable(), expected);
966    }
967
968    #[rstest]
969    #[case(AxInstrumentState::PreOpen, MarketStatusAction::PreOpen)]
970    #[case(AxInstrumentState::Open, MarketStatusAction::Trading)]
971    #[case(AxInstrumentState::Closed, MarketStatusAction::Close)]
972    #[case(AxInstrumentState::ClosedFrozen, MarketStatusAction::Close)]
973    #[case(AxInstrumentState::Halted, MarketStatusAction::Halt)]
974    #[case(AxInstrumentState::MatchAndCloseAuction, MarketStatusAction::Cross)]
975    #[case(AxInstrumentState::Suspended, MarketStatusAction::Suspend)]
976    #[case(
977        AxInstrumentState::Delisted,
978        MarketStatusAction::NotAvailableForTrading
979    )]
980    #[case(AxInstrumentState::Unknown, MarketStatusAction::NotAvailableForTrading)]
981    fn test_instrument_state_to_market_status_action(
982        #[case] state: AxInstrumentState,
983        #[case] expected: MarketStatusAction,
984    ) {
985        assert_eq!(MarketStatusAction::from(state), expected);
986    }
987
988    #[rstest]
989    #[case(AxOrderSide::Buy, "\"B\"")]
990    #[case(AxOrderSide::Sell, "\"S\"")]
991    fn test_order_side_serialization(#[case] side: AxOrderSide, #[case] expected: &str) {
992        let json = serde_json::to_string(&side).unwrap();
993        assert_eq!(json, expected);
994
995        let parsed: AxOrderSide = serde_json::from_str(&json).unwrap();
996        assert_eq!(parsed, side);
997    }
998
999    #[rstest]
1000    #[case("\"Buy\"")]
1001    #[case("\"Sell\"")]
1002    fn test_order_side_rejects_long_form(#[case] json: &str) {
1003        let error = serde_json::from_str::<AxOrderSide>(json).unwrap_err();
1004        assert_eq!(error.classify(), serde_json::error::Category::Data);
1005    }
1006
1007    #[rstest]
1008    #[case(AxOrderStatus::Pending, "\"PENDING\"")]
1009    #[case(AxOrderStatus::Accepted, "\"ACCEPTED\"")]
1010    #[case(AxOrderStatus::PartiallyFilled, "\"PARTIALLY_FILLED\"")]
1011    #[case(AxOrderStatus::Filled, "\"FILLED\"")]
1012    #[case(AxOrderStatus::Canceling, "\"CANCELING\"")]
1013    #[case(AxOrderStatus::Canceled, "\"CANCELED\"")]
1014    #[case(AxOrderStatus::Out, "\"OUT\"")]
1015    #[case(AxOrderStatus::ReconciledOut, "\"RECONCILED_OUT\"")]
1016    #[case(AxOrderStatus::Stale, "\"STALE\"")]
1017    fn test_order_status_serialization(#[case] status: AxOrderStatus, #[case] expected: &str) {
1018        let json = serde_json::to_string(&status).unwrap();
1019        assert_eq!(json, expected);
1020
1021        let parsed: AxOrderStatus = serde_json::from_str(&json).unwrap();
1022        assert_eq!(parsed, status);
1023    }
1024
1025    #[rstest]
1026    #[case(AxTimeInForce::Gtc, "\"GTC\"")]
1027    #[case(AxTimeInForce::Ioc, "\"IOC\"")]
1028    #[case(AxTimeInForce::Day, "\"DAY\"")]
1029    #[case(AxTimeInForce::Gtd, "\"GTD\"")]
1030    #[case(AxTimeInForce::Fok, "\"FOK\"")]
1031    #[case(AxTimeInForce::Ato, "\"ATO\"")]
1032    #[case(AxTimeInForce::Atc, "\"ATC\"")]
1033    fn test_time_in_force_serialization(#[case] tif: AxTimeInForce, #[case] expected: &str) {
1034        let json = serde_json::to_string(&tif).unwrap();
1035        assert_eq!(json, expected);
1036
1037        let parsed: AxTimeInForce = serde_json::from_str(&json).unwrap();
1038        assert_eq!(parsed, tif);
1039    }
1040
1041    #[rstest]
1042    #[case(AxMarketDataLevel::Level1, "\"LEVEL_1\"")]
1043    #[case(AxMarketDataLevel::Level2, "\"LEVEL_2\"")]
1044    #[case(AxMarketDataLevel::Level3, "\"LEVEL_3\"")]
1045    #[case(AxMarketDataLevel::Trades, "\"TRADES\"")]
1046    fn test_market_data_level_serialization(
1047        #[case] level: AxMarketDataLevel,
1048        #[case] expected: &str,
1049    ) {
1050        let json = serde_json::to_string(&level).unwrap();
1051        assert_eq!(json, expected);
1052
1053        let parsed: AxMarketDataLevel = serde_json::from_str(&json).unwrap();
1054        assert_eq!(parsed, level);
1055    }
1056
1057    #[rstest]
1058    #[case(AxCandleWidth::Seconds1, "\"1s\"")]
1059    #[case(AxCandleWidth::Minutes1, "\"1m\"")]
1060    #[case(AxCandleWidth::Minutes5, "\"5m\"")]
1061    #[case(AxCandleWidth::Hours1, "\"1h\"")]
1062    #[case(AxCandleWidth::Days1, "\"1d\"")]
1063    fn test_candle_width_serialization(#[case] width: AxCandleWidth, #[case] expected: &str) {
1064        let json = serde_json::to_string(&width).unwrap();
1065        assert_eq!(json, expected);
1066
1067        let parsed: AxCandleWidth = serde_json::from_str(&json).unwrap();
1068        assert_eq!(parsed, width);
1069    }
1070
1071    #[rstest]
1072    #[case(AxMdWsMessageType::Heartbeat, "\"h\"")]
1073    #[case(AxMdWsMessageType::Ticker, "\"s\"")]
1074    #[case(AxMdWsMessageType::Trade, "\"t\"")]
1075    #[case(AxMdWsMessageType::Candle, "\"c\"")]
1076    #[case(AxMdWsMessageType::BookLevel1, "\"1\"")]
1077    #[case(AxMdWsMessageType::BookLevel2, "\"2\"")]
1078    #[case(AxMdWsMessageType::BookLevel3, "\"3\"")]
1079    fn test_md_ws_message_type_serialization(
1080        #[case] msg_type: AxMdWsMessageType,
1081        #[case] expected: &str,
1082    ) {
1083        let json = serde_json::to_string(&msg_type).unwrap();
1084        assert_eq!(json, expected);
1085
1086        let parsed: AxMdWsMessageType = serde_json::from_str(&json).unwrap();
1087        assert_eq!(parsed, msg_type);
1088    }
1089
1090    #[rstest]
1091    #[case(AxOrderWsMessageType::Heartbeat, "\"h\"")]
1092    #[case(AxOrderWsMessageType::OrderAcknowledged, "\"n\"")]
1093    #[case(AxOrderWsMessageType::OrderCanceled, "\"c\"")]
1094    #[case(AxOrderWsMessageType::OrderFilled, "\"f\"")]
1095    #[case(AxOrderWsMessageType::OrderPartiallyFilled, "\"p\"")]
1096    fn test_order_ws_message_type_serialization(
1097        #[case] msg_type: AxOrderWsMessageType,
1098        #[case] expected: &str,
1099    ) {
1100        let json = serde_json::to_string(&msg_type).unwrap();
1101        assert_eq!(json, expected);
1102
1103        let parsed: AxOrderWsMessageType = serde_json::from_str(&json).unwrap();
1104        assert_eq!(parsed, msg_type);
1105    }
1106
1107    #[rstest]
1108    #[case(AxMdRequestType::Subscribe, "\"subscribe\"")]
1109    #[case(AxMdRequestType::Unsubscribe, "\"unsubscribe\"")]
1110    #[case(AxMdRequestType::SubscribeCandles, "\"subscribe_candles\"")]
1111    #[case(AxMdRequestType::UnsubscribeCandles, "\"unsubscribe_candles\"")]
1112    fn test_md_request_type_serialization(
1113        #[case] request_type: AxMdRequestType,
1114        #[case] expected: &str,
1115    ) {
1116        let json = serde_json::to_string(&request_type).unwrap();
1117        assert_eq!(json, expected);
1118
1119        let parsed: AxMdRequestType = serde_json::from_str(&json).unwrap();
1120        assert_eq!(parsed, request_type);
1121    }
1122
1123    #[rstest]
1124    #[case(AxOrderRequestType::PlaceOrder, "\"p\"")]
1125    #[case(AxOrderRequestType::CancelOrder, "\"x\"")]
1126    #[case(AxOrderRequestType::GetOpenOrders, "\"o\"")]
1127    fn test_order_request_type_serialization(
1128        #[case] request_type: AxOrderRequestType,
1129        #[case] expected: &str,
1130    ) {
1131        let json = serde_json::to_string(&request_type).unwrap();
1132        assert_eq!(json, expected);
1133
1134        let parsed: AxOrderRequestType = serde_json::from_str(&json).unwrap();
1135        assert_eq!(parsed, request_type);
1136    }
1137
1138    #[rstest]
1139    #[case("\"fx\"", AxCategory::Fx)]
1140    #[case("\"equities\"", AxCategory::Equities)]
1141    #[case("\"metals\"", AxCategory::Metals)]
1142    #[case("\"energy\"", AxCategory::Energy)]
1143    #[case("\"energy_etfs\"", AxCategory::EnergyEtfs)]
1144    #[case("\"treasuries\"", AxCategory::Treasuries)]
1145    #[case("\"compute\"", AxCategory::Compute)]
1146    #[case("\"crypto\"", AxCategory::Crypto)]
1147    #[case("\"something_new\"", AxCategory::Unknown)]
1148    fn test_category_deserialization(#[case] json: &str, #[case] expected: AxCategory) {
1149        let parsed: AxCategory = serde_json::from_str(json).unwrap();
1150        assert_eq!(parsed, expected);
1151    }
1152
1153    #[rstest]
1154    #[case(AxCategory::Fx, AssetClass::FX)]
1155    #[case(AxCategory::Equities, AssetClass::Equity)]
1156    #[case(AxCategory::EnergyEtfs, AssetClass::Equity)]
1157    #[case(AxCategory::Metals, AssetClass::Commodity)]
1158    #[case(AxCategory::Energy, AssetClass::Commodity)]
1159    #[case(AxCategory::Crypto, AssetClass::Cryptocurrency)]
1160    #[case(AxCategory::Treasuries, AssetClass::Debt)]
1161    #[case(AxCategory::Compute, AssetClass::Alternative)]
1162    #[case(AxCategory::Unknown, AssetClass::Alternative)]
1163    fn test_category_asset_class_mapping(
1164        #[case] category: AxCategory,
1165        #[case] expected: AssetClass,
1166    ) {
1167        assert_eq!(AssetClass::from(category), expected);
1168    }
1169}