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::enums::{AggressorSide, OrderSide, OrderStatus, PositionSide, TimeInForce};
19use serde::{Deserialize, Serialize};
20use strum::{AsRefStr, Display, EnumIter, EnumString};
21
22use super::consts::{
23    AX_HTTP_SANDBOX_URL, AX_HTTP_URL, AX_ORDERS_SANDBOX_URL, AX_ORDERS_URL, AX_WS_PRIVATE_URL,
24    AX_WS_PUBLIC_URL, AX_WS_SANDBOX_PRIVATE_URL, AX_WS_SANDBOX_PUBLIC_URL,
25};
26
27/// AX Exchange API environment.
28#[derive(
29    Clone,
30    Copy,
31    Debug,
32    Default,
33    Display,
34    Eq,
35    PartialEq,
36    Hash,
37    AsRefStr,
38    EnumIter,
39    EnumString,
40    Serialize,
41    Deserialize,
42)]
43#[cfg_attr(
44    feature = "python",
45    pyo3::pyclass(eq, eq_int, module = "nautilus_trader.core.nautilus_pyo3.architect")
46)]
47pub enum AxEnvironment {
48    /// Sandbox/test environment.
49    #[default]
50    Sandbox,
51    /// Production/live environment.
52    Production,
53}
54
55impl AxEnvironment {
56    /// Returns the HTTP API base URL for this environment.
57    #[must_use]
58    pub const fn http_url(&self) -> &'static str {
59        match self {
60            Self::Sandbox => AX_HTTP_SANDBOX_URL,
61            Self::Production => AX_HTTP_URL,
62        }
63    }
64
65    /// Returns the Orders API base URL for this environment.
66    #[must_use]
67    pub const fn orders_url(&self) -> &'static str {
68        match self {
69            Self::Sandbox => AX_ORDERS_SANDBOX_URL,
70            Self::Production => AX_ORDERS_URL,
71        }
72    }
73
74    /// Returns the market data WebSocket URL for this environment.
75    #[must_use]
76    pub const fn ws_md_url(&self) -> &'static str {
77        match self {
78            Self::Sandbox => AX_WS_SANDBOX_PUBLIC_URL,
79            Self::Production => AX_WS_PUBLIC_URL,
80        }
81    }
82
83    /// Returns the orders WebSocket URL for this environment.
84    #[must_use]
85    pub const fn ws_orders_url(&self) -> &'static str {
86        match self {
87            Self::Sandbox => AX_WS_SANDBOX_PRIVATE_URL,
88            Self::Production => AX_WS_PRIVATE_URL,
89        }
90    }
91}
92
93/// Instrument state as returned by the AX Exchange API.
94///
95/// # References
96/// - <https://docs.sandbox.x.architect.co/api-reference/symbols-instruments/get-instruments>
97#[derive(
98    Clone,
99    Copy,
100    Debug,
101    Display,
102    Eq,
103    PartialEq,
104    Hash,
105    AsRefStr,
106    EnumIter,
107    EnumString,
108    Serialize,
109    Deserialize,
110)]
111#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
112#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
113#[cfg_attr(
114    feature = "python",
115    pyo3::pyclass(eq, eq_int, module = "nautilus_trader.core.nautilus_pyo3.architect")
116)]
117pub enum AxInstrumentState {
118    /// Instrument is in pre-open state.
119    PreOpen,
120    /// Instrument is open for trading.
121    Open,
122    /// Instrument trading is suspended.
123    Suspended,
124    /// Instrument has been delisted.
125    Delisted,
126    /// Instrument state is unknown.
127    Unknown,
128}
129
130/// Order side for trading operations.
131///
132/// # References
133/// - <https://docs.sandbox.x.architect.co/api-reference/order-management/place-order>
134#[derive(
135    Clone,
136    Copy,
137    Debug,
138    Display,
139    Eq,
140    PartialEq,
141    Hash,
142    AsRefStr,
143    EnumIter,
144    EnumString,
145    Serialize,
146    Deserialize,
147)]
148#[cfg_attr(
149    feature = "python",
150    pyo3::pyclass(eq, eq_int, module = "nautilus_trader.core.nautilus_pyo3.architect")
151)]
152pub enum AxOrderSide {
153    /// Buy order.
154    #[serde(rename = "B")]
155    #[strum(serialize = "B")]
156    Buy,
157    /// Sell order.
158    #[serde(rename = "S")]
159    #[strum(serialize = "S")]
160    Sell,
161}
162
163impl From<AxOrderSide> for AggressorSide {
164    fn from(side: AxOrderSide) -> Self {
165        match side {
166            AxOrderSide::Buy => Self::Buyer,
167            AxOrderSide::Sell => Self::Seller,
168        }
169    }
170}
171
172impl From<AxOrderSide> for OrderSide {
173    fn from(side: AxOrderSide) -> Self {
174        match side {
175            AxOrderSide::Buy => Self::Buy,
176            AxOrderSide::Sell => Self::Sell,
177        }
178    }
179}
180
181impl From<AxOrderSide> for PositionSide {
182    fn from(side: AxOrderSide) -> Self {
183        match side {
184            AxOrderSide::Buy => Self::Long,
185            AxOrderSide::Sell => Self::Short,
186        }
187    }
188}
189
190/// Order status as returned by the AX Exchange API.
191///
192/// # References
193/// - <https://docs.sandbox.x.architect.co/api-reference/order-management/get-open-orders>
194#[derive(
195    Clone,
196    Copy,
197    Debug,
198    Display,
199    Eq,
200    PartialEq,
201    Hash,
202    AsRefStr,
203    EnumIter,
204    EnumString,
205    Serialize,
206    Deserialize,
207)]
208#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
209#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
210#[cfg_attr(
211    feature = "python",
212    pyo3::pyclass(eq, eq_int, module = "nautilus_trader.core.nautilus_pyo3.architect")
213)]
214pub enum AxOrderStatus {
215    /// Order is pending submission.
216    Pending,
217    /// Order has been accepted by the exchange.
218    Accepted,
219    /// Order has been partially filled.
220    PartiallyFilled,
221    /// Order has been completely filled.
222    Filled,
223    /// Order has been canceled.
224    Canceled,
225    /// Order has been rejected.
226    Rejected,
227    /// Order has expired.
228    Expired,
229    /// Order has been replaced.
230    Replaced,
231    /// Order is done for the day.
232    DoneForDay,
233    /// Order status is unknown.
234    Unknown,
235}
236
237impl From<AxOrderStatus> for OrderStatus {
238    fn from(status: AxOrderStatus) -> Self {
239        match status {
240            AxOrderStatus::Pending => Self::Submitted,
241            AxOrderStatus::Accepted => Self::Accepted,
242            AxOrderStatus::PartiallyFilled => Self::PartiallyFilled,
243            AxOrderStatus::Filled => Self::Filled,
244            AxOrderStatus::Canceled => Self::Canceled,
245            AxOrderStatus::Rejected => Self::Rejected,
246            AxOrderStatus::Expired => Self::Expired,
247            AxOrderStatus::Replaced => Self::Accepted,
248            AxOrderStatus::DoneForDay => Self::Canceled,
249            AxOrderStatus::Unknown => Self::Initialized,
250        }
251    }
252}
253
254/// Time in force for order validity.
255///
256/// # References
257/// - <https://docs.sandbox.x.architect.co/api-reference/order-management/place-order>
258#[derive(
259    Clone,
260    Copy,
261    Debug,
262    Display,
263    Eq,
264    PartialEq,
265    Hash,
266    AsRefStr,
267    EnumIter,
268    EnumString,
269    Serialize,
270    Deserialize,
271)]
272#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
273#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
274#[cfg_attr(
275    feature = "python",
276    pyo3::pyclass(eq, eq_int, module = "nautilus_trader.core.nautilus_pyo3.architect")
277)]
278pub enum AxTimeInForce {
279    /// Good-Till-Canceled: order remains active until filled or canceled.
280    Gtc,
281    /// Immediate-Or-Cancel: fill immediately or cancel unfilled portion.
282    Ioc,
283    /// Day order: valid until end of trading day.
284    Day,
285}
286
287impl From<AxTimeInForce> for TimeInForce {
288    fn from(tif: AxTimeInForce) -> Self {
289        match tif {
290            AxTimeInForce::Gtc => Self::Gtc,
291            AxTimeInForce::Ioc => Self::Ioc,
292            AxTimeInForce::Day => Self::Day,
293        }
294    }
295}
296
297/// Market data subscription level.
298///
299/// # References
300/// - <https://docs.sandbox.x.architect.co/api-reference/marketdata/md-ws>
301#[derive(
302    Clone,
303    Copy,
304    Debug,
305    Display,
306    Eq,
307    PartialEq,
308    Hash,
309    AsRefStr,
310    EnumIter,
311    EnumString,
312    Serialize,
313    Deserialize,
314)]
315#[cfg_attr(
316    feature = "python",
317    pyo3::pyclass(eq, eq_int, module = "nautilus_trader.core.nautilus_pyo3.architect")
318)]
319pub enum AxMarketDataLevel {
320    /// Level 1: best bid/ask only.
321    #[serde(rename = "LEVEL_1")]
322    #[strum(serialize = "LEVEL_1")]
323    Level1,
324    /// Level 2: aggregated price levels.
325    #[serde(rename = "LEVEL_2")]
326    #[strum(serialize = "LEVEL_2")]
327    Level2,
328    /// Level 3: individual order quantities.
329    #[serde(rename = "LEVEL_3")]
330    #[strum(serialize = "LEVEL_3")]
331    Level3,
332}
333
334/// Candle/bar width for market data subscriptions.
335///
336/// # References
337/// - <https://docs.sandbox.x.architect.co/api-reference/marketdata/md-ws>
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#[cfg_attr(
353    feature = "python",
354    pyo3::pyclass(eq, eq_int, module = "nautilus_trader.core.nautilus_pyo3.architect")
355)]
356pub enum AxCandleWidth {
357    /// 1-second candles.
358    #[serde(rename = "1s")]
359    #[strum(serialize = "1s")]
360    Seconds1,
361    /// 5-second candles.
362    #[serde(rename = "5s")]
363    #[strum(serialize = "5s")]
364    Seconds5,
365    /// 1-minute candles.
366    #[serde(rename = "1m")]
367    #[strum(serialize = "1m")]
368    Minutes1,
369    /// 5-minute candles.
370    #[serde(rename = "5m")]
371    #[strum(serialize = "5m")]
372    Minutes5,
373    /// 15-minute candles.
374    #[serde(rename = "15m")]
375    #[strum(serialize = "15m")]
376    Minutes15,
377    /// 1-hour candles.
378    #[serde(rename = "1h")]
379    #[strum(serialize = "1h")]
380    Hours1,
381    /// 1-day candles.
382    #[serde(rename = "1d")]
383    #[strum(serialize = "1d")]
384    Days1,
385}
386
387/// WebSocket market data message type (server to client).
388///
389/// # References
390/// - <https://docs.sandbox.x.architect.co/api-reference/marketdata/md-ws>
391#[derive(
392    Clone,
393    Copy,
394    Debug,
395    Display,
396    Eq,
397    PartialEq,
398    Hash,
399    AsRefStr,
400    EnumIter,
401    EnumString,
402    Serialize,
403    Deserialize,
404)]
405#[cfg_attr(
406    feature = "python",
407    pyo3::pyclass(eq, eq_int, module = "nautilus_trader.core.nautilus_pyo3.architect")
408)]
409pub enum AxMdWsMessageType {
410    /// Heartbeat event.
411    #[serde(rename = "h")]
412    #[strum(serialize = "h")]
413    Heartbeat,
414    /// Ticker statistics update.
415    #[serde(rename = "s")]
416    #[strum(serialize = "s")]
417    Ticker,
418    /// Trade event.
419    #[serde(rename = "t")]
420    #[strum(serialize = "t")]
421    Trade,
422    /// Candle/OHLCV update.
423    #[serde(rename = "c")]
424    #[strum(serialize = "c")]
425    Candle,
426    /// Level 1 book update (best bid/ask).
427    #[serde(rename = "1")]
428    #[strum(serialize = "1")]
429    BookLevel1,
430    /// Level 2 book update (aggregated levels).
431    #[serde(rename = "2")]
432    #[strum(serialize = "2")]
433    BookLevel2,
434    /// Level 3 book update (individual orders).
435    #[serde(rename = "3")]
436    #[strum(serialize = "3")]
437    BookLevel3,
438}
439
440/// WebSocket order message type (server to client).
441///
442/// # References
443/// - <https://docs.sandbox.x.architect.co/api-reference/order-management/orders-ws>
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#[cfg_attr(
459    feature = "python",
460    pyo3::pyclass(eq, eq_int, module = "nautilus_trader.core.nautilus_pyo3.architect")
461)]
462pub enum AxOrderWsMessageType {
463    /// Heartbeat event.
464    #[serde(rename = "h")]
465    #[strum(serialize = "h")]
466    Heartbeat,
467    /// Cancel rejected event.
468    #[serde(rename = "e")]
469    #[strum(serialize = "e")]
470    CancelRejected,
471    /// Order acknowledged event.
472    #[serde(rename = "n")]
473    #[strum(serialize = "n")]
474    OrderAcknowledged,
475    /// Order canceled event.
476    #[serde(rename = "c")]
477    #[strum(serialize = "c")]
478    OrderCanceled,
479    /// Order replaced/amended event.
480    #[serde(rename = "r")]
481    #[strum(serialize = "r")]
482    OrderReplaced,
483    /// Order rejected event.
484    #[serde(rename = "j")]
485    #[strum(serialize = "j")]
486    OrderRejected,
487    /// Order expired event.
488    #[serde(rename = "x")]
489    #[strum(serialize = "x")]
490    OrderExpired,
491    /// Order done for day event.
492    #[serde(rename = "d")]
493    #[strum(serialize = "d")]
494    OrderDoneForDay,
495    /// Order partially filled event.
496    #[serde(rename = "p")]
497    #[strum(serialize = "p")]
498    OrderPartiallyFilled,
499    /// Order filled event.
500    #[serde(rename = "f")]
501    #[strum(serialize = "f")]
502    OrderFilled,
503}
504
505/// Reason for order cancellation.
506///
507/// # References
508/// - <https://docs.sandbox.x.architect.co/api-reference/order-management/orders-ws>
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(eq, eq_int, module = "nautilus_trader.core.nautilus_pyo3.architect")
528)]
529pub enum AxCancelReason {
530    /// User requested cancellation.
531    UserRequested,
532}
533
534/// Reason for cancel rejection.
535///
536/// # References
537/// - <https://docs.sandbox.x.architect.co/api-reference/order-management/orders-ws>
538#[derive(
539    Clone,
540    Copy,
541    Debug,
542    Display,
543    Eq,
544    PartialEq,
545    Hash,
546    AsRefStr,
547    EnumIter,
548    EnumString,
549    Serialize,
550    Deserialize,
551)]
552#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
553#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
554#[cfg_attr(
555    feature = "python",
556    pyo3::pyclass(eq, eq_int, module = "nautilus_trader.core.nautilus_pyo3.architect")
557)]
558pub enum AxCancelRejectionReason {
559    /// Order not found or already canceled.
560    OrderNotFound,
561}
562
563#[cfg(test)]
564mod tests {
565    use rstest::rstest;
566
567    use super::*;
568
569    #[rstest]
570    #[case(AxInstrumentState::Open, "\"OPEN\"")]
571    #[case(AxInstrumentState::PreOpen, "\"PRE_OPEN\"")]
572    #[case(AxInstrumentState::Suspended, "\"SUSPENDED\"")]
573    #[case(AxInstrumentState::Delisted, "\"DELISTED\"")]
574    fn test_instrument_state_serialization(
575        #[case] state: AxInstrumentState,
576        #[case] expected: &str,
577    ) {
578        let json = serde_json::to_string(&state).unwrap();
579        assert_eq!(json, expected);
580
581        let parsed: AxInstrumentState = serde_json::from_str(&json).unwrap();
582        assert_eq!(parsed, state);
583    }
584
585    #[rstest]
586    #[case(AxOrderSide::Buy, "\"B\"")]
587    #[case(AxOrderSide::Sell, "\"S\"")]
588    fn test_order_side_serialization(#[case] side: AxOrderSide, #[case] expected: &str) {
589        let json = serde_json::to_string(&side).unwrap();
590        assert_eq!(json, expected);
591
592        let parsed: AxOrderSide = serde_json::from_str(&json).unwrap();
593        assert_eq!(parsed, side);
594    }
595
596    #[rstest]
597    #[case(AxOrderStatus::Pending, "\"PENDING\"")]
598    #[case(AxOrderStatus::Accepted, "\"ACCEPTED\"")]
599    #[case(AxOrderStatus::PartiallyFilled, "\"PARTIALLY_FILLED\"")]
600    #[case(AxOrderStatus::Filled, "\"FILLED\"")]
601    #[case(AxOrderStatus::Canceled, "\"CANCELED\"")]
602    fn test_order_status_serialization(#[case] status: AxOrderStatus, #[case] expected: &str) {
603        let json = serde_json::to_string(&status).unwrap();
604        assert_eq!(json, expected);
605
606        let parsed: AxOrderStatus = serde_json::from_str(&json).unwrap();
607        assert_eq!(parsed, status);
608    }
609
610    #[rstest]
611    #[case(AxTimeInForce::Gtc, "\"GTC\"")]
612    #[case(AxTimeInForce::Ioc, "\"IOC\"")]
613    #[case(AxTimeInForce::Day, "\"DAY\"")]
614    fn test_time_in_force_serialization(#[case] tif: AxTimeInForce, #[case] expected: &str) {
615        let json = serde_json::to_string(&tif).unwrap();
616        assert_eq!(json, expected);
617
618        let parsed: AxTimeInForce = serde_json::from_str(&json).unwrap();
619        assert_eq!(parsed, tif);
620    }
621
622    #[rstest]
623    #[case(AxMarketDataLevel::Level1, "\"LEVEL_1\"")]
624    #[case(AxMarketDataLevel::Level2, "\"LEVEL_2\"")]
625    #[case(AxMarketDataLevel::Level3, "\"LEVEL_3\"")]
626    fn test_market_data_level_serialization(
627        #[case] level: AxMarketDataLevel,
628        #[case] expected: &str,
629    ) {
630        let json = serde_json::to_string(&level).unwrap();
631        assert_eq!(json, expected);
632
633        let parsed: AxMarketDataLevel = serde_json::from_str(&json).unwrap();
634        assert_eq!(parsed, level);
635    }
636
637    #[rstest]
638    #[case(AxCandleWidth::Seconds1, "\"1s\"")]
639    #[case(AxCandleWidth::Minutes1, "\"1m\"")]
640    #[case(AxCandleWidth::Minutes5, "\"5m\"")]
641    #[case(AxCandleWidth::Hours1, "\"1h\"")]
642    #[case(AxCandleWidth::Days1, "\"1d\"")]
643    fn test_candle_width_serialization(#[case] width: AxCandleWidth, #[case] expected: &str) {
644        let json = serde_json::to_string(&width).unwrap();
645        assert_eq!(json, expected);
646
647        let parsed: AxCandleWidth = serde_json::from_str(&json).unwrap();
648        assert_eq!(parsed, width);
649    }
650
651    #[rstest]
652    #[case(AxMdWsMessageType::Heartbeat, "\"h\"")]
653    #[case(AxMdWsMessageType::Ticker, "\"s\"")]
654    #[case(AxMdWsMessageType::Trade, "\"t\"")]
655    #[case(AxMdWsMessageType::Candle, "\"c\"")]
656    #[case(AxMdWsMessageType::BookLevel1, "\"1\"")]
657    #[case(AxMdWsMessageType::BookLevel2, "\"2\"")]
658    #[case(AxMdWsMessageType::BookLevel3, "\"3\"")]
659    fn test_md_ws_message_type_serialization(
660        #[case] msg_type: AxMdWsMessageType,
661        #[case] expected: &str,
662    ) {
663        let json = serde_json::to_string(&msg_type).unwrap();
664        assert_eq!(json, expected);
665
666        let parsed: AxMdWsMessageType = serde_json::from_str(&json).unwrap();
667        assert_eq!(parsed, msg_type);
668    }
669
670    #[rstest]
671    #[case(AxOrderWsMessageType::Heartbeat, "\"h\"")]
672    #[case(AxOrderWsMessageType::OrderAcknowledged, "\"n\"")]
673    #[case(AxOrderWsMessageType::OrderCanceled, "\"c\"")]
674    #[case(AxOrderWsMessageType::OrderFilled, "\"f\"")]
675    #[case(AxOrderWsMessageType::OrderPartiallyFilled, "\"p\"")]
676    fn test_order_ws_message_type_serialization(
677        #[case] msg_type: AxOrderWsMessageType,
678        #[case] expected: &str,
679    ) {
680        let json = serde_json::to_string(&msg_type).unwrap();
681        assert_eq!(json, expected);
682
683        let parsed: AxOrderWsMessageType = serde_json::from_str(&json).unwrap();
684        assert_eq!(parsed, msg_type);
685    }
686}