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        OrderType, PositionSide, TimeInForce,
23    },
24};
25use serde::{Deserialize, Deserializer, 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.core.nautilus_pyo3.architect",
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.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.core.nautilus_pyo3.architect",
142        from_py_object,
143        rename_all = "SCREAMING_SNAKE_CASE",
144    )
145)]
146#[cfg_attr(
147    feature = "python",
148    pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.architect_ax")
149)]
150pub enum AxInstrumentState {
151    /// Instrument is in pre-open state.
152    PreOpen,
153    /// Instrument is open for trading.
154    Open,
155    /// Instrument trading is closed.
156    Closed,
157    /// Instrument trading is closed and frozen.
158    ClosedFrozen,
159    /// Instrument trading is halted.
160    Halted,
161    /// Instrument is in a match-and-close auction.
162    MatchAndCloseAuction,
163    /// Instrument trading is suspended.
164    Suspended,
165    /// Instrument has been delisted.
166    Delisted,
167    /// Instrument state is unknown.
168    #[serde(other)]
169    Unknown,
170}
171
172impl AxInstrumentState {
173    /// Returns whether the instrument is in a tradeable state.
174    #[must_use]
175    pub fn is_tradeable(self) -> bool {
176        matches!(self, Self::Open | Self::PreOpen)
177    }
178}
179
180impl From<AxInstrumentState> for MarketStatusAction {
181    fn from(state: AxInstrumentState) -> Self {
182        match state {
183            AxInstrumentState::PreOpen => Self::PreOpen,
184            AxInstrumentState::Open => Self::Trading,
185            AxInstrumentState::Closed | AxInstrumentState::ClosedFrozen => Self::Close,
186            AxInstrumentState::Halted => Self::Halt,
187            AxInstrumentState::MatchAndCloseAuction => Self::Cross,
188            AxInstrumentState::Suspended => Self::Suspend,
189            AxInstrumentState::Delisted | AxInstrumentState::Unknown => {
190                Self::NotAvailableForTrading
191            }
192        }
193    }
194}
195
196/// Instrument category as returned by the AX Exchange API.
197///
198/// Deserialization is case-insensitive; unrecognized values map to `Unknown`.
199///
200/// # References
201/// - <https://docs.architect.exchange/api-reference/symbols-instruments/get-instruments>
202#[derive(
203    Clone, Copy, Debug, Display, Eq, PartialEq, Hash, AsRefStr, EnumIter, EnumString, Serialize,
204)]
205#[strum(serialize_all = "lowercase")]
206pub enum AxCategory {
207    Fx,
208    Equities,
209    Metals,
210    Energy,
211    Crypto,
212    Rates,
213    Indexes,
214    Unknown,
215}
216
217impl<'de> Deserialize<'de> for AxCategory {
218    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
219    where
220        D: Deserializer<'de>,
221    {
222        let s = String::deserialize(deserializer)?;
223        Ok(match s.to_ascii_lowercase().as_str() {
224            "fx" => Self::Fx,
225            "equities" => Self::Equities,
226            "metals" => Self::Metals,
227            "energy" => Self::Energy,
228            "crypto" => Self::Crypto,
229            "rates" => Self::Rates,
230            "indexes" => Self::Indexes,
231            _ => Self::Unknown,
232        })
233    }
234}
235
236impl From<AxCategory> for AssetClass {
237    fn from(category: AxCategory) -> Self {
238        match category {
239            AxCategory::Fx => Self::FX,
240            AxCategory::Equities => Self::Equity,
241            AxCategory::Metals | AxCategory::Energy => Self::Commodity,
242            AxCategory::Crypto => Self::Cryptocurrency,
243            AxCategory::Rates => Self::Debt,
244            AxCategory::Indexes => Self::Index,
245            AxCategory::Unknown => Self::Alternative,
246        }
247    }
248}
249
250/// Order side for trading operations.
251///
252/// # References
253/// - <https://docs.architect.exchange/api-reference/order-management/place-order>
254#[derive(
255    Clone,
256    Copy,
257    Debug,
258    Display,
259    Eq,
260    PartialEq,
261    Hash,
262    AsRefStr,
263    EnumIter,
264    EnumString,
265    Serialize,
266    Deserialize,
267)]
268#[cfg_attr(
269    feature = "python",
270    pyo3::pyclass(
271        eq,
272        eq_int,
273        frozen,
274        hash,
275        module = "nautilus_trader.core.nautilus_pyo3.architect",
276        from_py_object,
277        rename_all = "SCREAMING_SNAKE_CASE",
278    )
279)]
280#[cfg_attr(
281    feature = "python",
282    pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.architect_ax")
283)]
284pub enum AxOrderSide {
285    /// Buy order.
286    #[serde(rename = "B", alias = "Buy")]
287    #[strum(serialize = "B")]
288    Buy,
289    /// Sell order.
290    #[serde(rename = "S", alias = "Sell")]
291    #[strum(serialize = "S")]
292    Sell,
293}
294
295impl From<AxOrderSide> for AggressorSide {
296    fn from(side: AxOrderSide) -> Self {
297        match side {
298            AxOrderSide::Buy => Self::Buyer,
299            AxOrderSide::Sell => Self::Seller,
300        }
301    }
302}
303
304impl From<AxOrderSide> for OrderSide {
305    fn from(side: AxOrderSide) -> Self {
306        match side {
307            AxOrderSide::Buy => Self::Buy,
308            AxOrderSide::Sell => Self::Sell,
309        }
310    }
311}
312
313impl From<AxOrderSide> for PositionSide {
314    fn from(side: AxOrderSide) -> Self {
315        match side {
316            AxOrderSide::Buy => Self::Long,
317            AxOrderSide::Sell => Self::Short,
318        }
319    }
320}
321
322impl TryFrom<OrderSide> for AxOrderSide {
323    type Error = &'static str;
324
325    fn try_from(side: OrderSide) -> Result<Self, Self::Error> {
326        match side {
327            OrderSide::Buy => Ok(Self::Buy),
328            OrderSide::Sell => Ok(Self::Sell),
329            _ => Err("Invalid order side for AX"),
330        }
331    }
332}
333
334/// Order status as returned by the AX Exchange API.
335///
336/// # References
337/// - <https://docs.architect.exchange/api-reference/order-management/get-open-orders>
338#[derive(
339    Clone,
340    Copy,
341    Debug,
342    Display,
343    Eq,
344    PartialEq,
345    Hash,
346    AsRefStr,
347    EnumIter,
348    EnumString,
349    Serialize,
350    Deserialize,
351)]
352#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
353#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
354#[cfg_attr(
355    feature = "python",
356    pyo3::pyclass(
357        eq,
358        eq_int,
359        frozen,
360        hash,
361        module = "nautilus_trader.core.nautilus_pyo3.architect",
362        from_py_object,
363        rename_all = "SCREAMING_SNAKE_CASE",
364    )
365)]
366#[cfg_attr(
367    feature = "python",
368    pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.architect_ax")
369)]
370pub enum AxOrderStatus {
371    /// Order is pending submission.
372    Pending,
373    /// Order has been accepted by the exchange (OPEN state).
374    Accepted,
375    /// Order has been partially filled.
376    PartiallyFilled,
377    /// Order has been completely filled.
378    Filled,
379    /// Order cancellation is in progress.
380    Canceling,
381    /// Order has been canceled.
382    Canceled,
383    /// Order has been rejected.
384    Rejected,
385    /// Order has expired.
386    Expired,
387    /// Order has been replaced.
388    Replaced,
389    /// Order is done for the day.
390    DoneForDay,
391    /// Order is no longer on the orderbook (terminal state).
392    Out,
393    /// Order was reconciled out asynchronously.
394    ReconciledOut,
395    /// Order is in a stale state (expected transitions not occurring).
396    Stale,
397    /// Order status is unknown.
398    Unknown,
399}
400
401impl From<AxOrderStatus> for OrderStatus {
402    fn from(status: AxOrderStatus) -> Self {
403        match status {
404            AxOrderStatus::Pending => Self::Submitted,
405            AxOrderStatus::Accepted => Self::Accepted,
406            AxOrderStatus::PartiallyFilled => Self::PartiallyFilled,
407            AxOrderStatus::Filled => Self::Filled,
408            AxOrderStatus::Canceling => Self::PendingCancel,
409            AxOrderStatus::Canceled => Self::Canceled,
410            AxOrderStatus::Rejected => Self::Rejected,
411            AxOrderStatus::Expired => Self::Expired,
412            AxOrderStatus::Replaced => Self::Accepted,
413            AxOrderStatus::DoneForDay => Self::Canceled,
414            AxOrderStatus::Out => Self::Canceled,
415            AxOrderStatus::ReconciledOut => Self::Canceled,
416            AxOrderStatus::Stale => Self::Accepted,
417            AxOrderStatus::Unknown => Self::Initialized,
418        }
419    }
420}
421
422/// Time in force for order validity.
423///
424/// # References
425/// - <https://docs.architect.exchange/api-reference/order-management/place-order>
426#[derive(
427    Clone,
428    Copy,
429    Debug,
430    Display,
431    Eq,
432    PartialEq,
433    Hash,
434    AsRefStr,
435    EnumIter,
436    EnumString,
437    Serialize,
438    Deserialize,
439)]
440#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
441#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
442#[cfg_attr(
443    feature = "python",
444    pyo3::pyclass(
445        eq,
446        eq_int,
447        frozen,
448        hash,
449        module = "nautilus_trader.core.nautilus_pyo3.architect",
450        from_py_object,
451        rename_all = "SCREAMING_SNAKE_CASE",
452    )
453)]
454#[cfg_attr(
455    feature = "python",
456    pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.architect_ax")
457)]
458pub enum AxTimeInForce {
459    /// Good-Till-Canceled: order remains active until filled or canceled.
460    Gtc,
461    /// Good-Till-Date: order remains active until specified datetime.
462    Gtd,
463    /// Day order: valid until end of trading day.
464    Day,
465    /// Immediate-Or-Cancel: fill immediately or cancel unfilled portion.
466    Ioc,
467    /// Fill-Or-Kill: execute entire order immediately or cancel.
468    Fok,
469    /// At-the-Open: execute at market opening or expire.
470    Ato,
471    /// At-the-Close: execute at market close or expire.
472    Atc,
473}
474
475impl From<AxTimeInForce> for TimeInForce {
476    fn from(tif: AxTimeInForce) -> Self {
477        match tif {
478            AxTimeInForce::Gtc => Self::Gtc,
479            AxTimeInForce::Gtd => Self::Gtd,
480            AxTimeInForce::Day => Self::Day,
481            AxTimeInForce::Ioc => Self::Ioc,
482            AxTimeInForce::Fok => Self::Fok,
483            AxTimeInForce::Ato => Self::AtTheOpen,
484            AxTimeInForce::Atc => Self::AtTheClose,
485        }
486    }
487}
488
489impl TryFrom<TimeInForce> for AxTimeInForce {
490    type Error = &'static str;
491
492    fn try_from(tif: TimeInForce) -> Result<Self, Self::Error> {
493        match tif {
494            TimeInForce::Gtc => Ok(Self::Gtc),
495            TimeInForce::Gtd => Ok(Self::Gtd),
496            TimeInForce::Day => Ok(Self::Day),
497            TimeInForce::Ioc => Ok(Self::Ioc),
498            TimeInForce::Fok => Ok(Self::Fok),
499            TimeInForce::AtTheOpen => Ok(Self::Ato),
500            TimeInForce::AtTheClose => Ok(Self::Atc),
501        }
502    }
503}
504
505/// Order type as defined by the AX Exchange API.
506///
507/// # References
508/// - <https://docs.architect.exchange/api-reference/order-management/place-order>
509#[derive(
510    Clone,
511    Copy,
512    Debug,
513    Display,
514    Eq,
515    PartialEq,
516    Hash,
517    AsRefStr,
518    EnumIter,
519    EnumString,
520    Serialize,
521    Deserialize,
522)]
523#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
524#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
525#[cfg_attr(
526    feature = "python",
527    pyo3::pyclass(
528        eq,
529        eq_int,
530        frozen,
531        hash,
532        module = "nautilus_trader.core.nautilus_pyo3.architect",
533        from_py_object,
534        rename_all = "SCREAMING_SNAKE_CASE",
535    )
536)]
537#[cfg_attr(
538    feature = "python",
539    pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.architect_ax")
540)]
541pub enum AxOrderType {
542    /// Market order; execute immediately at best available price.
543    Market,
544    /// Limit order; execute no worse than the limit price specified.
545    Limit,
546    /// Stop-limit order; if the trigger price is breached, place a limit order.
547    StopLossLimit,
548    /// Take-profit order; if the trigger price is breached, place a limit order.
549    /// Note: Not currently implemented by Architect.
550    TakeProfitLimit,
551}
552
553impl From<AxOrderType> for OrderType {
554    fn from(order_type: AxOrderType) -> Self {
555        match order_type {
556            AxOrderType::Market => Self::Market,
557            AxOrderType::Limit => Self::Limit,
558            AxOrderType::StopLossLimit => Self::StopLimit,
559            AxOrderType::TakeProfitLimit => Self::LimitIfTouched,
560        }
561    }
562}
563
564impl TryFrom<OrderType> for AxOrderType {
565    type Error = &'static str;
566
567    fn try_from(order_type: OrderType) -> Result<Self, Self::Error> {
568        match order_type {
569            OrderType::Market => Ok(Self::Market),
570            OrderType::Limit => Ok(Self::Limit),
571            OrderType::StopLimit => Ok(Self::StopLossLimit),
572            OrderType::LimitIfTouched => Ok(Self::TakeProfitLimit),
573            _ => Err("Unsupported order type for AX"),
574        }
575    }
576}
577
578/// Market data subscription level.
579///
580/// The AX API uses `LEVEL_1`, `LEVEL_2`, `LEVEL_3` on the wire (with underscore
581/// before the digit). Serde and strum per-variant renames handle the wire and
582/// string formats correctly, however PyO3's `rename_all` does not insert an
583/// underscore at letter-digit boundaries, so the Python variant names are
584/// `LEVEL1`, `LEVEL2`, `LEVEL3` (without underscore).
585///
586/// # References
587/// - <https://docs.architect.exchange/api-reference/marketdata/md-ws>
588#[derive(
589    Clone,
590    Copy,
591    Debug,
592    Display,
593    Eq,
594    PartialEq,
595    Hash,
596    AsRefStr,
597    EnumIter,
598    EnumString,
599    Serialize,
600    Deserialize,
601)]
602#[strum(ascii_case_insensitive)]
603#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
604#[cfg_attr(
605    feature = "python",
606    pyo3::pyclass(
607        eq,
608        eq_int,
609        frozen,
610        hash,
611        module = "nautilus_trader.core.nautilus_pyo3.architect",
612        from_py_object,
613        rename_all = "SCREAMING_SNAKE_CASE",
614    )
615)]
616#[cfg_attr(
617    feature = "python",
618    pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.architect_ax")
619)]
620pub enum AxMarketDataLevel {
621    /// Level 1: best bid/ask only.
622    #[serde(rename = "LEVEL_1")]
623    #[strum(serialize = "LEVEL_1")]
624    Level1,
625    /// Level 2: aggregated price levels.
626    #[serde(rename = "LEVEL_2")]
627    #[strum(serialize = "LEVEL_2")]
628    Level2,
629    /// Level 3: individual order quantities.
630    #[serde(rename = "LEVEL_3")]
631    #[strum(serialize = "LEVEL_3")]
632    Level3,
633}
634
635/// Candle/bar width for market data subscriptions.
636///
637/// # References
638/// - <https://docs.architect.exchange/api-reference/marketdata/md-ws>
639#[derive(
640    Clone,
641    Copy,
642    Debug,
643    Display,
644    Eq,
645    PartialEq,
646    Hash,
647    AsRefStr,
648    EnumIter,
649    EnumString,
650    Serialize,
651    Deserialize,
652)]
653pub enum AxCandleWidth {
654    /// 1-second candles.
655    #[serde(rename = "1s")]
656    #[strum(serialize = "1s")]
657    Seconds1,
658    /// 5-second candles.
659    #[serde(rename = "5s")]
660    #[strum(serialize = "5s")]
661    Seconds5,
662    /// 1-minute candles.
663    #[serde(rename = "1m")]
664    #[strum(serialize = "1m")]
665    Minutes1,
666    /// 5-minute candles.
667    #[serde(rename = "5m")]
668    #[strum(serialize = "5m")]
669    Minutes5,
670    /// 15-minute candles.
671    #[serde(rename = "15m")]
672    #[strum(serialize = "15m")]
673    Minutes15,
674    /// 1-hour candles.
675    #[serde(rename = "1h")]
676    #[strum(serialize = "1h")]
677    Hours1,
678    /// 1-day candles.
679    #[serde(rename = "1d")]
680    #[strum(serialize = "1d")]
681    Days1,
682}
683
684impl TryFrom<&BarSpecification> for AxCandleWidth {
685    type Error = anyhow::Error;
686
687    fn try_from(spec: &BarSpecification) -> Result<Self, Self::Error> {
688        let step = spec.step.get();
689        match (step, spec.aggregation) {
690            (1, BarAggregation::Second) => Ok(Self::Seconds1),
691            (5, BarAggregation::Second) => Ok(Self::Seconds5),
692            (1, BarAggregation::Minute) => Ok(Self::Minutes1),
693            (5, BarAggregation::Minute) => Ok(Self::Minutes5),
694            (15, BarAggregation::Minute) => Ok(Self::Minutes15),
695            (1, BarAggregation::Hour) => Ok(Self::Hours1),
696            (1, BarAggregation::Day) => Ok(Self::Days1),
697            _ => anyhow::bail!(
698                "Unsupported bar specification for AX: {step}-{:?}",
699                spec.aggregation,
700            ),
701        }
702    }
703}
704
705/// WebSocket market data request type (client to server).
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#[serde(rename_all = "snake_case")]
724#[strum(serialize_all = "snake_case")]
725pub enum AxMdRequestType {
726    /// Subscribe to market data for a symbol.
727    Subscribe,
728    /// Unsubscribe from market data for a symbol.
729    Unsubscribe,
730    /// Subscribe to candle data for a symbol.
731    SubscribeCandles,
732    /// Unsubscribe from candle data for a symbol.
733    UnsubscribeCandles,
734}
735
736/// WebSocket order request type (client to server).
737///
738/// # References
739/// - <https://docs.architect.exchange/api-reference/order-management/orders-ws>
740#[derive(
741    Clone,
742    Copy,
743    Debug,
744    Display,
745    Eq,
746    PartialEq,
747    Hash,
748    AsRefStr,
749    EnumIter,
750    EnumString,
751    Serialize,
752    Deserialize,
753)]
754pub enum AxOrderRequestType {
755    /// Place a new order.
756    #[serde(rename = "p")]
757    #[strum(serialize = "p")]
758    PlaceOrder,
759    /// Cancel an existing order.
760    #[serde(rename = "x")]
761    #[strum(serialize = "x")]
762    CancelOrder,
763    /// Get open orders.
764    #[serde(rename = "o")]
765    #[strum(serialize = "o")]
766    GetOpenOrders,
767}
768
769/// WebSocket market data message type (server to client).
770///
771/// # References
772/// - <https://docs.architect.exchange/api-reference/marketdata/md-ws>
773#[derive(
774    Clone,
775    Copy,
776    Debug,
777    Display,
778    Eq,
779    PartialEq,
780    Hash,
781    AsRefStr,
782    EnumIter,
783    EnumString,
784    Serialize,
785    Deserialize,
786)]
787#[cfg_attr(
788    feature = "python",
789    pyo3::pyclass(
790        eq,
791        eq_int,
792        frozen,
793        hash,
794        module = "nautilus_trader.core.nautilus_pyo3.architect",
795        from_py_object,
796        rename_all = "SCREAMING_SNAKE_CASE",
797    )
798)]
799#[cfg_attr(
800    feature = "python",
801    pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.architect_ax")
802)]
803pub enum AxMdWsMessageType {
804    /// Heartbeat event.
805    #[serde(rename = "h")]
806    #[strum(serialize = "h")]
807    Heartbeat,
808    /// Ticker statistics update.
809    #[serde(rename = "s")]
810    #[strum(serialize = "s")]
811    Ticker,
812    /// Trade event.
813    #[serde(rename = "t")]
814    #[strum(serialize = "t")]
815    Trade,
816    /// Candle/OHLCV update.
817    #[serde(rename = "c")]
818    #[strum(serialize = "c")]
819    Candle,
820    /// Level 1 book update (best bid/ask).
821    #[serde(rename = "1")]
822    #[strum(serialize = "1")]
823    BookLevel1,
824    /// Level 2 book update (aggregated levels).
825    #[serde(rename = "2")]
826    #[strum(serialize = "2")]
827    BookLevel2,
828    /// Level 3 book update (individual orders).
829    #[serde(rename = "3")]
830    #[strum(serialize = "3")]
831    BookLevel3,
832}
833
834/// WebSocket order message type (server to client).
835///
836/// # References
837/// - <https://docs.architect.exchange/api-reference/order-management/orders-ws>
838#[derive(
839    Clone,
840    Copy,
841    Debug,
842    Display,
843    Eq,
844    PartialEq,
845    Hash,
846    AsRefStr,
847    EnumIter,
848    EnumString,
849    Serialize,
850    Deserialize,
851)]
852#[cfg_attr(
853    feature = "python",
854    pyo3::pyclass(
855        eq,
856        eq_int,
857        frozen,
858        hash,
859        module = "nautilus_trader.core.nautilus_pyo3.architect",
860        from_py_object,
861        rename_all = "SCREAMING_SNAKE_CASE",
862    )
863)]
864#[cfg_attr(
865    feature = "python",
866    pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.architect_ax")
867)]
868pub enum AxOrderWsMessageType {
869    /// Heartbeat event.
870    #[serde(rename = "h")]
871    #[strum(serialize = "h")]
872    Heartbeat,
873    /// Cancel rejected event.
874    #[serde(rename = "e")]
875    #[strum(serialize = "e")]
876    CancelRejected,
877    /// Order acknowledged event.
878    #[serde(rename = "n")]
879    #[strum(serialize = "n")]
880    OrderAcknowledged,
881    /// Order canceled event.
882    #[serde(rename = "c")]
883    #[strum(serialize = "c")]
884    OrderCanceled,
885    /// Order replaced/amended event.
886    #[serde(rename = "r")]
887    #[strum(serialize = "r")]
888    OrderReplaced,
889    /// Order rejected event.
890    #[serde(rename = "j")]
891    #[strum(serialize = "j")]
892    OrderRejected,
893    /// Order expired event.
894    #[serde(rename = "x")]
895    #[strum(serialize = "x")]
896    OrderExpired,
897    /// Order done for day event.
898    #[serde(rename = "d")]
899    #[strum(serialize = "d")]
900    OrderDoneForDay,
901    /// Order partially filled event.
902    #[serde(rename = "p")]
903    #[strum(serialize = "p")]
904    OrderPartiallyFilled,
905    /// Order filled event.
906    #[serde(rename = "f")]
907    #[strum(serialize = "f")]
908    OrderFilled,
909}
910
911/// Reason for order cancellation.
912///
913/// # References
914/// - <https://docs.architect.exchange/api-reference/order-management/orders-ws>
915#[derive(
916    Clone,
917    Copy,
918    Debug,
919    Display,
920    Eq,
921    PartialEq,
922    Hash,
923    AsRefStr,
924    EnumIter,
925    EnumString,
926    Serialize,
927    Deserialize,
928)]
929#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
930#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
931#[cfg_attr(
932    feature = "python",
933    pyo3::pyclass(
934        eq,
935        eq_int,
936        frozen,
937        hash,
938        module = "nautilus_trader.core.nautilus_pyo3.architect",
939        from_py_object,
940        rename_all = "SCREAMING_SNAKE_CASE",
941    )
942)]
943#[cfg_attr(
944    feature = "python",
945    pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.architect_ax")
946)]
947pub enum AxCancelReason {
948    /// User requested cancellation.
949    UserRequested,
950    /// Unrecognized or empty reason from the server.
951    #[serde(other)]
952    Unknown,
953}
954
955/// Reason for cancel rejection.
956///
957/// # References
958/// - <https://docs.architect.exchange/api-reference/order-management/orders-ws>
959#[derive(
960    Clone,
961    Copy,
962    Debug,
963    Display,
964    Eq,
965    PartialEq,
966    Hash,
967    AsRefStr,
968    EnumIter,
969    EnumString,
970    Serialize,
971    Deserialize,
972)]
973#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
974#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
975#[cfg_attr(
976    feature = "python",
977    pyo3::pyclass(
978        eq,
979        eq_int,
980        frozen,
981        hash,
982        module = "nautilus_trader.core.nautilus_pyo3.architect",
983        from_py_object,
984        rename_all = "SCREAMING_SNAKE_CASE",
985    )
986)]
987#[cfg_attr(
988    feature = "python",
989    pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.architect_ax")
990)]
991pub enum AxCancelRejectionReason {
992    /// Order not found or already canceled.
993    OrderNotFound,
994    /// Unrecognized reason from the server.
995    #[serde(other)]
996    Unknown,
997}
998
999#[cfg(test)]
1000mod tests {
1001    use rstest::rstest;
1002
1003    use super::*;
1004
1005    #[rstest]
1006    #[case(AxInstrumentState::Open, "\"OPEN\"")]
1007    #[case(AxInstrumentState::PreOpen, "\"PRE_OPEN\"")]
1008    #[case(AxInstrumentState::Closed, "\"CLOSED\"")]
1009    #[case(AxInstrumentState::ClosedFrozen, "\"CLOSED_FROZEN\"")]
1010    #[case(AxInstrumentState::Halted, "\"HALTED\"")]
1011    #[case(AxInstrumentState::MatchAndCloseAuction, "\"MATCH_AND_CLOSE_AUCTION\"")]
1012    #[case(AxInstrumentState::Suspended, "\"SUSPENDED\"")]
1013    #[case(AxInstrumentState::Delisted, "\"DELISTED\"")]
1014    fn test_instrument_state_serialization(
1015        #[case] state: AxInstrumentState,
1016        #[case] expected: &str,
1017    ) {
1018        let json = serde_json::to_string(&state).unwrap();
1019        assert_eq!(json, expected);
1020
1021        let parsed: AxInstrumentState = serde_json::from_str(&json).unwrap();
1022        assert_eq!(parsed, state);
1023    }
1024
1025    #[rstest]
1026    fn test_instrument_state_unknown_string_deserializes_as_unknown() {
1027        let parsed: AxInstrumentState = serde_json::from_str("\"SOME_FUTURE_STATE\"").unwrap();
1028        assert_eq!(parsed, AxInstrumentState::Unknown);
1029    }
1030
1031    #[rstest]
1032    #[case(AxInstrumentState::PreOpen, true)]
1033    #[case(AxInstrumentState::Open, true)]
1034    #[case(AxInstrumentState::Closed, false)]
1035    #[case(AxInstrumentState::ClosedFrozen, false)]
1036    #[case(AxInstrumentState::Halted, false)]
1037    #[case(AxInstrumentState::MatchAndCloseAuction, false)]
1038    #[case(AxInstrumentState::Suspended, false)]
1039    #[case(AxInstrumentState::Delisted, false)]
1040    #[case(AxInstrumentState::Unknown, false)]
1041    fn test_instrument_state_is_tradeable(
1042        #[case] state: AxInstrumentState,
1043        #[case] expected: bool,
1044    ) {
1045        assert_eq!(state.is_tradeable(), expected);
1046    }
1047
1048    #[rstest]
1049    #[case(AxInstrumentState::PreOpen, MarketStatusAction::PreOpen)]
1050    #[case(AxInstrumentState::Open, MarketStatusAction::Trading)]
1051    #[case(AxInstrumentState::Closed, MarketStatusAction::Close)]
1052    #[case(AxInstrumentState::ClosedFrozen, MarketStatusAction::Close)]
1053    #[case(AxInstrumentState::Halted, MarketStatusAction::Halt)]
1054    #[case(AxInstrumentState::MatchAndCloseAuction, MarketStatusAction::Cross)]
1055    #[case(AxInstrumentState::Suspended, MarketStatusAction::Suspend)]
1056    #[case(
1057        AxInstrumentState::Delisted,
1058        MarketStatusAction::NotAvailableForTrading
1059    )]
1060    #[case(AxInstrumentState::Unknown, MarketStatusAction::NotAvailableForTrading)]
1061    fn test_instrument_state_to_market_status_action(
1062        #[case] state: AxInstrumentState,
1063        #[case] expected: MarketStatusAction,
1064    ) {
1065        assert_eq!(MarketStatusAction::from(state), expected);
1066    }
1067
1068    #[rstest]
1069    #[case(AxOrderSide::Buy, "\"B\"")]
1070    #[case(AxOrderSide::Sell, "\"S\"")]
1071    fn test_order_side_serialization(#[case] side: AxOrderSide, #[case] expected: &str) {
1072        let json = serde_json::to_string(&side).unwrap();
1073        assert_eq!(json, expected);
1074
1075        let parsed: AxOrderSide = serde_json::from_str(&json).unwrap();
1076        assert_eq!(parsed, side);
1077    }
1078
1079    #[rstest]
1080    #[case(AxOrderStatus::Pending, "\"PENDING\"")]
1081    #[case(AxOrderStatus::Accepted, "\"ACCEPTED\"")]
1082    #[case(AxOrderStatus::PartiallyFilled, "\"PARTIALLY_FILLED\"")]
1083    #[case(AxOrderStatus::Filled, "\"FILLED\"")]
1084    #[case(AxOrderStatus::Canceling, "\"CANCELING\"")]
1085    #[case(AxOrderStatus::Canceled, "\"CANCELED\"")]
1086    #[case(AxOrderStatus::Out, "\"OUT\"")]
1087    #[case(AxOrderStatus::ReconciledOut, "\"RECONCILED_OUT\"")]
1088    #[case(AxOrderStatus::Stale, "\"STALE\"")]
1089    fn test_order_status_serialization(#[case] status: AxOrderStatus, #[case] expected: &str) {
1090        let json = serde_json::to_string(&status).unwrap();
1091        assert_eq!(json, expected);
1092
1093        let parsed: AxOrderStatus = serde_json::from_str(&json).unwrap();
1094        assert_eq!(parsed, status);
1095    }
1096
1097    #[rstest]
1098    #[case(AxTimeInForce::Gtc, "\"GTC\"")]
1099    #[case(AxTimeInForce::Ioc, "\"IOC\"")]
1100    #[case(AxTimeInForce::Day, "\"DAY\"")]
1101    #[case(AxTimeInForce::Gtd, "\"GTD\"")]
1102    #[case(AxTimeInForce::Fok, "\"FOK\"")]
1103    #[case(AxTimeInForce::Ato, "\"ATO\"")]
1104    #[case(AxTimeInForce::Atc, "\"ATC\"")]
1105    fn test_time_in_force_serialization(#[case] tif: AxTimeInForce, #[case] expected: &str) {
1106        let json = serde_json::to_string(&tif).unwrap();
1107        assert_eq!(json, expected);
1108
1109        let parsed: AxTimeInForce = serde_json::from_str(&json).unwrap();
1110        assert_eq!(parsed, tif);
1111    }
1112
1113    #[rstest]
1114    #[case(AxOrderType::Market, "\"MARKET\"")]
1115    #[case(AxOrderType::Limit, "\"LIMIT\"")]
1116    #[case(AxOrderType::StopLossLimit, "\"STOP_LOSS_LIMIT\"")]
1117    #[case(AxOrderType::TakeProfitLimit, "\"TAKE_PROFIT_LIMIT\"")]
1118    fn test_order_type_serialization(#[case] order_type: AxOrderType, #[case] expected: &str) {
1119        let json = serde_json::to_string(&order_type).unwrap();
1120        assert_eq!(json, expected);
1121
1122        let parsed: AxOrderType = serde_json::from_str(&json).unwrap();
1123        assert_eq!(parsed, order_type);
1124    }
1125
1126    #[rstest]
1127    #[case(AxMarketDataLevel::Level1, "\"LEVEL_1\"")]
1128    #[case(AxMarketDataLevel::Level2, "\"LEVEL_2\"")]
1129    #[case(AxMarketDataLevel::Level3, "\"LEVEL_3\"")]
1130    fn test_market_data_level_serialization(
1131        #[case] level: AxMarketDataLevel,
1132        #[case] expected: &str,
1133    ) {
1134        let json = serde_json::to_string(&level).unwrap();
1135        assert_eq!(json, expected);
1136
1137        let parsed: AxMarketDataLevel = serde_json::from_str(&json).unwrap();
1138        assert_eq!(parsed, level);
1139    }
1140
1141    #[rstest]
1142    #[case(AxCandleWidth::Seconds1, "\"1s\"")]
1143    #[case(AxCandleWidth::Minutes1, "\"1m\"")]
1144    #[case(AxCandleWidth::Minutes5, "\"5m\"")]
1145    #[case(AxCandleWidth::Hours1, "\"1h\"")]
1146    #[case(AxCandleWidth::Days1, "\"1d\"")]
1147    fn test_candle_width_serialization(#[case] width: AxCandleWidth, #[case] expected: &str) {
1148        let json = serde_json::to_string(&width).unwrap();
1149        assert_eq!(json, expected);
1150
1151        let parsed: AxCandleWidth = serde_json::from_str(&json).unwrap();
1152        assert_eq!(parsed, width);
1153    }
1154
1155    #[rstest]
1156    #[case(AxMdWsMessageType::Heartbeat, "\"h\"")]
1157    #[case(AxMdWsMessageType::Ticker, "\"s\"")]
1158    #[case(AxMdWsMessageType::Trade, "\"t\"")]
1159    #[case(AxMdWsMessageType::Candle, "\"c\"")]
1160    #[case(AxMdWsMessageType::BookLevel1, "\"1\"")]
1161    #[case(AxMdWsMessageType::BookLevel2, "\"2\"")]
1162    #[case(AxMdWsMessageType::BookLevel3, "\"3\"")]
1163    fn test_md_ws_message_type_serialization(
1164        #[case] msg_type: AxMdWsMessageType,
1165        #[case] expected: &str,
1166    ) {
1167        let json = serde_json::to_string(&msg_type).unwrap();
1168        assert_eq!(json, expected);
1169
1170        let parsed: AxMdWsMessageType = serde_json::from_str(&json).unwrap();
1171        assert_eq!(parsed, msg_type);
1172    }
1173
1174    #[rstest]
1175    #[case(AxOrderWsMessageType::Heartbeat, "\"h\"")]
1176    #[case(AxOrderWsMessageType::OrderAcknowledged, "\"n\"")]
1177    #[case(AxOrderWsMessageType::OrderCanceled, "\"c\"")]
1178    #[case(AxOrderWsMessageType::OrderFilled, "\"f\"")]
1179    #[case(AxOrderWsMessageType::OrderPartiallyFilled, "\"p\"")]
1180    fn test_order_ws_message_type_serialization(
1181        #[case] msg_type: AxOrderWsMessageType,
1182        #[case] expected: &str,
1183    ) {
1184        let json = serde_json::to_string(&msg_type).unwrap();
1185        assert_eq!(json, expected);
1186
1187        let parsed: AxOrderWsMessageType = serde_json::from_str(&json).unwrap();
1188        assert_eq!(parsed, msg_type);
1189    }
1190
1191    #[rstest]
1192    #[case(AxMdRequestType::Subscribe, "\"subscribe\"")]
1193    #[case(AxMdRequestType::Unsubscribe, "\"unsubscribe\"")]
1194    #[case(AxMdRequestType::SubscribeCandles, "\"subscribe_candles\"")]
1195    #[case(AxMdRequestType::UnsubscribeCandles, "\"unsubscribe_candles\"")]
1196    fn test_md_request_type_serialization(
1197        #[case] request_type: AxMdRequestType,
1198        #[case] expected: &str,
1199    ) {
1200        let json = serde_json::to_string(&request_type).unwrap();
1201        assert_eq!(json, expected);
1202
1203        let parsed: AxMdRequestType = serde_json::from_str(&json).unwrap();
1204        assert_eq!(parsed, request_type);
1205    }
1206
1207    #[rstest]
1208    #[case(AxOrderRequestType::PlaceOrder, "\"p\"")]
1209    #[case(AxOrderRequestType::CancelOrder, "\"x\"")]
1210    #[case(AxOrderRequestType::GetOpenOrders, "\"o\"")]
1211    fn test_order_request_type_serialization(
1212        #[case] request_type: AxOrderRequestType,
1213        #[case] expected: &str,
1214    ) {
1215        let json = serde_json::to_string(&request_type).unwrap();
1216        assert_eq!(json, expected);
1217
1218        let parsed: AxOrderRequestType = serde_json::from_str(&json).unwrap();
1219        assert_eq!(parsed, request_type);
1220    }
1221
1222    #[rstest]
1223    #[case("\"fx\"", AxCategory::Fx)]
1224    #[case("\"FX\"", AxCategory::Fx)]
1225    #[case("\"Fx\"", AxCategory::Fx)]
1226    #[case("\"equities\"", AxCategory::Equities)]
1227    #[case("\"EQUITIES\"", AxCategory::Equities)]
1228    #[case("\"metals\"", AxCategory::Metals)]
1229    #[case("\"Metals\"", AxCategory::Metals)]
1230    #[case("\"energy\"", AxCategory::Energy)]
1231    #[case("\"crypto\"", AxCategory::Crypto)]
1232    #[case("\"rates\"", AxCategory::Rates)]
1233    #[case("\"indexes\"", AxCategory::Indexes)]
1234    #[case("\"something_new\"", AxCategory::Unknown)]
1235    fn test_category_deserialization_case_insensitive(
1236        #[case] json: &str,
1237        #[case] expected: AxCategory,
1238    ) {
1239        let parsed: AxCategory = serde_json::from_str(json).unwrap();
1240        assert_eq!(parsed, expected);
1241    }
1242}