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 = "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/// 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>
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    /// A funding classification added by the venue.
329    #[serde(other)]
330    Unknown,
331}
332
333/// Status of one funding slot within a `GET /funding-slots` trading day.
334///
335/// # References
336/// - <https://docs.architect.exchange/api-reference>
337#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
338#[serde(rename_all = "snake_case")]
339pub enum AxFundingSlotStatus {
340    /// Slot funding has settled.
341    Realized,
342    /// Slot funding is forecast from current mark and underlying TWAPs.
343    Projected,
344    /// Slot did not settle (for example a holiday or suspension); see the slot `reason`.
345    Skipped,
346    /// Slot is scheduled but not yet realized or projected.
347    Pending,
348    /// A funding classification added by the venue.
349    #[serde(other)]
350    Unknown,
351}
352
353/// Order status as returned by the AX Exchange API.
354///
355/// # References
356/// - <https://docs.architect.exchange/api-reference/order-management/get-open-orders>
357#[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    /// Order is pending submission.
387    Pending,
388    /// Order has been accepted by the exchange (OPEN state).
389    Accepted,
390    /// Order has been partially filled.
391    PartiallyFilled,
392    /// Order has been completely filled.
393    Filled,
394    /// Order cancellation is in progress.
395    Canceling,
396    /// Order has been canceled.
397    Canceled,
398    /// Order has been rejected.
399    Rejected,
400    /// Order has expired.
401    Expired,
402    /// Order has been replaced.
403    Replaced,
404    /// Order is done for the day.
405    DoneForDay,
406    /// Order is no longer on the orderbook (terminal state).
407    Out,
408    /// Order was reconciled out asynchronously.
409    ReconciledOut,
410    /// Order is in a stale state (expected transitions not occurring).
411    Stale,
412    /// Order status is unknown.
413    #[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/// Time in force for order validity.
441///
442/// # References
443/// - <https://docs.architect.exchange/api-reference/order-management/place-order>
444#[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    /// Good-Till-Canceled: order remains active until filled or canceled.
474    Gtc,
475    /// Good-Till-Date: order remains active until specified datetime.
476    Gtd,
477    /// Day order: valid until end of trading day.
478    Day,
479    /// Immediate-Or-Cancel: fill immediately or cancel unfilled portion.
480    Ioc,
481    /// Fill-Or-Kill: execute entire order immediately or cancel.
482    Fok,
483    /// At-the-Open: execute at market opening or expire.
484    Ato,
485    /// At-the-Close: execute at market close or expire.
486    Atc,
487    /// Time in force added by the venue.
488    #[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/// Market data subscription level.
526///
527/// The AX API uses `LEVEL_1`, `LEVEL_2`, `LEVEL_3` on the wire (with underscore
528/// before the digit). Serde and strum per-variant renames handle the wire and
529/// string formats correctly, however PyO3's `rename_all` does not insert an
530/// underscore at letter-digit boundaries, so the Python variant names are
531/// `LEVEL1`, `LEVEL2`, `LEVEL3` (without underscore).
532///
533/// # References
534/// - <https://docs.architect.exchange/api-reference/marketdata/md-ws>
535#[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    /// Level 1: best bid/ask only.
569    #[serde(rename = "LEVEL_1")]
570    #[strum(serialize = "LEVEL_1")]
571    Level1,
572    /// Level 2: aggregated price levels.
573    #[serde(rename = "LEVEL_2")]
574    #[strum(serialize = "LEVEL_2")]
575    Level2,
576    /// Level 3: individual order quantities.
577    #[serde(rename = "LEVEL_3")]
578    #[strum(serialize = "LEVEL_3")]
579    Level3,
580    /// Trade prints only.
581    #[serde(rename = "TRADES")]
582    #[strum(serialize = "TRADES")]
583    Trades,
584}
585
586/// Candle/bar width for market data subscriptions.
587///
588/// # References
589/// - <https://docs.architect.exchange/api-reference/marketdata/md-ws>
590#[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    /// 1-second candles.
606    #[serde(rename = "1s")]
607    #[strum(serialize = "1s")]
608    Seconds1,
609    /// 5-second candles.
610    #[serde(rename = "5s")]
611    #[strum(serialize = "5s")]
612    Seconds5,
613    /// 1-minute candles.
614    #[serde(rename = "1m")]
615    #[strum(serialize = "1m")]
616    Minutes1,
617    /// 5-minute candles.
618    #[serde(rename = "5m")]
619    #[strum(serialize = "5m")]
620    Minutes5,
621    /// 15-minute candles.
622    #[serde(rename = "15m")]
623    #[strum(serialize = "15m")]
624    Minutes15,
625    /// 1-hour candles.
626    #[serde(rename = "1h")]
627    #[strum(serialize = "1h")]
628    Hours1,
629    /// 1-day candles.
630    #[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/// WebSocket market data request type (client to server).
657///
658/// # References
659/// - <https://docs.architect.exchange/api-reference/marketdata/md-ws>
660#[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 to market data for a symbol.
678    Subscribe,
679    /// Unsubscribe from market data for a symbol.
680    Unsubscribe,
681    /// Subscribe to candle data for a symbol.
682    SubscribeCandles,
683    /// Unsubscribe from candle data for a symbol.
684    UnsubscribeCandles,
685}
686
687/// WebSocket order request type (client to server).
688///
689/// # References
690/// - <https://docs.architect.exchange/api-reference/order-management/orders-ws>
691#[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    /// Place a new order.
707    #[serde(rename = "p")]
708    #[strum(serialize = "p")]
709    PlaceOrder,
710    /// Cancel an existing order.
711    #[serde(rename = "x")]
712    #[strum(serialize = "x")]
713    CancelOrder,
714    /// Get open orders.
715    #[serde(rename = "o")]
716    #[strum(serialize = "o")]
717    GetOpenOrders,
718}
719
720/// WebSocket market data message type (server to client).
721///
722/// # References
723/// - <https://docs.architect.exchange/api-reference/marketdata/md-ws>
724#[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    /// Heartbeat event.
752    #[serde(rename = "h")]
753    #[strum(serialize = "h")]
754    Heartbeat,
755    /// Ticker statistics update.
756    #[serde(rename = "s")]
757    #[strum(serialize = "s")]
758    Ticker,
759    /// Trade event.
760    #[serde(rename = "t")]
761    #[strum(serialize = "t")]
762    Trade,
763    /// Candle/OHLCV update.
764    #[serde(rename = "c")]
765    #[strum(serialize = "c")]
766    Candle,
767    /// Level 1 book update (best bid/ask).
768    #[serde(rename = "1")]
769    #[strum(serialize = "1")]
770    BookLevel1,
771    /// Level 2 book update (aggregated levels).
772    #[serde(rename = "2")]
773    #[strum(serialize = "2")]
774    BookLevel2,
775    /// Level 3 book update (individual orders).
776    #[serde(rename = "3")]
777    #[strum(serialize = "3")]
778    BookLevel3,
779}
780
781/// WebSocket order message type (server to client).
782///
783/// # References
784/// - <https://docs.architect.exchange/api-reference/order-management/orders-ws>
785#[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    /// Heartbeat event.
813    #[serde(rename = "h")]
814    #[strum(serialize = "h")]
815    Heartbeat,
816    /// Cancel rejected event.
817    #[serde(rename = "e")]
818    #[strum(serialize = "e")]
819    CancelRejected,
820    /// Order acknowledged event.
821    #[serde(rename = "n")]
822    #[strum(serialize = "n")]
823    OrderAcknowledged,
824    /// Order canceled event.
825    #[serde(rename = "c")]
826    #[strum(serialize = "c")]
827    OrderCanceled,
828    /// Order replaced/amended event.
829    #[serde(rename = "r")]
830    #[strum(serialize = "r")]
831    OrderReplaced,
832    /// Order rejected event.
833    #[serde(rename = "j")]
834    #[strum(serialize = "j")]
835    OrderRejected,
836    /// Order expired event.
837    #[serde(rename = "x")]
838    #[strum(serialize = "x")]
839    OrderExpired,
840    /// Order done for day event.
841    #[serde(rename = "d")]
842    #[strum(serialize = "d")]
843    OrderDoneForDay,
844    /// Order partially filled event.
845    #[serde(rename = "p")]
846    #[strum(serialize = "p")]
847    OrderPartiallyFilled,
848    /// Order filled event.
849    #[serde(rename = "f")]
850    #[strum(serialize = "f")]
851    OrderFilled,
852}
853
854/// Reason for order cancellation.
855///
856/// # References
857/// - <https://docs.architect.exchange/api-reference/order-management/orders-ws>
858#[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    /// User requested cancellation.
888    UserRequested,
889    /// Unrecognized or empty reason from the server.
890    #[serde(other)]
891    Unknown,
892}
893
894/// Reason for cancel rejection.
895///
896/// # References
897/// - <https://docs.architect.exchange/api-reference/order-management/orders-ws>
898#[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    /// Order not found or already canceled.
928    OrderNotFound,
929    /// Unrecognized reason from the server.
930    #[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}