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