Skip to main content

nautilus_model/
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 for the trading domain model.
17
18use std::{borrow::Cow, fmt::Display, marker::PhantomData, str::FromStr};
19
20use serde::{Deserialize, Deserializer, Serializer};
21use strum::{AsRefStr, Display, EnumIter, EnumString, FromRepr};
22
23use crate::enum_strum_serde;
24
25/// Provides conversion from a `u8` value to an enum type.
26pub trait FromU8 {
27    /// Converts a `u8` value to the implementing type.
28    ///
29    /// Returns `None` if the value is not a valid representation.
30    fn from_u8(value: u8) -> Option<Self>
31    where
32        Self: Sized;
33}
34
35/// Provides conversion from a `u16` value to an enum type.
36pub trait FromU16 {
37    /// Converts a `u16` value to the implementing type.
38    ///
39    /// Returns `None` if the value is not a valid representation.
40    fn from_u16(value: u16) -> Option<Self>
41    where
42        Self: Sized;
43}
44
45/// An account type provided by a trading venue or broker.
46#[repr(C)]
47#[derive(
48    Copy,
49    Clone,
50    Debug,
51    Display,
52    Hash,
53    PartialEq,
54    Eq,
55    PartialOrd,
56    Ord,
57    AsRefStr,
58    FromRepr,
59    EnumIter,
60    EnumString,
61)]
62#[strum(ascii_case_insensitive)]
63#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
64#[cfg_attr(
65    feature = "python",
66    pyo3::pyclass(
67        frozen,
68        eq,
69        eq_int,
70        module = "nautilus_trader.model",
71        from_py_object,
72        rename_all = "SCREAMING_SNAKE_CASE",
73    )
74)]
75#[cfg_attr(
76    feature = "python",
77    pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.model")
78)]
79pub enum AccountType {
80    /// An account with unleveraged cash assets only.
81    Cash = 1,
82    /// An account which facilitates trading on margin, using account assets as collateral.
83    Margin = 2,
84    /// An account specific to betting markets.
85    Betting = 3,
86    /// An account which represents a blockchain wallet,
87    Wallet = 4,
88}
89
90/// An aggregation source for derived data.
91#[repr(C)]
92#[derive(
93    Copy,
94    Clone,
95    Debug,
96    Display,
97    Hash,
98    PartialEq,
99    Eq,
100    PartialOrd,
101    Ord,
102    AsRefStr,
103    FromRepr,
104    EnumIter,
105    EnumString,
106)]
107#[strum(ascii_case_insensitive)]
108#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
109#[cfg_attr(
110    feature = "python",
111    pyo3::pyclass(
112        frozen,
113        eq,
114        eq_int,
115        module = "nautilus_trader.model",
116        from_py_object,
117        rename_all = "SCREAMING_SNAKE_CASE",
118    )
119)]
120#[cfg_attr(
121    feature = "python",
122    pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.model")
123)]
124pub enum AggregationSource {
125    /// The data is externally aggregated (outside the Nautilus system boundary).
126    External = 1,
127    /// The data is internally aggregated (inside the Nautilus system boundary).
128    Internal = 2,
129}
130
131/// The side for the aggressing order of a trade in a market.
132#[repr(C)]
133#[derive(
134    Copy,
135    Clone,
136    Debug,
137    Default,
138    Display,
139    Hash,
140    PartialEq,
141    Eq,
142    PartialOrd,
143    Ord,
144    AsRefStr,
145    FromRepr,
146    EnumIter,
147    EnumString,
148)]
149#[strum(ascii_case_insensitive)]
150#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
151#[cfg_attr(
152    feature = "python",
153    pyo3::pyclass(
154        frozen,
155        eq,
156        eq_int,
157        module = "nautilus_trader.model",
158        from_py_object,
159        rename_all = "SCREAMING_SNAKE_CASE",
160    )
161)]
162#[cfg_attr(
163    feature = "python",
164    pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.model")
165)]
166pub enum AggressorSide {
167    /// There was no specific aggressor for the trade.
168    #[default]
169    NoAggressor = 0,
170    /// The BUY order was the aggressor for the trade.
171    ///
172    /// The deprecated `BUYER` serialization value is still accepted when parsing.
173    #[strum(serialize = "BUYER", to_string = "BUY")]
174    Buy = 1,
175    /// The SELL order was the aggressor for the trade.
176    ///
177    /// The deprecated `SELLER` serialization value is still accepted when parsing.
178    #[strum(serialize = "SELLER", to_string = "SELL")]
179    Sell = 2,
180}
181
182impl FromU8 for AggressorSide {
183    fn from_u8(value: u8) -> Option<Self> {
184        Self::from_repr(usize::from(value))
185    }
186}
187
188/// A broad financial market asset class.
189#[repr(C)]
190#[derive(
191    Copy,
192    Clone,
193    Debug,
194    Display,
195    Hash,
196    PartialEq,
197    Eq,
198    PartialOrd,
199    Ord,
200    AsRefStr,
201    FromRepr,
202    EnumIter,
203    EnumString,
204)]
205#[strum(ascii_case_insensitive)]
206#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
207#[cfg_attr(
208    feature = "python",
209    pyo3::pyclass(
210        frozen,
211        eq,
212        eq_int,
213        module = "nautilus_trader.model",
214        from_py_object,
215        rename_all = "SCREAMING_SNAKE_CASE",
216    )
217)]
218#[cfg_attr(
219    feature = "python",
220    pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.model")
221)]
222#[allow(non_camel_case_types)]
223pub enum AssetClass {
224    /// Foreign exchange (FOREX) assets.
225    FX = 1,
226    /// Equity / stock assets.
227    Equity = 2,
228    /// Commodity assets.
229    Commodity = 3,
230    /// Debt based assets.
231    Debt = 4,
232    /// Index based assets (baskets).
233    Index = 5,
234    /// Cryptocurrency or crypto token assets.
235    Cryptocurrency = 6,
236    /// Alternative assets.
237    Alternative = 7,
238}
239
240impl FromU8 for AssetClass {
241    fn from_u8(value: u8) -> Option<Self> {
242        Self::from_repr(usize::from(value))
243    }
244}
245
246/// The aggregation method through which a bar is generated and closed.
247#[repr(C)]
248#[derive(
249    Copy,
250    Clone,
251    Debug,
252    Display,
253    Hash,
254    PartialEq,
255    Eq,
256    PartialOrd,
257    Ord,
258    AsRefStr,
259    FromRepr,
260    EnumIter,
261    EnumString,
262)]
263#[strum(ascii_case_insensitive)]
264#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
265#[cfg_attr(
266    feature = "python",
267    pyo3::pyclass(
268        frozen,
269        eq,
270        eq_int,
271        module = "nautilus_trader.model",
272        from_py_object,
273        rename_all = "SCREAMING_SNAKE_CASE",
274    )
275)]
276#[cfg_attr(
277    feature = "python",
278    pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.model")
279)]
280pub enum BarAggregation {
281    /// Based on a number of ticks.
282    Tick = 1,
283    /// Based on the buy/sell imbalance of ticks.
284    TickImbalance = 2,
285    /// Based on sequential buy/sell runs of ticks.
286    TickRuns = 3,
287    /// Based on traded volume.
288    Volume = 4,
289    /// Based on the buy/sell imbalance of traded volume.
290    VolumeImbalance = 5,
291    /// Based on sequential runs of buy/sell traded volume.
292    VolumeRuns = 6,
293    /// Based on the 'notional' value of the instrument.
294    Value = 7,
295    /// Based on the buy/sell imbalance of trading by notional value.
296    ValueImbalance = 8,
297    /// Based on sequential buy/sell runs of trading by notional value.
298    ValueRuns = 9,
299    /// Based on time intervals with millisecond granularity.
300    Millisecond = 10,
301    /// Based on time intervals with second granularity.
302    Second = 11,
303    /// Based on time intervals with minute granularity.
304    Minute = 12,
305    /// Based on time intervals with hour granularity.
306    Hour = 13,
307    /// Based on time intervals with day granularity.
308    Day = 14,
309    /// Based on time intervals with week granularity.
310    Week = 15,
311    /// Based on time intervals with month granularity.
312    Month = 16,
313    /// Based on time intervals with year granularity.
314    Year = 17,
315    /// Based on fixed price movements (brick size).
316    Renko = 18,
317}
318
319/// The interval type for bar aggregation.
320#[repr(C)]
321#[derive(
322    Copy,
323    Clone,
324    Debug,
325    Default,
326    Display,
327    Hash,
328    PartialEq,
329    Eq,
330    PartialOrd,
331    Ord,
332    AsRefStr,
333    FromRepr,
334    EnumIter,
335    EnumString,
336)]
337#[strum(ascii_case_insensitive)]
338#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
339#[cfg_attr(
340    feature = "python",
341    pyo3::pyclass(
342        frozen,
343        eq,
344        eq_int,
345        module = "nautilus_trader.model",
346        from_py_object,
347        rename_all = "SCREAMING_SNAKE_CASE",
348    )
349)]
350#[cfg_attr(
351    feature = "python",
352    pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.model")
353)]
354pub enum BarIntervalType {
355    /// Left-open interval `(start, end]`: start is exclusive, end is inclusive (default).
356    #[default]
357    LeftOpen = 1,
358    /// Right-open interval `[start, end)`: start is inclusive, end is exclusive.
359    RightOpen = 2,
360}
361
362/// Represents the side of a bet in a betting market.
363#[repr(C)]
364#[derive(
365    Copy,
366    Clone,
367    Debug,
368    Display,
369    Hash,
370    PartialEq,
371    Eq,
372    PartialOrd,
373    Ord,
374    AsRefStr,
375    FromRepr,
376    EnumIter,
377    EnumString,
378)]
379#[strum(ascii_case_insensitive)]
380#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
381#[cfg_attr(
382    feature = "python",
383    pyo3::pyclass(
384        frozen,
385        eq,
386        eq_int,
387        module = "nautilus_trader.model",
388        from_py_object,
389        rename_all = "SCREAMING_SNAKE_CASE",
390    )
391)]
392#[cfg_attr(
393    feature = "python",
394    pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.model")
395)]
396pub enum BetSide {
397    /// A "Back" bet signifies support for a specific outcome.
398    Back = 1,
399    /// A "Lay" bet signifies opposition to a specific outcome.
400    Lay = 2,
401}
402
403impl BetSide {
404    /// Returns the opposite betting side.
405    #[must_use]
406    pub fn opposite(&self) -> Self {
407        match self {
408            Self::Back => Self::Lay,
409            Self::Lay => Self::Back,
410        }
411    }
412}
413
414impl From<OrderSide> for BetSide {
415    fn from(side: OrderSide) -> Self {
416        match side {
417            OrderSide::Buy => Self::Back,
418            OrderSide::Sell => Self::Lay,
419        }
420    }
421}
422
423/// The type of order book action for an order book event.
424#[repr(C)]
425#[derive(
426    Copy,
427    Clone,
428    Debug,
429    Display,
430    Hash,
431    PartialEq,
432    Eq,
433    PartialOrd,
434    Ord,
435    AsRefStr,
436    FromRepr,
437    EnumIter,
438    EnumString,
439)]
440#[strum(ascii_case_insensitive)]
441#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
442#[cfg_attr(
443    feature = "python",
444    pyo3::pyclass(
445        frozen,
446        eq,
447        eq_int,
448        module = "nautilus_trader.model",
449        from_py_object,
450        rename_all = "SCREAMING_SNAKE_CASE",
451    )
452)]
453#[cfg_attr(
454    feature = "python",
455    pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.model")
456)]
457pub enum BookAction {
458    /// An order is added to the book.
459    Add = 1,
460    /// An existing order in the book is updated/modified.
461    Update = 2,
462    /// An existing order in the book is deleted/canceled.
463    Delete = 3,
464    /// The state of the order book is cleared.
465    Clear = 4,
466}
467
468impl FromU8 for BookAction {
469    fn from_u8(value: u8) -> Option<Self> {
470        Self::from_repr(usize::from(value))
471    }
472}
473
474/// The order book type, representing the type of levels granularity and delta updating heuristics.
475#[repr(C)]
476#[derive(
477    Copy,
478    Clone,
479    Debug,
480    Display,
481    Hash,
482    PartialEq,
483    Eq,
484    PartialOrd,
485    Ord,
486    AsRefStr,
487    FromRepr,
488    EnumIter,
489    EnumString,
490)]
491#[strum(ascii_case_insensitive)]
492#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
493#[cfg_attr(
494    feature = "python",
495    pyo3::pyclass(
496        frozen,
497        eq,
498        eq_int,
499        module = "nautilus_trader.model",
500        from_py_object,
501        rename_all = "SCREAMING_SNAKE_CASE",
502    )
503)]
504#[cfg_attr(
505    feature = "python",
506    pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.model")
507)]
508#[allow(non_camel_case_types)]
509pub enum BookType {
510    /// Top-of-book best bid/ask, one level per side.
511    L1_MBP = 1,
512    /// Market by price, one order per level (aggregated).
513    L2_MBP = 2,
514    /// Market by order, multiple orders per level (full granularity).
515    L3_MBO = 3,
516}
517
518impl FromU8 for BookType {
519    fn from_u8(value: u8) -> Option<Self> {
520        Self::from_repr(usize::from(value))
521    }
522}
523
524/// The order contingency type which specifies the behavior of linked orders.
525///
526/// [FIX 5.0 SP2 : ContingencyType <1385> field](https://www.onixs.biz/fix-dictionary/5.0.sp2/tagnum_1385.html).
527///
528/// Python retains `NO_CONTINGENCY` as a compatibility alias for `None`. The alias is not an enum
529/// variant and may be removed in a future version.
530#[repr(C)]
531#[derive(
532    Copy,
533    Clone,
534    Debug,
535    Display,
536    Hash,
537    PartialEq,
538    Eq,
539    PartialOrd,
540    Ord,
541    AsRefStr,
542    FromRepr,
543    EnumIter,
544    EnumString,
545)]
546#[strum(ascii_case_insensitive)]
547#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
548#[cfg_attr(
549    feature = "python",
550    pyo3::pyclass(
551        frozen,
552        eq,
553        eq_int,
554        module = "nautilus_trader.model",
555        from_py_object,
556        rename_all = "SCREAMING_SNAKE_CASE",
557    )
558)]
559#[cfg_attr(
560    feature = "python",
561    pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.model")
562)]
563pub enum ContingencyType {
564    /// One-Cancels-the-Other.
565    Oco = 1,
566    /// One-Triggers-the-Other.
567    Oto = 2,
568    /// One-Updates-the-Other (by proportional quantity).
569    Ouo = 3,
570}
571
572/// The price-adjustment scheme applied when stitching segment contracts into a
573/// continuous future series.
574///
575/// The direction (backward vs. forward) selects the anchor contract:
576/// - Backward modes anchor on the most recent contract; prices in older
577///   segments are shifted into the latest contract's frame.
578/// - Forward modes anchor on the first contract; prices in later segments
579///   are shifted into the first contract's frame.
580///
581/// The kind (spread vs. ratio) selects how each transition's offset is combined:
582/// - Spread modes accumulate additive offsets (`post_price - pre_price`).
583/// - Ratio modes accumulate multiplicative factors (`post_price / pre_price`)
584///   and require strictly positive prices.
585#[repr(C)]
586#[derive(
587    Copy,
588    Clone,
589    Debug,
590    Default,
591    Display,
592    Hash,
593    PartialEq,
594    Eq,
595    PartialOrd,
596    Ord,
597    AsRefStr,
598    FromRepr,
599    EnumIter,
600    EnumString,
601)]
602#[strum(ascii_case_insensitive)]
603#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
604#[cfg_attr(
605    feature = "python",
606    pyo3::pyclass(
607        frozen,
608        eq,
609        eq_int,
610        module = "nautilus_trader.model",
611        from_py_object,
612        rename_all = "SCREAMING_SNAKE_CASE",
613    )
614)]
615#[cfg_attr(
616    feature = "python",
617    pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.model")
618)]
619pub enum ContinuousFutureAdjustmentType {
620    /// Additive adjustment, anchored on the most recent contract.
621    #[default]
622    BackwardSpread = 1,
623    /// Additive adjustment, anchored on the first contract.
624    ForwardSpread = 2,
625    /// Multiplicative adjustment, anchored on the most recent contract.
626    BackwardRatio = 3,
627    /// Multiplicative adjustment, anchored on the first contract.
628    ForwardRatio = 4,
629}
630
631impl ContinuousFutureAdjustmentType {
632    /// Returns whether this mode accumulates multiplicative factors.
633    #[must_use]
634    pub const fn is_ratio(&self) -> bool {
635        matches!(self, Self::BackwardRatio | Self::ForwardRatio)
636    }
637
638    /// Returns whether this mode anchors on the most recent contract.
639    #[must_use]
640    pub const fn is_backward(&self) -> bool {
641        matches!(self, Self::BackwardSpread | Self::BackwardRatio)
642    }
643}
644
645/// The broad currency type.
646#[repr(C)]
647#[derive(
648    Copy,
649    Clone,
650    Debug,
651    Display,
652    Hash,
653    PartialEq,
654    Eq,
655    PartialOrd,
656    Ord,
657    AsRefStr,
658    FromRepr,
659    EnumIter,
660    EnumString,
661)]
662#[strum(ascii_case_insensitive)]
663#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
664#[cfg_attr(
665    feature = "python",
666    pyo3::pyclass(
667        frozen,
668        eq,
669        eq_int,
670        module = "nautilus_trader.model",
671        from_py_object,
672        rename_all = "SCREAMING_SNAKE_CASE",
673    )
674)]
675#[cfg_attr(
676    feature = "python",
677    pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.model")
678)]
679pub enum CurrencyType {
680    /// A type of cryptocurrency or crypto token.
681    Crypto = 1,
682    /// A type of currency issued by governments which is not backed by a commodity.
683    Fiat = 2,
684    /// A type of currency that is based on the value of an underlying commodity.
685    CommodityBacked = 3,
686}
687
688/// The instrument class.
689#[repr(C)]
690#[derive(
691    Copy,
692    Clone,
693    Debug,
694    Display,
695    Hash,
696    PartialEq,
697    Eq,
698    PartialOrd,
699    Ord,
700    AsRefStr,
701    FromRepr,
702    EnumIter,
703    EnumString,
704)]
705#[strum(ascii_case_insensitive)]
706#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
707#[cfg_attr(
708    feature = "python",
709    pyo3::pyclass(
710        frozen,
711        eq,
712        eq_int,
713        module = "nautilus_trader.model",
714        from_py_object,
715        rename_all = "SCREAMING_SNAKE_CASE",
716    )
717)]
718#[cfg_attr(
719    feature = "python",
720    pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.model")
721)]
722pub enum InstrumentClass {
723    /// A spot market instrument class. The current market price of an instrument that is bought or sold for immediate delivery and payment.
724    Spot = 1,
725    /// A swap instrument class. A derivative contract through which two parties exchange the cash flows or liabilities from two different financial instruments.
726    Swap = 2,
727    /// A futures contract instrument class. A legal agreement to buy or sell an asset at a predetermined price at a specified time in the future.
728    Future = 3,
729    /// A futures spread instrument class. A strategy involving the use of futures contracts to take advantage of price differentials between different contract months, underlying assets, or marketplaces.
730    FuturesSpread = 4,
731    /// A forward derivative instrument class. A customized contract between two parties to buy or sell an asset at a specified price on a future date.
732    Forward = 5,
733    /// A contract-for-difference (CFD) instrument class. A contract between an investor and a CFD broker to exchange the difference in the value of a financial product between the time the contract opens and closes.
734    Cfd = 6,
735    /// A bond instrument class. A type of debt investment where an investor loans money to an entity (typically corporate or governmental) which borrows the funds for a defined period of time at a variable or fixed interest rate.
736    Bond = 7,
737    /// An option contract instrument class. A type of derivative that gives the holder the right, but not the obligation, to buy or sell an underlying asset at a predetermined price before or at a certain future date.
738    Option = 8,
739    /// An option spread instrument class. A strategy involving the purchase and/or sale of multiple option contracts on the same underlying asset with different strike prices or expiration dates to hedge risk or speculate on price movements.
740    OptionSpread = 9,
741    /// A warrant instrument class. A derivative that gives the holder the right, but not the obligation, to buy or sell a security - most commonly an equity - at a certain price before expiration.
742    Warrant = 10,
743    /// A sports betting instrument class. A financialized derivative that allows wagering on the outcome of sports events using structured contracts or prediction markets.
744    SportsBetting = 11,
745    /// A binary option instrument class. A type of derivative where the payoff is either a fixed monetary amount or nothing, depending on whether the price of an underlying asset is above or below a predetermined level at expiration.
746    BinaryOption = 12,
747}
748
749impl InstrumentClass {
750    /// Returns whether this instrument class has an expiration.
751    #[must_use]
752    pub const fn has_expiration(&self) -> bool {
753        matches!(
754            self,
755            Self::Future | Self::FuturesSpread | Self::Option | Self::OptionSpread
756        )
757    }
758
759    /// Returns whether this instrument class allows negative prices.
760    #[must_use]
761    pub const fn allows_negative_price(&self) -> bool {
762        matches!(
763            self,
764            Self::Option | Self::FuturesSpread | Self::OptionSpread
765        )
766    }
767
768    /// Returns the [`InstrumentClass`] for the parent-symbol suffix, if recognized.
769    ///
770    /// Matches strict uppercase forms only. Both Databento-style abbreviations
771    /// (`FUT`, `OPT`) and long forms (`FUTURE`, `OPTION`) are accepted.
772    #[must_use]
773    pub fn try_from_parent_suffix(suffix: &str) -> Option<Self> {
774        match suffix {
775            "FUT" | "FUTURE" => Some(Self::Future),
776            "OPT" | "OPTION" => Some(Self::Option),
777            _ => None,
778        }
779    }
780
781    /// Returns the canonical parent-symbol suffix for this class, if one exists.
782    ///
783    /// Always emits the short form (`FUT`, `OPT`) so that adapters constructing
784    /// parent ids produce a single canonical string per class.
785    #[must_use]
786    pub const fn parent_suffix(self) -> Option<&'static str> {
787        match self {
788            Self::Future => Some("FUT"),
789            Self::Option => Some("OPT"),
790            _ => None,
791        }
792    }
793}
794
795/// The type of event for an instrument close.
796#[repr(C)]
797#[derive(
798    Copy,
799    Clone,
800    Debug,
801    Display,
802    Hash,
803    PartialEq,
804    Eq,
805    PartialOrd,
806    Ord,
807    AsRefStr,
808    FromRepr,
809    EnumIter,
810    EnumString,
811)]
812#[strum(ascii_case_insensitive)]
813#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
814#[cfg_attr(
815    feature = "python",
816    pyo3::pyclass(
817        frozen,
818        eq,
819        eq_int,
820        module = "nautilus_trader.model",
821        from_py_object,
822        rename_all = "SCREAMING_SNAKE_CASE",
823    )
824)]
825#[cfg_attr(
826    feature = "python",
827    pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.model")
828)]
829pub enum InstrumentCloseType {
830    /// When the market session ended.
831    EndOfSession = 1,
832    /// When the instrument expiration was reached.
833    ContractExpired = 2,
834}
835
836/// Convert the given `value` to an [`InstrumentCloseType`].
837impl FromU8 for InstrumentCloseType {
838    fn from_u8(value: u8) -> Option<Self> {
839        Self::from_repr(usize::from(value))
840    }
841}
842
843/// The liquidity side for a trade.
844#[repr(C)]
845#[derive(
846    Copy,
847    Clone,
848    Debug,
849    Display,
850    Hash,
851    PartialEq,
852    Eq,
853    PartialOrd,
854    Ord,
855    AsRefStr,
856    FromRepr,
857    EnumIter,
858    EnumString,
859)]
860#[strum(ascii_case_insensitive)]
861#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
862#[cfg_attr(
863    feature = "python",
864    pyo3::pyclass(
865        frozen,
866        eq,
867        eq_int,
868        module = "nautilus_trader.model",
869        from_py_object,
870        rename_all = "SCREAMING_SNAKE_CASE",
871    )
872)]
873#[cfg_attr(
874    feature = "python",
875    pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.model")
876)]
877pub enum LiquiditySide {
878    /// No liquidity side specified.
879    NoLiquiditySide = 0,
880    /// The order passively provided liquidity to the market to complete the trade (made a market).
881    Maker = 1,
882    /// The order aggressively took liquidity from the market to complete the trade.
883    Taker = 2,
884}
885
886/// The status of an individual market on a trading venue.
887#[repr(C)]
888#[derive(
889    Copy,
890    Clone,
891    Debug,
892    Display,
893    Hash,
894    PartialEq,
895    Eq,
896    PartialOrd,
897    Ord,
898    AsRefStr,
899    FromRepr,
900    EnumIter,
901    EnumString,
902)]
903#[strum(ascii_case_insensitive)]
904#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
905#[cfg_attr(
906    feature = "python",
907    pyo3::pyclass(
908        frozen,
909        eq,
910        eq_int,
911        module = "nautilus_trader.model",
912        from_py_object,
913        rename_all = "SCREAMING_SNAKE_CASE",
914    )
915)]
916#[cfg_attr(
917    feature = "python",
918    pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.model")
919)]
920pub enum MarketStatus {
921    /// The instrument is trading.
922    Open = 1,
923    /// Trading in the instrument has closed.
924    Closed = 2,
925    /// Trading in the instrument has been paused.
926    Paused = 3,
927    /// Trading in the instrument has been halted.
928    Halted = 4,
929    /// Trading in the instrument has been suspended.
930    Suspended = 5,
931    /// Trading in the instrument is not available.
932    NotAvailable = 6,
933}
934
935/// An action affecting the status of an individual market on a trading venue.
936#[repr(C)]
937#[derive(
938    Copy,
939    Clone,
940    Debug,
941    Display,
942    Hash,
943    PartialEq,
944    Eq,
945    PartialOrd,
946    Ord,
947    AsRefStr,
948    FromRepr,
949    EnumIter,
950    EnumString,
951)]
952#[strum(ascii_case_insensitive)]
953#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
954#[cfg_attr(
955    feature = "python",
956    pyo3::pyclass(
957        frozen,
958        eq,
959        eq_int,
960        module = "nautilus_trader.model",
961        from_py_object,
962        rename_all = "SCREAMING_SNAKE_CASE",
963    )
964)]
965#[cfg_attr(
966    feature = "python",
967    pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.model")
968)]
969pub enum MarketStatusAction {
970    /// No change.
971    None = 0,
972    /// The instrument is in a pre-open period.
973    PreOpen = 1,
974    /// The instrument is in a pre-cross period.
975    PreCross = 2,
976    /// The instrument is quoting but not trading.
977    Quoting = 3,
978    /// The instrument is in a cross/auction.
979    Cross = 4,
980    /// The instrument is being opened through a trading rotation.
981    Rotation = 5,
982    /// A new price indication is available for the instrument.
983    NewPriceIndication = 6,
984    /// The instrument is trading.
985    Trading = 7,
986    /// Trading in the instrument has been halted.
987    Halt = 8,
988    /// Trading in the instrument has been paused.
989    Pause = 9,
990    /// Trading in the instrument has been suspended.
991    Suspend = 10,
992    /// The instrument is in a pre-close period.
993    PreClose = 11,
994    /// Trading in the instrument has closed.
995    Close = 12,
996    /// The instrument is in a post-close period.
997    PostClose = 13,
998    /// A change in short-selling restrictions.
999    ShortSellRestrictionChange = 14,
1000    /// The instrument is not available for trading, either trading has closed or been halted.
1001    NotAvailableForTrading = 15,
1002}
1003
1004/// Convert the given `value` to a [`MarketStatusAction`].
1005impl FromU16 for MarketStatusAction {
1006    fn from_u16(value: u16) -> Option<Self> {
1007        Self::from_repr(usize::from(value))
1008    }
1009}
1010
1011/// The order management system (OMS) type for a trading venue or trading strategy.
1012#[repr(C)]
1013#[derive(
1014    Copy,
1015    Clone,
1016    Debug,
1017    Default,
1018    Display,
1019    Hash,
1020    PartialEq,
1021    Eq,
1022    PartialOrd,
1023    Ord,
1024    AsRefStr,
1025    FromRepr,
1026    EnumIter,
1027    EnumString,
1028)]
1029#[strum(ascii_case_insensitive)]
1030#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
1031#[cfg_attr(
1032    feature = "python",
1033    pyo3::pyclass(
1034        frozen,
1035        eq,
1036        eq_int,
1037        module = "nautilus_trader.model",
1038        from_py_object,
1039        rename_all = "SCREAMING_SNAKE_CASE",
1040    )
1041)]
1042#[cfg_attr(
1043    feature = "python",
1044    pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.model")
1045)]
1046pub enum OmsType {
1047    /// There is no specific type of order management specified (will defer to the venue OMS).
1048    #[default]
1049    Unspecified = 0,
1050    /// The netting type where there is one position per instrument.
1051    Netting = 1,
1052    /// The hedging type where there can be multiple positions per instrument.
1053    /// This can be in LONG/SHORT directions, by position/ticket ID, or tracked virtually by
1054    /// Nautilus.
1055    Hedging = 2,
1056}
1057
1058/// The kind of option contract.
1059#[repr(C)]
1060#[derive(
1061    Copy,
1062    Clone,
1063    Debug,
1064    Display,
1065    Hash,
1066    PartialEq,
1067    Eq,
1068    PartialOrd,
1069    Ord,
1070    AsRefStr,
1071    FromRepr,
1072    EnumIter,
1073    EnumString,
1074)]
1075#[strum(ascii_case_insensitive)]
1076#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
1077#[cfg_attr(
1078    feature = "python",
1079    pyo3::pyclass(
1080        frozen,
1081        eq,
1082        eq_int,
1083        module = "nautilus_trader.model",
1084        from_py_object,
1085        rename_all = "SCREAMING_SNAKE_CASE",
1086    )
1087)]
1088#[cfg_attr(
1089    feature = "python",
1090    pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.model")
1091)]
1092pub enum OptionKind {
1093    /// A Call option gives the holder the right, but not the obligation, to buy an underlying asset at a specified strike price within a specified period of time.
1094    Call = 1,
1095    /// A Put option gives the holder the right, but not the obligation, to sell an underlying asset at a specified strike price within a specified period of time.
1096    Put = 2,
1097}
1098
1099/// The numeraire convention for option greeks published by a venue.
1100///
1101/// Crypto option venues commonly publish two parallel greek sets for the same
1102/// instrument: Black-Scholes greeks in USD, and price-adjusted greeks denominated
1103/// in the underlying/coin units. Deribit and OKX both expose the distinction;
1104/// see the OKX reference for the canonical definition:
1105/// <https://www.okx.com/docs-v5/en/#public-data-websocket-option-market-data>.
1106///
1107/// This is orthogonal to the percent-greeks transformation in the internal
1108/// [`GreeksCalculator`](../../../nautilus_common/greeks/struct.GreeksCalculator.html),
1109/// which rescales the delta/gamma input step rather than the numeraire.
1110#[repr(C)]
1111#[derive(
1112    Copy,
1113    Clone,
1114    Debug,
1115    Default,
1116    Display,
1117    Hash,
1118    PartialEq,
1119    Eq,
1120    PartialOrd,
1121    Ord,
1122    AsRefStr,
1123    FromRepr,
1124    EnumIter,
1125    EnumString,
1126)]
1127#[strum(ascii_case_insensitive)]
1128#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
1129#[cfg_attr(
1130    feature = "python",
1131    pyo3::pyclass(
1132        frozen,
1133        eq,
1134        eq_int,
1135        module = "nautilus_trader.model",
1136        from_py_object,
1137        rename_all = "SCREAMING_SNAKE_CASE",
1138    )
1139)]
1140#[cfg_attr(
1141    feature = "python",
1142    pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.model")
1143)]
1144pub enum GreeksConvention {
1145    /// Black-Scholes greeks in USD.
1146    #[default]
1147    BlackScholes = 1,
1148    /// Price-adjusted greeks in the underlying/coin units.
1149    PriceAdjusted = 2,
1150}
1151
1152/// Defines when OTO (One-Triggers-Other) child orders are released.
1153#[repr(C)]
1154#[derive(
1155    Copy,
1156    Clone,
1157    Debug,
1158    Default,
1159    Display,
1160    Hash,
1161    PartialEq,
1162    Eq,
1163    PartialOrd,
1164    Ord,
1165    AsRefStr,
1166    FromRepr,
1167    EnumIter,
1168    EnumString,
1169)]
1170#[strum(ascii_case_insensitive)]
1171#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
1172#[cfg_attr(
1173    feature = "python",
1174    pyo3::pyclass(
1175        frozen,
1176        eq,
1177        eq_int,
1178        module = "nautilus_trader.model",
1179        from_py_object,
1180        rename_all = "SCREAMING_SNAKE_CASE",
1181    )
1182)]
1183#[cfg_attr(
1184    feature = "python",
1185    pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.model")
1186)]
1187pub enum OtoTriggerMode {
1188    /// Release child order(s) pro-rata to each partial fill (default).
1189    #[default]
1190    Partial = 0,
1191    /// Release child order(s) only once the parent is fully filled.
1192    Full = 1,
1193}
1194
1195/// The order side (BUY or SELL).
1196///
1197/// Python retains `NO_ORDER_SIDE` as a compatibility alias for `None`. The alias is not an enum
1198/// variant and may be removed in a future version.
1199#[repr(C)]
1200#[derive(
1201    Copy,
1202    Clone,
1203    Debug,
1204    Display,
1205    Hash,
1206    PartialEq,
1207    Eq,
1208    PartialOrd,
1209    Ord,
1210    AsRefStr,
1211    FromRepr,
1212    EnumIter,
1213    EnumString,
1214)]
1215#[strum(ascii_case_insensitive)]
1216#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
1217#[cfg_attr(
1218    feature = "python",
1219    pyo3::pyclass(
1220        frozen,
1221        eq,
1222        eq_int,
1223        module = "nautilus_trader.model",
1224        from_py_object,
1225        rename_all = "SCREAMING_SNAKE_CASE",
1226    )
1227)]
1228#[cfg_attr(
1229    feature = "python",
1230    pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.model")
1231)]
1232pub enum OrderSide {
1233    /// The order is a BUY.
1234    Buy = 1,
1235    /// The order is a SELL.
1236    Sell = 2,
1237}
1238
1239impl OrderSide {
1240    /// Returns the opposite order side.
1241    #[must_use]
1242    pub fn opposite(&self) -> Self {
1243        match &self {
1244            Self::Buy => Self::Sell,
1245            Self::Sell => Self::Buy,
1246        }
1247    }
1248}
1249
1250/// The status for a specific order.
1251///
1252/// An order is considered _open_ for the following status:
1253///  - `ACCEPTED`
1254///  - `TRIGGERED`
1255///  - `PENDING_UPDATE`
1256///  - `PENDING_CANCEL`
1257///  - `PARTIALLY_FILLED`
1258///
1259/// An order is considered _in-flight_ for the following status:
1260///  - `SUBMITTED`
1261///  - `PENDING_UPDATE`
1262///  - `PENDING_CANCEL`
1263///
1264/// An order is considered _closed_ for the following status:
1265///  - `DENIED`
1266///  - `REJECTED`
1267///  - `CANCELED`
1268///  - `EXPIRED`
1269///  - `FILLED`
1270///  - `VOIDED`
1271#[repr(C)]
1272#[derive(
1273    Copy,
1274    Clone,
1275    Debug,
1276    Display,
1277    Hash,
1278    PartialEq,
1279    Eq,
1280    PartialOrd,
1281    Ord,
1282    AsRefStr,
1283    FromRepr,
1284    EnumIter,
1285    EnumString,
1286)]
1287#[strum(ascii_case_insensitive)]
1288#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
1289#[cfg_attr(
1290    feature = "python",
1291    pyo3::pyclass(
1292        frozen,
1293        eq,
1294        eq_int,
1295        module = "nautilus_trader.model",
1296        from_py_object,
1297        rename_all = "SCREAMING_SNAKE_CASE",
1298    )
1299)]
1300#[cfg_attr(
1301    feature = "python",
1302    pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.model")
1303)]
1304pub enum OrderStatus {
1305    /// The order is initialized (instantiated) within the Nautilus system.
1306    Initialized = 1,
1307    /// The order was denied by the Nautilus system, either for being invalid, unprocessable, or exceeding a risk limit.
1308    Denied = 2,
1309    /// The order became emulated by the Nautilus system in the `OrderEmulator` component.
1310    Emulated = 3,
1311    /// The order was released by the Nautilus system from the `OrderEmulator` component.
1312    Released = 4,
1313    /// The order was submitted by the Nautilus system to the external service or trading venue (awaiting acknowledgement).
1314    Submitted = 5,
1315    /// The order was acknowledged by the trading venue as being received and valid (may now be working).
1316    Accepted = 6,
1317    /// The order was rejected by the trading venue.
1318    Rejected = 7,
1319    /// The order was canceled (closed/done).
1320    Canceled = 8,
1321    /// The order reached a GTD expiration (closed/done).
1322    Expired = 9,
1323    /// The order STOP price was triggered on a trading venue.
1324    Triggered = 10,
1325    /// The order is currently pending a request to modify on a trading venue.
1326    PendingUpdate = 11,
1327    /// The order is currently pending a request to cancel on a trading venue.
1328    PendingCancel = 12,
1329    /// The order has been partially filled on a trading venue.
1330    PartiallyFilled = 13,
1331    /// The order has been completely filled on a trading venue (closed/done).
1332    Filled = 14,
1333    /// The order is terminal after an authoritative venue void or fill correction.
1334    Voided = 15,
1335}
1336
1337impl OrderStatus {
1338    /// Returns whether the order status represents an open (working) order at the venue.
1339    ///
1340    /// This excludes `Submitted`, which is in-flight rather than working. When filtering venue
1341    /// order status reports for orders the venue still holds, use `is_open() || is_inflight()`:
1342    /// a venue that reports a resting order as pending maps it to `Submitted`, and testing
1343    /// `is_open()` alone silently drops it from reconciliation.
1344    #[must_use]
1345    pub const fn is_open(self) -> bool {
1346        matches!(
1347            self,
1348            Self::Accepted
1349                | Self::Triggered
1350                | Self::PendingUpdate
1351                | Self::PendingCancel
1352                | Self::PartiallyFilled
1353        )
1354    }
1355
1356    /// Returns whether the order status represents a terminal (closed) state.
1357    #[must_use]
1358    pub const fn is_closed(self) -> bool {
1359        matches!(
1360            self,
1361            Self::Denied
1362                | Self::Rejected
1363                | Self::Canceled
1364                | Self::Expired
1365                | Self::Filled
1366                | Self::Voided
1367        )
1368    }
1369
1370    /// Returns whether the order status represents an in-flight request to the venue.
1371    ///
1372    /// `PENDING_UPDATE` and `PENDING_CANCEL` are both open and in-flight: the order is working at
1373    /// the venue while a modify or cancel request is outstanding.
1374    #[must_use]
1375    pub const fn is_inflight(self) -> bool {
1376        matches!(
1377            self,
1378            Self::Submitted | Self::PendingUpdate | Self::PendingCancel
1379        )
1380    }
1381
1382    /// Returns whether the order can be cancelled from this status.
1383    #[must_use]
1384    pub const fn is_cancellable(self) -> bool {
1385        matches!(
1386            self,
1387            Self::Accepted | Self::Triggered | Self::PendingUpdate | Self::PartiallyFilled
1388        )
1389    }
1390}
1391
1392/// The type of order.
1393#[repr(C)]
1394#[derive(
1395    Copy,
1396    Clone,
1397    Debug,
1398    Display,
1399    Hash,
1400    PartialEq,
1401    Eq,
1402    PartialOrd,
1403    Ord,
1404    AsRefStr,
1405    FromRepr,
1406    EnumIter,
1407    EnumString,
1408)]
1409#[strum(ascii_case_insensitive)]
1410#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
1411#[cfg_attr(
1412    feature = "python",
1413    pyo3::pyclass(
1414        frozen,
1415        eq,
1416        eq_int,
1417        module = "nautilus_trader.model",
1418        from_py_object,
1419        rename_all = "SCREAMING_SNAKE_CASE",
1420    )
1421)]
1422#[cfg_attr(
1423    feature = "python",
1424    pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.model")
1425)]
1426pub enum OrderType {
1427    /// A market order to buy or sell at the best available price in the current market.
1428    Market = 1,
1429    /// A limit order to buy or sell at a specific price or better.
1430    Limit = 2,
1431    /// A stop market order to buy or sell once the price reaches the specified stop/trigger price. When the stop price is reached, the order effectively becomes a market order.
1432    StopMarket = 3,
1433    /// A stop limit order to buy or sell which combines the features of a stop order and a limit order. Once the stop/trigger price is reached, a stop-limit order effectively becomes a limit order.
1434    StopLimit = 4,
1435    /// A market-to-limit order is a market order that is to be executed as a limit order at the current best market price after reaching the market.
1436    MarketToLimit = 5,
1437    /// A market-if-touched order effectively becomes a market order when the specified trigger price is reached.
1438    MarketIfTouched = 6,
1439    /// A limit-if-touched order effectively becomes a limit order when the specified trigger price is reached.
1440    LimitIfTouched = 7,
1441    /// A trailing stop market order sets the stop/trigger price at a fixed "trailing offset" amount from the market.
1442    TrailingStopMarket = 8,
1443    /// A trailing stop limit order combines the features of a trailing stop order with those of a limit order.
1444    TrailingStopLimit = 9,
1445}
1446
1447/// The type of position adjustment.
1448#[repr(C)]
1449#[derive(
1450    Copy,
1451    Clone,
1452    Debug,
1453    Display,
1454    Hash,
1455    PartialEq,
1456    Eq,
1457    PartialOrd,
1458    Ord,
1459    AsRefStr,
1460    FromRepr,
1461    EnumIter,
1462    EnumString,
1463)]
1464#[strum(ascii_case_insensitive)]
1465#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
1466#[cfg_attr(
1467    feature = "python",
1468    pyo3::pyclass(
1469        frozen,
1470        eq,
1471        eq_int,
1472        module = "nautilus_trader.model",
1473        from_py_object,
1474        rename_all = "SCREAMING_SNAKE_CASE",
1475    )
1476)]
1477#[cfg_attr(
1478    feature = "python",
1479    pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.model")
1480)]
1481pub enum PositionAdjustmentType {
1482    /// Commission adjustment affecting position quantity.
1483    Commission = 1,
1484    /// Funding payment affecting position realized PnL.
1485    Funding = 2,
1486}
1487
1488impl FromU8 for PositionAdjustmentType {
1489    fn from_u8(value: u8) -> Option<Self> {
1490        Self::from_repr(usize::from(value))
1491    }
1492}
1493
1494/// The position side (FLAT, LONG, or SHORT).
1495///
1496/// Python retains `NO_POSITION_SIDE` as a compatibility alias for `None`. The alias is not an enum
1497/// variant and may be removed in a future version.
1498#[repr(C)]
1499#[derive(
1500    Copy,
1501    Clone,
1502    Debug,
1503    Display,
1504    Hash,
1505    PartialEq,
1506    Eq,
1507    PartialOrd,
1508    Ord,
1509    AsRefStr,
1510    FromRepr,
1511    EnumIter,
1512    EnumString,
1513)]
1514#[strum(ascii_case_insensitive)]
1515#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
1516#[cfg_attr(
1517    feature = "python",
1518    pyo3::pyclass(
1519        frozen,
1520        eq,
1521        eq_int,
1522        module = "nautilus_trader.model",
1523        from_py_object,
1524        rename_all = "SCREAMING_SNAKE_CASE",
1525    )
1526)]
1527#[cfg_attr(
1528    feature = "python",
1529    pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.model")
1530)]
1531pub enum PositionSide {
1532    /// A neutral/flat position, where no position is currently held in the market.
1533    Flat = 1,
1534    /// A long position in the market, typically acquired through one or many BUY orders.
1535    Long = 2,
1536    /// A short position in the market, typically acquired through one or many SELL orders.
1537    Short = 3,
1538}
1539
1540/// Serde compatibility for an optional order side previously encoded with `NO_ORDER_SIDE`.
1541pub mod serde_option_order_side {
1542    use serde::{Deserializer, Serializer};
1543
1544    use super::{OrderSide, deserialize_optional_enum, serialize_optional_enum};
1545
1546    /// Serializes an optional order side using the legacy no-side token.
1547    ///
1548    /// # Errors
1549    ///
1550    /// Returns an error if the serializer cannot encode the value.
1551    pub fn serialize<S>(value: &Option<OrderSide>, serializer: S) -> Result<S::Ok, S::Error>
1552    where
1553        S: Serializer,
1554    {
1555        serialize_optional_enum(value.as_ref(), serializer, "NO_ORDER_SIDE")
1556    }
1557
1558    /// Deserializes an optional order side from a side token or null.
1559    ///
1560    /// # Errors
1561    ///
1562    /// Returns an error if the input is not a valid order side.
1563    pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<OrderSide>, D::Error>
1564    where
1565        D: Deserializer<'de>,
1566    {
1567        deserialize_optional_enum(
1568            deserializer,
1569            "NO_ORDER_SIDE",
1570            "BUY, SELL, NO_ORDER_SIDE, or null",
1571        )
1572    }
1573}
1574
1575/// Serde compatibility for an optional position side previously encoded with `NO_POSITION_SIDE`.
1576pub mod serde_option_position_side {
1577    use serde::{Deserializer, Serializer};
1578
1579    use super::{PositionSide, deserialize_optional_enum, serialize_optional_enum};
1580
1581    /// Serializes an optional position side using the legacy no-side token.
1582    ///
1583    /// # Errors
1584    ///
1585    /// Returns an error if the serializer cannot encode the value.
1586    pub fn serialize<S>(value: &Option<PositionSide>, serializer: S) -> Result<S::Ok, S::Error>
1587    where
1588        S: Serializer,
1589    {
1590        serialize_optional_enum(value.as_ref(), serializer, "NO_POSITION_SIDE")
1591    }
1592
1593    /// Deserializes an optional position side from a side token or null.
1594    ///
1595    /// # Errors
1596    ///
1597    /// Returns an error if the input is not a valid position side.
1598    pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<PositionSide>, D::Error>
1599    where
1600        D: Deserializer<'de>,
1601    {
1602        deserialize_optional_enum(
1603            deserializer,
1604            "NO_POSITION_SIDE",
1605            "FLAT, LONG, SHORT, NO_POSITION_SIDE, or null",
1606        )
1607    }
1608}
1609
1610/// Serde compatibility for an optional contingency type previously encoded with `NO_CONTINGENCY`.
1611pub mod serde_option_contingency_type {
1612    use serde::{Deserializer, Serializer};
1613
1614    use super::{ContingencyType, deserialize_optional_enum, serialize_optional_enum};
1615
1616    /// Serializes an optional contingency type using the legacy no-contingency token.
1617    ///
1618    /// # Errors
1619    ///
1620    /// Returns an error if the serializer cannot encode the value.
1621    pub fn serialize<S>(value: &Option<ContingencyType>, serializer: S) -> Result<S::Ok, S::Error>
1622    where
1623        S: Serializer,
1624    {
1625        serialize_optional_enum(value.as_ref(), serializer, "NO_CONTINGENCY")
1626    }
1627
1628    /// Deserializes an optional contingency type from a contingency token or null.
1629    ///
1630    /// # Errors
1631    ///
1632    /// Returns an error if the input is not a valid contingency type.
1633    pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<ContingencyType>, D::Error>
1634    where
1635        D: Deserializer<'de>,
1636    {
1637        deserialize_optional_enum(
1638            deserializer,
1639            "NO_CONTINGENCY",
1640            "OCO, OTO, OUO, NO_CONTINGENCY, or null",
1641        )
1642    }
1643}
1644
1645/// Serde compatibility for an optional trailing offset type previously encoded with
1646/// `NO_TRAILING_OFFSET`.
1647pub mod serde_option_trailing_offset_type {
1648    use serde::{Deserializer, Serializer};
1649
1650    use super::{TrailingOffsetType, deserialize_optional_enum, serialize_optional_enum};
1651
1652    /// Serializes an optional trailing offset type using the legacy no-offset token.
1653    ///
1654    /// # Errors
1655    ///
1656    /// Returns an error if the serializer cannot encode the value.
1657    pub fn serialize<S>(
1658        value: &Option<TrailingOffsetType>,
1659        serializer: S,
1660    ) -> Result<S::Ok, S::Error>
1661    where
1662        S: Serializer,
1663    {
1664        serialize_optional_enum(value.as_ref(), serializer, "NO_TRAILING_OFFSET")
1665    }
1666
1667    /// Deserializes an optional trailing offset type from an offset token or null.
1668    ///
1669    /// # Errors
1670    ///
1671    /// Returns an error if the input is not a valid trailing offset type.
1672    pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<TrailingOffsetType>, D::Error>
1673    where
1674        D: Deserializer<'de>,
1675    {
1676        deserialize_optional_enum(
1677            deserializer,
1678            "NO_TRAILING_OFFSET",
1679            "PRICE, BASIS_POINTS, TICKS, PRICE_TIER, NO_TRAILING_OFFSET, or null",
1680        )
1681    }
1682}
1683
1684/// Serde compatibility for an optional trigger type previously encoded with `NO_TRIGGER`.
1685pub mod serde_option_trigger_type {
1686    use serde::{Deserializer, Serializer};
1687
1688    use super::{TriggerType, deserialize_optional_enum, serialize_optional_enum};
1689
1690    /// Serializes an optional trigger type using the legacy no-trigger token.
1691    ///
1692    /// # Errors
1693    ///
1694    /// Returns an error if the serializer cannot encode the value.
1695    pub fn serialize<S>(value: &Option<TriggerType>, serializer: S) -> Result<S::Ok, S::Error>
1696    where
1697        S: Serializer,
1698    {
1699        serialize_optional_enum(value.as_ref(), serializer, "NO_TRIGGER")
1700    }
1701
1702    /// Deserializes an optional trigger type from a trigger token or null.
1703    ///
1704    /// # Errors
1705    ///
1706    /// Returns an error if the input is not a valid trigger type.
1707    pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<TriggerType>, D::Error>
1708    where
1709        D: Deserializer<'de>,
1710    {
1711        deserialize_optional_enum(
1712            deserializer,
1713            "NO_TRIGGER",
1714            "a trigger type, NO_TRIGGER, or null",
1715        )
1716    }
1717}
1718
1719fn serialize_optional_enum<S, T>(
1720    value: Option<&T>,
1721    serializer: S,
1722    none_token: &'static str,
1723) -> Result<S::Ok, S::Error>
1724where
1725    S: Serializer,
1726    T: AsRef<str>,
1727{
1728    serializer.serialize_str(value.map_or(none_token, AsRef::as_ref))
1729}
1730
1731fn deserialize_optional_enum<'de, D, T>(
1732    deserializer: D,
1733    none_token: &'static str,
1734    expected: &'static str,
1735) -> Result<Option<T>, D::Error>
1736where
1737    D: Deserializer<'de>,
1738    T: FromStr,
1739    T::Err: Display,
1740{
1741    struct OptionalEnumVisitor<T> {
1742        none_token: &'static str,
1743        expected: &'static str,
1744        marker: PhantomData<T>,
1745    }
1746
1747    impl<'de, T> serde::de::Visitor<'de> for OptionalEnumVisitor<T>
1748    where
1749        T: FromStr,
1750        T::Err: Display,
1751    {
1752        type Value = Option<T>;
1753
1754        fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1755            formatter.write_str(self.expected)
1756        }
1757
1758        fn visit_none<E>(self) -> Result<Self::Value, E> {
1759            Ok(None)
1760        }
1761
1762        fn visit_unit<E>(self) -> Result<Self::Value, E> {
1763            Ok(None)
1764        }
1765
1766        fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
1767        where
1768            D: Deserializer<'de>,
1769        {
1770            let value = Cow::<'de, str>::deserialize(deserializer)?;
1771            if value.eq_ignore_ascii_case(self.none_token) {
1772                Ok(None)
1773            } else {
1774                T::from_str(&value)
1775                    .map(Some)
1776                    .map_err(serde::de::Error::custom)
1777            }
1778        }
1779    }
1780
1781    deserializer.deserialize_option(OptionalEnumVisitor {
1782        none_token,
1783        expected,
1784        marker: PhantomData,
1785    })
1786}
1787
1788/// The type of price for an instrument in a market.
1789#[repr(C)]
1790#[derive(
1791    Copy,
1792    Clone,
1793    Debug,
1794    Display,
1795    Hash,
1796    PartialEq,
1797    Eq,
1798    PartialOrd,
1799    Ord,
1800    AsRefStr,
1801    FromRepr,
1802    EnumIter,
1803    EnumString,
1804)]
1805#[strum(ascii_case_insensitive)]
1806#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
1807#[cfg_attr(
1808    feature = "python",
1809    pyo3::pyclass(
1810        frozen,
1811        eq,
1812        eq_int,
1813        module = "nautilus_trader.model",
1814        from_py_object,
1815        rename_all = "SCREAMING_SNAKE_CASE",
1816    )
1817)]
1818#[cfg_attr(
1819    feature = "python",
1820    pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.model")
1821)]
1822pub enum PriceType {
1823    // Bar price sources are not yet consistent with mark/index price subscriptions. The open
1824    // decisions are whether to add a `PriceType::Index` variant, whether to aggregate bars
1825    // internally from mark/index updates, and what the documented source derivation order is.
1826    /// The best quoted price at which buyers are willing to buy a quantity of an instrument.
1827    /// Often considered the best bid in the order book.
1828    Bid = 1,
1829    /// The best quoted price at which sellers are willing to sell a quantity of an instrument.
1830    /// Often considered the best ask in the order book.
1831    Ask = 2,
1832    /// The arithmetic midpoint between the best bid and ask quotes.
1833    Mid = 3,
1834    /// The price at which the last trade of an instrument was executed.
1835    Last = 4,
1836    /// A reference price reflecting an instrument's fair value, often used for portfolio
1837    /// calculations and risk management.
1838    Mark = 5,
1839}
1840
1841/// A record flag bit field, indicating event end and data information.
1842#[repr(C)]
1843#[derive(
1844    Copy,
1845    Clone,
1846    Debug,
1847    Display,
1848    Hash,
1849    PartialEq,
1850    Eq,
1851    PartialOrd,
1852    Ord,
1853    AsRefStr,
1854    FromRepr,
1855    EnumIter,
1856    EnumString,
1857)]
1858#[strum(ascii_case_insensitive)]
1859#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
1860#[cfg_attr(
1861    feature = "python",
1862    pyo3::pyclass(
1863        frozen,
1864        eq,
1865        eq_int,
1866        module = "nautilus_trader.model",
1867        from_py_object,
1868        rename_all = "SCREAMING_SNAKE_CASE",
1869    )
1870)]
1871#[cfg_attr(
1872    feature = "python",
1873    pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.model")
1874)]
1875#[allow(non_camel_case_types)]
1876pub enum RecordFlag {
1877    /// Last message in the book event or packet from the venue for a given `instrument_id`.
1878    F_LAST = 1 << 7, // 128
1879    /// Top-of-book message, not an individual order.
1880    F_TOB = 1 << 6, // 64
1881    /// Message sourced from a replay, such as a snapshot server.
1882    F_SNAPSHOT = 1 << 5, // 32
1883    /// Aggregated price level message, not an individual order.
1884    F_MBP = 1 << 4, // 16
1885    /// Reserved for future use.
1886    RESERVED_2 = 1 << 3, // 8
1887    /// Reserved for future use.
1888    RESERVED_1 = 1 << 2, // 4
1889}
1890
1891impl RecordFlag {
1892    /// Checks if the flag matches a given value.
1893    #[must_use]
1894    pub fn matches(self, value: u8) -> bool {
1895        (self as u8) & value != 0
1896    }
1897}
1898
1899/// The 'Time in Force' instruction for an order.
1900#[repr(C)]
1901#[derive(
1902    Copy,
1903    Clone,
1904    Debug,
1905    Display,
1906    Hash,
1907    PartialEq,
1908    Eq,
1909    PartialOrd,
1910    Ord,
1911    AsRefStr,
1912    FromRepr,
1913    EnumIter,
1914    EnumString,
1915)]
1916#[strum(ascii_case_insensitive)]
1917#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
1918#[cfg_attr(
1919    feature = "python",
1920    pyo3::pyclass(
1921        frozen,
1922        eq,
1923        eq_int,
1924        module = "nautilus_trader.model",
1925        from_py_object,
1926        rename_all = "SCREAMING_SNAKE_CASE",
1927    )
1928)]
1929#[cfg_attr(
1930    feature = "python",
1931    pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.model")
1932)]
1933pub enum TimeInForce {
1934    /// Good Till Cancel (GTC) - Remains active until canceled.
1935    Gtc = 1,
1936    /// Immediate or Cancel (IOC) - Executes immediately to the extent possible, with any unfilled portion canceled.
1937    Ioc = 2,
1938    /// Fill or Kill (FOK) - Executes in its entirety immediately or is canceled if full execution is not possible.
1939    Fok = 3,
1940    /// Good Till Date (GTD) - Remains active until the specified expiration date or time is reached.
1941    Gtd = 4,
1942    /// Day - Remains active until the close of the current trading session.
1943    Day = 5,
1944    /// At the Opening (ATO) - Executes at the market opening or expires if not filled.
1945    AtTheOpen = 6,
1946    /// At the Closing (ATC) - Executes at the market close or expires if not filled.
1947    AtTheClose = 7,
1948}
1949
1950/// The trading state for a node.
1951#[repr(C)]
1952#[derive(
1953    Copy,
1954    Clone,
1955    Debug,
1956    Display,
1957    Hash,
1958    PartialEq,
1959    Eq,
1960    PartialOrd,
1961    Ord,
1962    AsRefStr,
1963    FromRepr,
1964    EnumIter,
1965    EnumString,
1966)]
1967#[strum(ascii_case_insensitive)]
1968#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
1969#[cfg_attr(
1970    feature = "python",
1971    pyo3::pyclass(
1972        frozen,
1973        eq,
1974        eq_int,
1975        module = "nautilus_trader.model",
1976        from_py_object,
1977        rename_all = "SCREAMING_SNAKE_CASE",
1978    )
1979)]
1980#[cfg_attr(
1981    feature = "python",
1982    pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.model")
1983)]
1984pub enum TradingState {
1985    /// Normal trading operations.
1986    Active = 1,
1987    /// Only cancels, queries, and eligible reduce-only submissions are permitted.
1988    Reducing = 2,
1989    /// Only cancels and queries are permitted.
1990    Halted = 3,
1991}
1992
1993/// The trailing offset type for an order type which specifies a trailing stop/trigger or limit price.
1994///
1995/// Python retains `NO_TRAILING_OFFSET` as a compatibility alias for `None`. The alias is not an enum
1996/// variant and may be removed in a future version.
1997#[repr(C)]
1998#[derive(
1999    Copy,
2000    Clone,
2001    Debug,
2002    Display,
2003    Hash,
2004    PartialEq,
2005    Eq,
2006    PartialOrd,
2007    Ord,
2008    AsRefStr,
2009    FromRepr,
2010    EnumIter,
2011    EnumString,
2012)]
2013#[strum(ascii_case_insensitive)]
2014#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
2015#[cfg_attr(
2016    feature = "python",
2017    pyo3::pyclass(
2018        frozen,
2019        eq,
2020        eq_int,
2021        module = "nautilus_trader.model",
2022        from_py_object,
2023        rename_all = "SCREAMING_SNAKE_CASE",
2024    )
2025)]
2026#[cfg_attr(
2027    feature = "python",
2028    pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.model")
2029)]
2030pub enum TrailingOffsetType {
2031    /// The trailing offset is based on a market price.
2032    Price = 1,
2033    /// The trailing offset is based on a percentage represented in basis points, of a market price.
2034    BasisPoints = 2,
2035    /// The trailing offset is based on the number of ticks from a market price.
2036    Ticks = 3,
2037    /// The trailing offset is based on a price tier set by a specific trading venue.
2038    PriceTier = 4,
2039}
2040
2041/// The trigger type for the stop/trigger price of an order.
2042///
2043/// Python retains `NO_TRIGGER` as a compatibility alias for `None`. The alias is not an enum variant
2044/// and may be removed in a future version.
2045#[repr(C)]
2046#[derive(
2047    Copy,
2048    Clone,
2049    Debug,
2050    Display,
2051    Hash,
2052    PartialEq,
2053    Eq,
2054    PartialOrd,
2055    Ord,
2056    AsRefStr,
2057    FromRepr,
2058    EnumIter,
2059    EnumString,
2060)]
2061#[strum(ascii_case_insensitive)]
2062#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
2063#[cfg_attr(
2064    feature = "python",
2065    pyo3::pyclass(
2066        frozen,
2067        eq,
2068        eq_int,
2069        module = "nautilus_trader.model",
2070        from_py_object,
2071        rename_all = "SCREAMING_SNAKE_CASE",
2072    )
2073)]
2074#[cfg_attr(
2075    feature = "python",
2076    pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.model")
2077)]
2078pub enum TriggerType {
2079    /// The default trigger type set by the trading venue.
2080    Default = 1,
2081    /// Based on the last traded price for the instrument.
2082    LastPrice = 2,
2083    /// Based on the mark price for the instrument.
2084    MarkPrice = 3,
2085    /// Based on the index price for the instrument.
2086    IndexPrice = 4,
2087    /// Based on the top-of-book quoted prices for the instrument.
2088    BidAsk = 5,
2089    /// Based on a 'double match' of the last traded price for the instrument
2090    DoubleLast = 6,
2091    /// Based on a 'double match' of the bid/ask price for the instrument
2092    DoubleBidAsk = 7,
2093    /// Based on both the [`TriggerType::LastPrice`] and [`TriggerType::BidAsk`].
2094    LastOrBidAsk = 8,
2095    /// Based on the mid-point of the [`TriggerType::BidAsk`].
2096    MidPoint = 9,
2097}
2098
2099enum_strum_serde!(AccountType);
2100enum_strum_serde!(AggregationSource);
2101enum_strum_serde!(AggressorSide);
2102enum_strum_serde!(AssetClass);
2103enum_strum_serde!(BarAggregation);
2104enum_strum_serde!(BarIntervalType);
2105enum_strum_serde!(BetSide);
2106enum_strum_serde!(BookAction);
2107enum_strum_serde!(BookType);
2108enum_strum_serde!(ContingencyType);
2109enum_strum_serde!(ContinuousFutureAdjustmentType);
2110enum_strum_serde!(CurrencyType);
2111enum_strum_serde!(GreeksConvention);
2112enum_strum_serde!(InstrumentClass);
2113enum_strum_serde!(InstrumentCloseType);
2114enum_strum_serde!(LiquiditySide);
2115enum_strum_serde!(MarketStatus);
2116enum_strum_serde!(MarketStatusAction);
2117enum_strum_serde!(OmsType);
2118enum_strum_serde!(OptionKind);
2119enum_strum_serde!(OrderSide);
2120enum_strum_serde!(OrderStatus);
2121enum_strum_serde!(OrderType);
2122enum_strum_serde!(OtoTriggerMode);
2123enum_strum_serde!(PositionAdjustmentType);
2124enum_strum_serde!(PositionSide);
2125enum_strum_serde!(PriceType);
2126enum_strum_serde!(RecordFlag);
2127enum_strum_serde!(TimeInForce);
2128enum_strum_serde!(TradingState);
2129enum_strum_serde!(TrailingOffsetType);
2130enum_strum_serde!(TriggerType);
2131
2132#[cfg(test)]
2133mod tests {
2134    use rstest::rstest;
2135    use serde::{Serialize, de::DeserializeOwned};
2136    use strum::IntoEnumIterator;
2137
2138    use super::*;
2139
2140    #[derive(Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
2141    struct OptionalSides {
2142        #[serde(with = "serde_option_order_side")]
2143        order: Option<OrderSide>,
2144        #[serde(with = "serde_option_position_side")]
2145        position: Option<PositionSide>,
2146    }
2147
2148    #[derive(Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
2149    struct OptionalOrderTypes {
2150        #[serde(with = "serde_option_contingency_type")]
2151        contingency: Option<ContingencyType>,
2152        #[serde(with = "serde_option_trailing_offset_type")]
2153        trailing_offset: Option<TrailingOffsetType>,
2154        #[serde(with = "serde_option_trigger_type")]
2155        trigger: Option<TriggerType>,
2156    }
2157
2158    #[rstest]
2159    fn test_optional_sides_serde_preserves_legacy_none_tokens() {
2160        let value = OptionalSides {
2161            order: None,
2162            position: None,
2163        };
2164
2165        let json = serde_json::to_string(&value).unwrap();
2166        let decoded: OptionalSides = serde_json::from_str(&json).unwrap();
2167
2168        assert_eq!(
2169            json,
2170            r#"{"order":"NO_ORDER_SIDE","position":"NO_POSITION_SIDE"}"#
2171        );
2172        assert_eq!(decoded, value);
2173    }
2174
2175    #[rstest]
2176    fn test_optional_sides_serde_accepts_null_and_valid_sides() {
2177        let json = r#"{"order":null,"position":"LONG"}"#;
2178        let decoded: OptionalSides = serde_json::from_str(json).unwrap();
2179
2180        assert_eq!(
2181            decoded,
2182            OptionalSides {
2183                order: None,
2184                position: Some(PositionSide::Long),
2185            }
2186        );
2187    }
2188
2189    #[rstest]
2190    fn test_optional_order_types_serde_preserves_legacy_none_tokens() {
2191        let value = OptionalOrderTypes {
2192            contingency: None,
2193            trailing_offset: None,
2194            trigger: None,
2195        };
2196
2197        let json = serde_json::to_string(&value).unwrap();
2198        let decoded: OptionalOrderTypes = serde_json::from_str(&json).unwrap();
2199
2200        assert_eq!(
2201            json,
2202            r#"{"contingency":"NO_CONTINGENCY","trailing_offset":"NO_TRAILING_OFFSET","trigger":"NO_TRIGGER"}"#,
2203        );
2204        assert_eq!(decoded, value);
2205    }
2206
2207    #[rstest]
2208    fn test_optional_order_types_serde_accepts_null_and_valid_values() {
2209        let json = r#"{"contingency":null,"trailing_offset":"PRICE","trigger":"LAST_PRICE"}"#;
2210        let decoded: OptionalOrderTypes = serde_json::from_str(json).unwrap();
2211
2212        assert_eq!(
2213            decoded,
2214            OptionalOrderTypes {
2215                contingency: None,
2216                trailing_offset: Some(TrailingOffsetType::Price),
2217                trigger: Some(TriggerType::LastPrice),
2218            },
2219        );
2220    }
2221
2222    #[rstest]
2223    #[case(r#"{"contingency":"INVALID","trailing_offset":"NO_TRAILING_OFFSET","trigger":"NO_TRIGGER"}"#)]
2224    #[case(
2225        r#"{"contingency":"NO_CONTINGENCY","trailing_offset":"INVALID","trigger":"NO_TRIGGER"}"#
2226    )]
2227    #[case(r#"{"contingency":"NO_CONTINGENCY","trailing_offset":"NO_TRAILING_OFFSET","trigger":"INVALID"}"#)]
2228    fn test_optional_order_types_serde_rejects_invalid_values(#[case] json: &str) {
2229        assert!(serde_json::from_str::<OptionalOrderTypes>(json).is_err());
2230    }
2231
2232    #[rstest]
2233    #[case::bet_side_back(BetSide::Back, r#""BACK""#)]
2234    #[case::bet_side_lay(BetSide::Lay, r#""LAY""#)]
2235    fn test_bet_side_serde_uses_canonical_label(#[case] value: BetSide, #[case] expected: &str) {
2236        let json = serde_json::to_string(&value).unwrap();
2237
2238        assert_eq!(json, expected);
2239        assert_eq!(serde_json::from_str::<BetSide>(&json).unwrap(), value);
2240    }
2241
2242    #[rstest]
2243    #[case::oto_trigger_mode_partial(OtoTriggerMode::Partial, r#""PARTIAL""#)]
2244    #[case::oto_trigger_mode_full(OtoTriggerMode::Full, r#""FULL""#)]
2245    fn test_oto_trigger_mode_serde_uses_canonical_label(
2246        #[case] value: OtoTriggerMode,
2247        #[case] expected: &str,
2248    ) {
2249        let json = serde_json::to_string(&value).unwrap();
2250
2251        assert_eq!(json, expected);
2252        assert_eq!(
2253            serde_json::from_str::<OtoTriggerMode>(&json).unwrap(),
2254            value
2255        );
2256    }
2257
2258    #[rstest]
2259    #[case::pascal(r#""Back""#, BetSide::Back)]
2260    #[case::lower(r#""lay""#, BetSide::Lay)]
2261    fn test_bet_side_serde_is_case_insensitive(#[case] json: &str, #[case] expected: BetSide) {
2262        assert_eq!(serde_json::from_str::<BetSide>(json).unwrap(), expected);
2263    }
2264
2265    #[rstest]
2266    #[case::wrong_domain(r#""BUY""#)]
2267    #[case::empty(r#""""#)]
2268    fn test_bet_side_serde_rejects_unknown_label(#[case] json: &str) {
2269        assert!(serde_json::from_str::<BetSide>(json).is_err());
2270    }
2271
2272    #[rstest]
2273    #[case::no_aggressor(0, Some(AggressorSide::NoAggressor))]
2274    #[case::buy(1, Some(AggressorSide::Buy))]
2275    #[case::sell(2, Some(AggressorSide::Sell))]
2276    #[case::invalid(3, None)]
2277    #[case::max_u8(255, None)]
2278    fn test_aggressor_side_from_u8(#[case] value: u8, #[case] expected: Option<AggressorSide>) {
2279        assert_eq!(AggressorSide::from_u8(value), expected);
2280    }
2281
2282    #[rstest]
2283    #[case::active(TradingState::Active, 1)]
2284    #[case::reducing(TradingState::Reducing, 2)]
2285    #[case::halted(TradingState::Halted, 3)]
2286    fn test_trading_state_discriminants(#[case] state: TradingState, #[case] value: usize) {
2287        assert_eq!(state as usize, value);
2288        assert_eq!(TradingState::from_repr(value), Some(state));
2289    }
2290
2291    #[rstest]
2292    #[case(AggressorSide::NoAggressor, "NO_AGGRESSOR")]
2293    #[case(AggressorSide::Buy, "BUY")]
2294    #[case(AggressorSide::Sell, "SELL")]
2295    fn test_aggressor_side_to_string(#[case] value: AggressorSide, #[case] expected: &str) {
2296        assert_eq!(value.to_string(), expected);
2297        assert_eq!(value.as_ref(), expected);
2298    }
2299
2300    #[rstest]
2301    #[case(AggressorSide::NoAggressor, "NO_AGGRESSOR")]
2302    #[case(AggressorSide::Buy, "BUY")]
2303    #[case(AggressorSide::Sell, "SELL")]
2304    #[case(AggressorSide::Buy, "BUYER")]
2305    #[case(AggressorSide::Sell, "SELLER")]
2306    #[case(AggressorSide::Buy, "buy")]
2307    #[case(AggressorSide::Sell, "seller")]
2308    fn test_aggressor_side_from_str(#[case] expected: AggressorSide, #[case] value: &str) {
2309        assert_eq!(AggressorSide::from_str(value), Ok(expected));
2310    }
2311
2312    #[rstest]
2313    #[case(AggressorSide::Buy, "\"BUY\"")]
2314    #[case(AggressorSide::Sell, "\"SELL\"")]
2315    #[case(AggressorSide::NoAggressor, "\"NO_AGGRESSOR\"")]
2316    fn test_aggressor_side_serde_roundtrip(#[case] input: AggressorSide, #[case] expected: &str) {
2317        let json = serde_json::to_string(&input).unwrap();
2318        assert_eq!(json, expected);
2319        let parsed: AggressorSide = serde_json::from_str(expected).unwrap();
2320        assert_eq!(parsed, input);
2321    }
2322
2323    #[rstest]
2324    #[case("BUYER", AggressorSide::Buy)]
2325    #[case("SELLER", AggressorSide::Sell)]
2326    fn test_aggressor_side_serde_accepts_historical(
2327        #[case] value: &str,
2328        #[case] expected: AggressorSide,
2329    ) {
2330        let parsed: AggressorSide = serde_json::from_str(&format!("\"{value}\"")).unwrap();
2331        assert_eq!(parsed, expected);
2332    }
2333
2334    #[rstest]
2335    #[case(GreeksConvention::BlackScholes, "\"BLACK_SCHOLES\"")]
2336    #[case(GreeksConvention::PriceAdjusted, "\"PRICE_ADJUSTED\"")]
2337    fn test_greeks_convention_serde_roundtrip(
2338        #[case] input: GreeksConvention,
2339        #[case] expected: &str,
2340    ) {
2341        let json = serde_json::to_string(&input).unwrap();
2342        assert_eq!(json, expected);
2343        let parsed: GreeksConvention = serde_json::from_str(expected).unwrap();
2344        assert_eq!(parsed, input);
2345    }
2346
2347    #[rstest]
2348    fn test_greeks_convention_default_is_black_scholes() {
2349        assert_eq!(GreeksConvention::default(), GreeksConvention::BlackScholes);
2350    }
2351
2352    #[rstest]
2353    #[case(ContinuousFutureAdjustmentType::BackwardSpread, false, true)]
2354    #[case(ContinuousFutureAdjustmentType::ForwardSpread, false, false)]
2355    #[case(ContinuousFutureAdjustmentType::BackwardRatio, true, true)]
2356    #[case(ContinuousFutureAdjustmentType::ForwardRatio, true, false)]
2357    fn test_continuous_future_adjustment_type_predicates(
2358        #[case] mode: ContinuousFutureAdjustmentType,
2359        #[case] expected_is_ratio: bool,
2360        #[case] expected_is_backward: bool,
2361    ) {
2362        assert_eq!(mode.is_ratio(), expected_is_ratio);
2363        assert_eq!(mode.is_backward(), expected_is_backward);
2364    }
2365
2366    #[rstest]
2367    #[case(ContinuousFutureAdjustmentType::BackwardSpread, "\"BACKWARD_SPREAD\"")]
2368    #[case(ContinuousFutureAdjustmentType::ForwardSpread, "\"FORWARD_SPREAD\"")]
2369    #[case(ContinuousFutureAdjustmentType::BackwardRatio, "\"BACKWARD_RATIO\"")]
2370    #[case(ContinuousFutureAdjustmentType::ForwardRatio, "\"FORWARD_RATIO\"")]
2371    fn test_continuous_future_adjustment_type_serde_roundtrip(
2372        #[case] input: ContinuousFutureAdjustmentType,
2373        #[case] expected: &str,
2374    ) {
2375        let json = serde_json::to_string(&input).unwrap();
2376        assert_eq!(json, expected);
2377        let parsed: ContinuousFutureAdjustmentType = serde_json::from_str(expected).unwrap();
2378        assert_eq!(parsed, input);
2379    }
2380
2381    #[rstest]
2382    fn test_continuous_future_adjustment_type_default_is_backward_spread() {
2383        assert_eq!(
2384            ContinuousFutureAdjustmentType::default(),
2385            ContinuousFutureAdjustmentType::BackwardSpread,
2386        );
2387    }
2388
2389    #[rstest]
2390    #[case(InstrumentClass::Option, true)]
2391    #[case(InstrumentClass::FuturesSpread, true)]
2392    #[case(InstrumentClass::OptionSpread, true)]
2393    #[case(InstrumentClass::Spot, false)]
2394    #[case(InstrumentClass::Swap, false)]
2395    #[case(InstrumentClass::Future, false)]
2396    #[case(InstrumentClass::Forward, false)]
2397    #[case(InstrumentClass::Cfd, false)]
2398    #[case(InstrumentClass::Bond, false)]
2399    #[case(InstrumentClass::Warrant, false)]
2400    #[case(InstrumentClass::SportsBetting, false)]
2401    #[case(InstrumentClass::BinaryOption, false)]
2402    fn test_instrument_class_allows_negative_price(
2403        #[case] class: InstrumentClass,
2404        #[case] expected: bool,
2405    ) {
2406        assert_eq!(class.allows_negative_price(), expected);
2407    }
2408
2409    #[rstest]
2410    #[case("FUT", Some(InstrumentClass::Future))]
2411    #[case("FUTURE", Some(InstrumentClass::Future))]
2412    #[case("OPT", Some(InstrumentClass::Option))]
2413    #[case("OPTION", Some(InstrumentClass::Option))]
2414    #[case("fut", None)]
2415    #[case("Fut", None)]
2416    #[case("option", None)]
2417    #[case("Option", None)]
2418    #[case("SPREAD", None)]
2419    #[case("UNKNOWN", None)]
2420    #[case("", None)]
2421    fn test_instrument_class_try_from_parent_suffix(
2422        #[case] suffix: &str,
2423        #[case] expected: Option<InstrumentClass>,
2424    ) {
2425        assert_eq!(InstrumentClass::try_from_parent_suffix(suffix), expected);
2426    }
2427
2428    #[rstest]
2429    #[case(InstrumentClass::Future, Some("FUT"))]
2430    #[case(InstrumentClass::Option, Some("OPT"))]
2431    #[case(InstrumentClass::Spot, None)]
2432    #[case(InstrumentClass::Swap, None)]
2433    #[case(InstrumentClass::FuturesSpread, None)]
2434    #[case(InstrumentClass::Forward, None)]
2435    #[case(InstrumentClass::Cfd, None)]
2436    #[case(InstrumentClass::Bond, None)]
2437    #[case(InstrumentClass::OptionSpread, None)]
2438    #[case(InstrumentClass::Warrant, None)]
2439    #[case(InstrumentClass::SportsBetting, None)]
2440    #[case(InstrumentClass::BinaryOption, None)]
2441    fn test_instrument_class_parent_suffix(
2442        #[case] class: InstrumentClass,
2443        #[case] expected: Option<&'static str>,
2444    ) {
2445        assert_eq!(class.parent_suffix(), expected);
2446    }
2447
2448    #[rstest]
2449    #[case(InstrumentClass::Future)]
2450    #[case(InstrumentClass::Option)]
2451    fn test_instrument_class_parent_suffix_roundtrip(#[case] class: InstrumentClass) {
2452        let suffix = class.parent_suffix().unwrap();
2453        assert_eq!(InstrumentClass::try_from_parent_suffix(suffix), Some(class));
2454    }
2455
2456    /// Asserts the string and serde contract shared by every enum registered with
2457    /// [`enum_strum_serde`]: `AsRef` and `Display` agree, parsing accepts the emitted name in any
2458    /// ASCII case, and JSON round-trips through that same name.
2459    fn assert_enum_string_contract<T>(type_name: &str)
2460    where
2461        T: IntoEnumIterator
2462            + Copy
2463            + std::fmt::Debug
2464            + PartialEq
2465            + Display
2466            + AsRef<str>
2467            + FromStr
2468            + Serialize
2469            + DeserializeOwned,
2470    {
2471        for variant in T::iter() {
2472            let wire = variant.to_string();
2473            assert_eq!(
2474                variant.as_ref(),
2475                wire,
2476                "{type_name}::{variant:?} must expose the same name through `AsRef` and `Display`",
2477            );
2478            assert_eq!(
2479                T::from_str(&wire).ok(),
2480                Some(variant),
2481                "{type_name} must parse `{wire}` back to {variant:?}",
2482            );
2483            assert_eq!(
2484                T::from_str(&wire.to_ascii_lowercase()).ok(),
2485                Some(variant),
2486                "{type_name} must parse `{wire}` case-insensitively",
2487            );
2488            let json = serde_json::to_string(&variant).unwrap();
2489            assert_eq!(
2490                json,
2491                format!("\"{wire}\""),
2492                "{type_name}::{variant:?} must serialize as its display name",
2493            );
2494            assert_eq!(
2495                serde_json::from_str::<T>(&json).unwrap(),
2496                variant,
2497                "{type_name}::{variant:?} must round-trip through JSON",
2498            );
2499        }
2500    }
2501
2502    #[rstest]
2503    fn test_enum_string_and_serde_contract() {
2504        macro_rules! assert_contract {
2505            ($($t:ty),+ $(,)?) => {
2506                $(assert_enum_string_contract::<$t>(stringify!($t));)+
2507            };
2508        }
2509
2510        assert_contract!(
2511            AccountType,
2512            AggregationSource,
2513            AggressorSide,
2514            AssetClass,
2515            BarAggregation,
2516            BarIntervalType,
2517            BetSide,
2518            BookAction,
2519            BookType,
2520            ContingencyType,
2521            ContinuousFutureAdjustmentType,
2522            CurrencyType,
2523            GreeksConvention,
2524            InstrumentClass,
2525            InstrumentCloseType,
2526            LiquiditySide,
2527            MarketStatus,
2528            MarketStatusAction,
2529            OmsType,
2530            OptionKind,
2531            OrderSide,
2532            OrderStatus,
2533            OrderType,
2534            OtoTriggerMode,
2535            PositionAdjustmentType,
2536            PositionSide,
2537            PriceType,
2538            RecordFlag,
2539            TimeInForce,
2540            TradingState,
2541            TrailingOffsetType,
2542            TriggerType,
2543        );
2544    }
2545
2546    /// Every enum variant's wire name, as `Type::Variant=NAME`.
2547    ///
2548    /// These names are persisted in catalogs and exchanged with adapters and the Python bindings,
2549    /// so a variant rename or a change to strum's casing rules must be a deliberate edit here.
2550    const EXPECTED_WIRE_NAMES: &[&str] = &[
2551        "AccountType::Betting=BETTING",
2552        "AccountType::Cash=CASH",
2553        "AccountType::Margin=MARGIN",
2554        "AccountType::Wallet=WALLET",
2555        "AggregationSource::External=EXTERNAL",
2556        "AggregationSource::Internal=INTERNAL",
2557        "AggressorSide::Buy=BUY",
2558        "AggressorSide::NoAggressor=NO_AGGRESSOR",
2559        "AggressorSide::Sell=SELL",
2560        "AssetClass::Alternative=ALTERNATIVE",
2561        "AssetClass::Commodity=COMMODITY",
2562        "AssetClass::Cryptocurrency=CRYPTOCURRENCY",
2563        "AssetClass::Debt=DEBT",
2564        "AssetClass::Equity=EQUITY",
2565        "AssetClass::FX=FX",
2566        "AssetClass::Index=INDEX",
2567        "BarAggregation::Day=DAY",
2568        "BarAggregation::Hour=HOUR",
2569        "BarAggregation::Millisecond=MILLISECOND",
2570        "BarAggregation::Minute=MINUTE",
2571        "BarAggregation::Month=MONTH",
2572        "BarAggregation::Renko=RENKO",
2573        "BarAggregation::Second=SECOND",
2574        "BarAggregation::Tick=TICK",
2575        "BarAggregation::TickImbalance=TICK_IMBALANCE",
2576        "BarAggregation::TickRuns=TICK_RUNS",
2577        "BarAggregation::Value=VALUE",
2578        "BarAggregation::ValueImbalance=VALUE_IMBALANCE",
2579        "BarAggregation::ValueRuns=VALUE_RUNS",
2580        "BarAggregation::Volume=VOLUME",
2581        "BarAggregation::VolumeImbalance=VOLUME_IMBALANCE",
2582        "BarAggregation::VolumeRuns=VOLUME_RUNS",
2583        "BarAggregation::Week=WEEK",
2584        "BarAggregation::Year=YEAR",
2585        "BarIntervalType::LeftOpen=LEFT_OPEN",
2586        "BarIntervalType::RightOpen=RIGHT_OPEN",
2587        "BetSide::Back=BACK",
2588        "BetSide::Lay=LAY",
2589        "BookAction::Add=ADD",
2590        "BookAction::Clear=CLEAR",
2591        "BookAction::Delete=DELETE",
2592        "BookAction::Update=UPDATE",
2593        "BookType::L1_MBP=L1_MBP",
2594        "BookType::L2_MBP=L2_MBP",
2595        "BookType::L3_MBO=L3_MBO",
2596        "ContingencyType::Oco=OCO",
2597        "ContingencyType::Oto=OTO",
2598        "ContingencyType::Ouo=OUO",
2599        "ContinuousFutureAdjustmentType::BackwardRatio=BACKWARD_RATIO",
2600        "ContinuousFutureAdjustmentType::BackwardSpread=BACKWARD_SPREAD",
2601        "ContinuousFutureAdjustmentType::ForwardRatio=FORWARD_RATIO",
2602        "ContinuousFutureAdjustmentType::ForwardSpread=FORWARD_SPREAD",
2603        "CurrencyType::CommodityBacked=COMMODITY_BACKED",
2604        "CurrencyType::Crypto=CRYPTO",
2605        "CurrencyType::Fiat=FIAT",
2606        "GreeksConvention::BlackScholes=BLACK_SCHOLES",
2607        "GreeksConvention::PriceAdjusted=PRICE_ADJUSTED",
2608        "InstrumentClass::BinaryOption=BINARY_OPTION",
2609        "InstrumentClass::Bond=BOND",
2610        "InstrumentClass::Cfd=CFD",
2611        "InstrumentClass::Forward=FORWARD",
2612        "InstrumentClass::Future=FUTURE",
2613        "InstrumentClass::FuturesSpread=FUTURES_SPREAD",
2614        "InstrumentClass::Option=OPTION",
2615        "InstrumentClass::OptionSpread=OPTION_SPREAD",
2616        "InstrumentClass::SportsBetting=SPORTS_BETTING",
2617        "InstrumentClass::Spot=SPOT",
2618        "InstrumentClass::Swap=SWAP",
2619        "InstrumentClass::Warrant=WARRANT",
2620        "InstrumentCloseType::ContractExpired=CONTRACT_EXPIRED",
2621        "InstrumentCloseType::EndOfSession=END_OF_SESSION",
2622        "LiquiditySide::Maker=MAKER",
2623        "LiquiditySide::NoLiquiditySide=NO_LIQUIDITY_SIDE",
2624        "LiquiditySide::Taker=TAKER",
2625        "MarketStatus::Closed=CLOSED",
2626        "MarketStatus::Halted=HALTED",
2627        "MarketStatus::NotAvailable=NOT_AVAILABLE",
2628        "MarketStatus::Open=OPEN",
2629        "MarketStatus::Paused=PAUSED",
2630        "MarketStatus::Suspended=SUSPENDED",
2631        "MarketStatusAction::Close=CLOSE",
2632        "MarketStatusAction::Cross=CROSS",
2633        "MarketStatusAction::Halt=HALT",
2634        "MarketStatusAction::NewPriceIndication=NEW_PRICE_INDICATION",
2635        "MarketStatusAction::None=NONE",
2636        "MarketStatusAction::NotAvailableForTrading=NOT_AVAILABLE_FOR_TRADING",
2637        "MarketStatusAction::Pause=PAUSE",
2638        "MarketStatusAction::PostClose=POST_CLOSE",
2639        "MarketStatusAction::PreClose=PRE_CLOSE",
2640        "MarketStatusAction::PreCross=PRE_CROSS",
2641        "MarketStatusAction::PreOpen=PRE_OPEN",
2642        "MarketStatusAction::Quoting=QUOTING",
2643        "MarketStatusAction::Rotation=ROTATION",
2644        "MarketStatusAction::ShortSellRestrictionChange=SHORT_SELL_RESTRICTION_CHANGE",
2645        "MarketStatusAction::Suspend=SUSPEND",
2646        "MarketStatusAction::Trading=TRADING",
2647        "OmsType::Hedging=HEDGING",
2648        "OmsType::Netting=NETTING",
2649        "OmsType::Unspecified=UNSPECIFIED",
2650        "OptionKind::Call=CALL",
2651        "OptionKind::Put=PUT",
2652        "OrderSide::Buy=BUY",
2653        "OrderSide::Sell=SELL",
2654        "OrderStatus::Accepted=ACCEPTED",
2655        "OrderStatus::Canceled=CANCELED",
2656        "OrderStatus::Denied=DENIED",
2657        "OrderStatus::Emulated=EMULATED",
2658        "OrderStatus::Expired=EXPIRED",
2659        "OrderStatus::Filled=FILLED",
2660        "OrderStatus::Initialized=INITIALIZED",
2661        "OrderStatus::PartiallyFilled=PARTIALLY_FILLED",
2662        "OrderStatus::PendingCancel=PENDING_CANCEL",
2663        "OrderStatus::PendingUpdate=PENDING_UPDATE",
2664        "OrderStatus::Rejected=REJECTED",
2665        "OrderStatus::Released=RELEASED",
2666        "OrderStatus::Submitted=SUBMITTED",
2667        "OrderStatus::Triggered=TRIGGERED",
2668        "OrderStatus::Voided=VOIDED",
2669        "OrderType::Limit=LIMIT",
2670        "OrderType::LimitIfTouched=LIMIT_IF_TOUCHED",
2671        "OrderType::Market=MARKET",
2672        "OrderType::MarketIfTouched=MARKET_IF_TOUCHED",
2673        "OrderType::MarketToLimit=MARKET_TO_LIMIT",
2674        "OrderType::StopLimit=STOP_LIMIT",
2675        "OrderType::StopMarket=STOP_MARKET",
2676        "OrderType::TrailingStopLimit=TRAILING_STOP_LIMIT",
2677        "OrderType::TrailingStopMarket=TRAILING_STOP_MARKET",
2678        "OtoTriggerMode::Full=FULL",
2679        "OtoTriggerMode::Partial=PARTIAL",
2680        "PositionAdjustmentType::Commission=COMMISSION",
2681        "PositionAdjustmentType::Funding=FUNDING",
2682        "PositionSide::Flat=FLAT",
2683        "PositionSide::Long=LONG",
2684        "PositionSide::Short=SHORT",
2685        "PriceType::Ask=ASK",
2686        "PriceType::Bid=BID",
2687        "PriceType::Last=LAST",
2688        "PriceType::Mark=MARK",
2689        "PriceType::Mid=MID",
2690        "RecordFlag::F_LAST=F_LAST",
2691        "RecordFlag::F_MBP=F_MBP",
2692        "RecordFlag::F_SNAPSHOT=F_SNAPSHOT",
2693        "RecordFlag::F_TOB=F_TOB",
2694        "RecordFlag::RESERVED_1=RESERVED_1",
2695        "RecordFlag::RESERVED_2=RESERVED_2",
2696        "TimeInForce::AtTheClose=AT_THE_CLOSE",
2697        "TimeInForce::AtTheOpen=AT_THE_OPEN",
2698        "TimeInForce::Day=DAY",
2699        "TimeInForce::Fok=FOK",
2700        "TimeInForce::Gtc=GTC",
2701        "TimeInForce::Gtd=GTD",
2702        "TimeInForce::Ioc=IOC",
2703        "TradingState::Active=ACTIVE",
2704        "TradingState::Halted=HALTED",
2705        "TradingState::Reducing=REDUCING",
2706        "TrailingOffsetType::BasisPoints=BASIS_POINTS",
2707        "TrailingOffsetType::Price=PRICE",
2708        "TrailingOffsetType::PriceTier=PRICE_TIER",
2709        "TrailingOffsetType::Ticks=TICKS",
2710        "TriggerType::BidAsk=BID_ASK",
2711        "TriggerType::Default=DEFAULT",
2712        "TriggerType::DoubleBidAsk=DOUBLE_BID_ASK",
2713        "TriggerType::DoubleLast=DOUBLE_LAST",
2714        "TriggerType::IndexPrice=INDEX_PRICE",
2715        "TriggerType::LastOrBidAsk=LAST_OR_BID_ASK",
2716        "TriggerType::LastPrice=LAST_PRICE",
2717        "TriggerType::MarkPrice=MARK_PRICE",
2718        "TriggerType::MidPoint=MID_POINT",
2719    ];
2720
2721    #[rstest]
2722    fn test_enum_wire_names_are_stable() {
2723        let mut actual = Vec::new();
2724        macro_rules! collect_wire_names {
2725            ($($t:ty),+ $(,)?) => {
2726                $(for variant in <$t>::iter() {
2727                    actual.push(format!("{}::{variant:?}={variant}", stringify!($t)));
2728                })+
2729            };
2730        }
2731
2732        collect_wire_names!(
2733            AccountType,
2734            AggregationSource,
2735            AggressorSide,
2736            AssetClass,
2737            BarAggregation,
2738            BarIntervalType,
2739            BetSide,
2740            BookAction,
2741            BookType,
2742            ContingencyType,
2743            ContinuousFutureAdjustmentType,
2744            CurrencyType,
2745            GreeksConvention,
2746            InstrumentClass,
2747            InstrumentCloseType,
2748            LiquiditySide,
2749            MarketStatus,
2750            MarketStatusAction,
2751            OmsType,
2752            OptionKind,
2753            OrderSide,
2754            OrderStatus,
2755            OrderType,
2756            OtoTriggerMode,
2757            PositionAdjustmentType,
2758            PositionSide,
2759            PriceType,
2760            RecordFlag,
2761            TimeInForce,
2762            TradingState,
2763            TrailingOffsetType,
2764            TriggerType,
2765        );
2766
2767        actual.sort();
2768
2769        assert_eq!(
2770            actual.len(),
2771            EXPECTED_WIRE_NAMES.len(),
2772            "variant count changed; update `EXPECTED_WIRE_NAMES`",
2773        );
2774
2775        for (got, expected) in actual.iter().zip(EXPECTED_WIRE_NAMES) {
2776            assert_eq!(got, expected);
2777        }
2778    }
2779
2780    /// Pins each numeric discriminant to its variant with literal values.
2781    ///
2782    /// These numbers are written into Parquet catalogs and SBE payloads, and `MarketStatusAction`
2783    /// additionally mirrors Databento's external DBN `StatusMsg.action` numbering. The conversions
2784    /// delegate to the derived `from_repr`, so comparing them against `from_repr` would be a
2785    /// tautology: only literal expected values catch a renumbered variant, which would otherwise
2786    /// change encode and decode together and silently misread already-persisted data.
2787    macro_rules! assert_numeric_mapping {
2788        ($t:ty, $from:ident, $width:ty, $($value:literal => $variant:expr),+ $(,)?) => {{
2789            let valid: &[$width] = &[$($value),+];
2790            $(
2791                assert_eq!(
2792                    <$t>::$from($value),
2793                    Some($variant),
2794                    "{}::{}({}) must map to the pinned variant",
2795                    stringify!($t),
2796                    stringify!($from),
2797                    $value,
2798                );
2799            )+
2800
2801            for value in <$width>::MIN..=<$width>::MAX {
2802                if !valid.contains(&value) {
2803                    assert_eq!(
2804                        <$t>::$from(value),
2805                        None,
2806                        "{} must reject {value}",
2807                        stringify!($t),
2808                    );
2809                }
2810            }
2811        }};
2812    }
2813
2814    #[rstest]
2815    fn test_from_u8_pins_numeric_discriminants() {
2816        assert_numeric_mapping!(
2817            AggressorSide, from_u8, u8,
2818            0 => AggressorSide::NoAggressor,
2819            1 => AggressorSide::Buy,
2820            2 => AggressorSide::Sell,
2821        );
2822        assert_numeric_mapping!(
2823            AssetClass, from_u8, u8,
2824            1 => AssetClass::FX,
2825            2 => AssetClass::Equity,
2826            3 => AssetClass::Commodity,
2827            4 => AssetClass::Debt,
2828            5 => AssetClass::Index,
2829            6 => AssetClass::Cryptocurrency,
2830            7 => AssetClass::Alternative,
2831        );
2832        assert_numeric_mapping!(
2833            BookAction, from_u8, u8,
2834            1 => BookAction::Add,
2835            2 => BookAction::Update,
2836            3 => BookAction::Delete,
2837            4 => BookAction::Clear,
2838        );
2839        assert_numeric_mapping!(
2840            BookType, from_u8, u8,
2841            1 => BookType::L1_MBP,
2842            2 => BookType::L2_MBP,
2843            3 => BookType::L3_MBO,
2844        );
2845        assert_numeric_mapping!(
2846            InstrumentCloseType, from_u8, u8,
2847            1 => InstrumentCloseType::EndOfSession,
2848            2 => InstrumentCloseType::ContractExpired,
2849        );
2850        assert_numeric_mapping!(
2851            PositionAdjustmentType, from_u8, u8,
2852            1 => PositionAdjustmentType::Commission,
2853            2 => PositionAdjustmentType::Funding,
2854        );
2855    }
2856
2857    #[rstest]
2858    fn test_market_status_action_from_u16_pins_numeric_discriminants() {
2859        assert_numeric_mapping!(
2860            MarketStatusAction, from_u16, u16,
2861            0 => MarketStatusAction::None,
2862            1 => MarketStatusAction::PreOpen,
2863            2 => MarketStatusAction::PreCross,
2864            3 => MarketStatusAction::Quoting,
2865            4 => MarketStatusAction::Cross,
2866            5 => MarketStatusAction::Rotation,
2867            6 => MarketStatusAction::NewPriceIndication,
2868            7 => MarketStatusAction::Trading,
2869            8 => MarketStatusAction::Halt,
2870            9 => MarketStatusAction::Pause,
2871            10 => MarketStatusAction::Suspend,
2872            11 => MarketStatusAction::PreClose,
2873            12 => MarketStatusAction::Close,
2874            13 => MarketStatusAction::PostClose,
2875            14 => MarketStatusAction::ShortSellRestrictionChange,
2876            15 => MarketStatusAction::NotAvailableForTrading,
2877        );
2878    }
2879
2880    #[rstest]
2881    #[case(OrderStatus::Initialized, false, false, false, false)]
2882    #[case(OrderStatus::Denied, false, true, false, false)]
2883    #[case(OrderStatus::Emulated, false, false, false, false)]
2884    #[case(OrderStatus::Released, false, false, false, false)]
2885    #[case(OrderStatus::Submitted, false, false, false, true)]
2886    #[case(OrderStatus::Accepted, true, false, true, false)]
2887    #[case(OrderStatus::Rejected, false, true, false, false)]
2888    #[case(OrderStatus::Canceled, false, true, false, false)]
2889    #[case(OrderStatus::Expired, false, true, false, false)]
2890    #[case(OrderStatus::Triggered, true, false, true, false)]
2891    #[case(OrderStatus::PendingUpdate, true, false, true, true)]
2892    #[case(OrderStatus::PendingCancel, true, false, false, true)]
2893    #[case(OrderStatus::PartiallyFilled, true, false, true, false)]
2894    #[case(OrderStatus::Filled, false, true, false, false)]
2895    #[case(OrderStatus::Voided, false, true, false, false)]
2896    fn test_order_status_predicates(
2897        #[case] status: OrderStatus,
2898        #[case] expected_open: bool,
2899        #[case] expected_closed: bool,
2900        #[case] expected_cancellable: bool,
2901        #[case] expected_inflight: bool,
2902    ) {
2903        assert_eq!(status.is_open(), expected_open, "{status} is_open");
2904        assert_eq!(status.is_closed(), expected_closed, "{status} is_closed");
2905        assert_eq!(
2906            status.is_cancellable(),
2907            expected_cancellable,
2908            "{status} is_cancellable",
2909        );
2910        assert_eq!(
2911            status.is_inflight(),
2912            expected_inflight,
2913            "{status} is_inflight"
2914        );
2915    }
2916
2917    #[rstest]
2918    fn test_order_status_predicates_hold_across_all_variants() {
2919        for status in OrderStatus::iter() {
2920            assert!(
2921                !(status.is_open() && status.is_closed()),
2922                "{status} cannot be both open and closed",
2923            );
2924            assert!(
2925                !status.is_cancellable() || status.is_open(),
2926                "{status} must be open to be cancellable",
2927            );
2928            assert!(
2929                !(status.is_inflight() && status.is_closed()),
2930                "{status} cannot be both in-flight and closed",
2931            );
2932        }
2933    }
2934
2935    #[rstest]
2936    #[case(OrderSide::Buy, OrderSide::Sell)]
2937    #[case(OrderSide::Sell, OrderSide::Buy)]
2938    fn test_order_side_opposite(#[case] side: OrderSide, #[case] expected: OrderSide) {
2939        assert_eq!(side.opposite(), expected);
2940        assert_eq!(side.opposite().opposite(), side);
2941    }
2942
2943    #[rstest]
2944    #[case(BetSide::Back, BetSide::Lay)]
2945    #[case(BetSide::Lay, BetSide::Back)]
2946    fn test_bet_side_opposite(#[case] side: BetSide, #[case] expected: BetSide) {
2947        assert_eq!(side.opposite(), expected);
2948        assert_eq!(side.opposite().opposite(), side);
2949    }
2950
2951    #[rstest]
2952    #[case(OrderSide::Buy, BetSide::Back)]
2953    #[case(OrderSide::Sell, BetSide::Lay)]
2954    fn test_bet_side_from_order_side(#[case] side: OrderSide, #[case] expected: BetSide) {
2955        assert_eq!(BetSide::from(side), expected);
2956    }
2957
2958    #[rstest]
2959    #[case(RecordFlag::F_LAST, 0b0000_0000, false)]
2960    #[case(RecordFlag::F_LAST, 0b1000_0000, true)]
2961    #[case(RecordFlag::F_LAST, 0b0111_1111, false)]
2962    #[case(RecordFlag::F_TOB, 0b1100_0000, true)]
2963    #[case(RecordFlag::F_TOB, 0b1000_0000, false)]
2964    #[case(RecordFlag::F_SNAPSHOT, 0b0010_0000, true)]
2965    #[case(RecordFlag::F_SNAPSHOT, 0b1101_1111, false)]
2966    #[case(RecordFlag::F_MBP, 0b0001_0000, true)]
2967    #[case(RecordFlag::RESERVED_2, 0b0000_1000, true)]
2968    #[case(RecordFlag::RESERVED_1, 0b0000_0100, true)]
2969    #[case(RecordFlag::RESERVED_1, 0b0000_1000, false)]
2970    #[case(RecordFlag::F_LAST, u8::MAX, true)]
2971    fn test_record_flag_matches(
2972        #[case] flag: RecordFlag,
2973        #[case] value: u8,
2974        #[case] expected: bool,
2975    ) {
2976        assert_eq!(flag.matches(value), expected);
2977    }
2978
2979    #[rstest]
2980    fn test_record_flag_matches_only_its_own_bit() {
2981        for flag in RecordFlag::iter() {
2982            let bit = flag as u8;
2983            assert_eq!(bit.count_ones(), 1, "{flag} must occupy exactly one bit");
2984            assert!(flag.matches(bit), "{flag} must match its own bit");
2985            assert!(
2986                !flag.matches(!bit),
2987                "{flag} must not match the inverse mask"
2988            );
2989        }
2990    }
2991
2992    #[rstest]
2993    #[case(InstrumentClass::Future, true)]
2994    #[case(InstrumentClass::FuturesSpread, true)]
2995    #[case(InstrumentClass::Option, true)]
2996    #[case(InstrumentClass::OptionSpread, true)]
2997    #[case(InstrumentClass::Spot, false)]
2998    #[case(InstrumentClass::Swap, false)]
2999    #[case(InstrumentClass::Forward, false)]
3000    #[case(InstrumentClass::Cfd, false)]
3001    #[case(InstrumentClass::Bond, false)]
3002    #[case(InstrumentClass::Warrant, false)]
3003    #[case(InstrumentClass::SportsBetting, false)]
3004    #[case(InstrumentClass::BinaryOption, false)]
3005    fn test_instrument_class_has_expiration(
3006        #[case] class: InstrumentClass,
3007        #[case] expected: bool,
3008    ) {
3009        assert_eq!(class.has_expiration(), expected);
3010    }
3011
3012    #[rstest]
3013    fn test_optional_sides_serde_round_trips_present_values() {
3014        let value = OptionalSides {
3015            order: Some(OrderSide::Sell),
3016            position: Some(PositionSide::Short),
3017        };
3018
3019        let json = serde_json::to_string(&value).unwrap();
3020
3021        assert_eq!(json, r#"{"order":"SELL","position":"SHORT"}"#);
3022        assert_eq!(serde_json::from_str::<OptionalSides>(&json).unwrap(), value);
3023    }
3024
3025    #[rstest]
3026    #[case(r#"{"order":"no_order_side","position":"FLAT"}"#)]
3027    #[case(r#"{"order":"No_Order_Side","position":"FLAT"}"#)]
3028    fn test_optional_sides_serde_accepts_none_token_in_any_case(#[case] json: &str) {
3029        let decoded: OptionalSides = serde_json::from_str(json).unwrap();
3030
3031        assert_eq!(
3032            decoded,
3033            OptionalSides {
3034                order: None,
3035                position: Some(PositionSide::Flat),
3036            },
3037        );
3038    }
3039
3040    #[rstest]
3041    #[case(r#"{"order":"INVALID","position":"FLAT"}"#)]
3042    #[case(r#"{"order":5,"position":"FLAT"}"#)]
3043    #[case(r#"{"order":"BUY","position":"INVALID"}"#)]
3044    #[case(r#"{"order":"BUY","position":true}"#)]
3045    fn test_optional_sides_serde_rejects_invalid_values(#[case] json: &str) {
3046        assert!(serde_json::from_str::<OptionalSides>(json).is_err());
3047    }
3048}