Skip to main content

nautilus_derive/common/
enums.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Wire-format enums for the Derive REST/WS APIs.
17//!
18//! Variants are sourced from the venue's own Rust SDK at
19//! [`derivexyz/cockpit`](https://github.com/derivexyz/cockpit/tree/master/orderbook-types/src),
20//! which is generated from Derive's JSON-RPC schemas. Variant wire strings are
21//! case-sensitive and must round-trip byte-equivalent.
22
23use serde::{Deserialize, Serialize};
24use strum::{AsRefStr, Display, EnumIter, EnumString};
25
26/// Derive network selector. Drives REST/WS URLs and per-network protocol
27/// constants (`DOMAIN_SEPARATOR`, module addresses).
28#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
29#[serde(rename_all = "lowercase")]
30#[cfg_attr(
31    feature = "python",
32    pyo3::pyclass(
33        module = "nautilus_trader.core.nautilus_pyo3.derive",
34        eq,
35        eq_int,
36        frozen,
37        from_py_object,
38        rename_all = "SCREAMING_SNAKE_CASE"
39    )
40)]
41#[cfg_attr(
42    feature = "python",
43    pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.adapters.derive")
44)]
45pub enum DeriveEnvironment {
46    /// Production environment.
47    #[default]
48    Mainnet,
49    /// Public testnet environment.
50    Testnet,
51}
52
53impl DeriveEnvironment {
54    #[must_use]
55    pub const fn is_testnet(self) -> bool {
56        matches!(self, Self::Testnet)
57    }
58}
59
60/// Wire-level instrument type returned by `public/get_instruments` and used as
61/// the `instrument_type` filter on listing endpoints and WS channel names like
62/// `trades.{instrument_type}.{currency}`.
63#[derive(
64    Debug,
65    Clone,
66    Copy,
67    PartialEq,
68    Eq,
69    Hash,
70    Serialize,
71    Deserialize,
72    Display,
73    EnumString,
74    AsRefStr,
75    EnumIter,
76)]
77#[serde(rename_all = "lowercase")]
78#[strum(serialize_all = "lowercase")]
79pub enum DeriveInstrumentType {
80    /// ERC-20 spot asset.
81    Erc20,
82    /// Option contract.
83    Option,
84    /// Perpetual swap.
85    Perp,
86}
87
88/// Wire-level asset type as returned in subaccount and collateral responses
89/// (`asset_type` field on `private/get_subaccount`, `public/get_assets`,
90/// `private/get_collaterals`). The variants line up byte-for-byte with
91/// [`DeriveInstrumentType`] but the venue treats it as a distinct field.
92#[derive(
93    Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Display, EnumString, EnumIter,
94)]
95#[serde(rename_all = "lowercase")]
96#[strum(serialize_all = "lowercase")]
97pub enum DeriveAssetType {
98    /// ERC-20 collateral asset.
99    Erc20,
100    /// Option position asset.
101    Option,
102    /// Perpetual position asset.
103    Perp,
104}
105
106/// Order side (`direction` field on the venue API).
107#[derive(
108    Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Display, EnumString, AsRefStr,
109)]
110#[serde(rename_all = "lowercase")]
111#[strum(serialize_all = "lowercase")]
112pub enum DeriveOrderSide {
113    Buy,
114    Sell,
115}
116
117/// Order type accepted by `private/order` and `private/trigger_order`.
118#[derive(
119    Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Display, EnumString, AsRefStr,
120)]
121#[serde(rename_all = "lowercase")]
122#[strum(serialize_all = "lowercase")]
123pub enum DeriveOrderType {
124    Limit,
125    Market,
126}
127
128/// Trigger side accepted by `private/trigger_order`.
129#[derive(
130    Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Display, EnumString, AsRefStr,
131)]
132#[serde(rename_all = "lowercase")]
133#[strum(serialize_all = "lowercase")]
134pub enum DeriveTriggerType {
135    /// Stop-loss trigger.
136    Stoploss,
137    /// Take-profit trigger.
138    Takeprofit,
139}
140
141/// Trigger price source accepted by `private/trigger_order`.
142#[derive(
143    Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Display, EnumString, AsRefStr,
144)]
145#[serde(rename_all = "lowercase")]
146#[strum(serialize_all = "lowercase")]
147pub enum DeriveTriggerPriceType {
148    /// Derive mark price.
149    Mark,
150    /// Derive index price. Present in schemas, but currently rejected by the venue.
151    Index,
152}
153
154/// Order lifecycle status reported by `private/get_orders`,
155/// `private/get_order_history`, and the WS `{subaccount_id}.orders` channel.
156#[derive(
157    Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Display, EnumString, AsRefStr,
158)]
159#[serde(rename_all = "snake_case")]
160#[strum(serialize_all = "snake_case")]
161pub enum DeriveOrderStatus {
162    /// Resting in the book or accepted by the matching engine.
163    Open,
164    /// Fully filled.
165    Filled,
166    /// Rejected by matching engine or risk checks.
167    Rejected,
168    /// Cancelled by user, system, or expiry; see `cancel_reason`.
169    Cancelled,
170    /// `signature_expiry_sec` was reached.
171    Expired,
172    /// Trigger order saved but not yet fired.
173    Untriggered,
174    /// Algorithmic parent order active on the venue.
175    AlgoActive,
176}
177
178/// Time-in-force flag accepted by `private/order`.
179#[derive(
180    Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Display, EnumString, AsRefStr,
181)]
182#[serde(rename_all = "snake_case")]
183#[strum(serialize_all = "snake_case")]
184pub enum DeriveTimeInForce {
185    /// Good-till-cancelled.
186    Gtc,
187    /// Post-only: reject if it would cross the spread.
188    PostOnly,
189    /// Fill-or-kill: fill in full immediately or cancel.
190    Fok,
191    /// Immediate-or-cancel: fill any tradable quantity, cancel the rest.
192    Ioc,
193}
194
195/// Option kind parsed from the `instrument_name` suffix (e.g. `-C` or `-P`).
196#[derive(
197    Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Display, EnumString, AsRefStr,
198)]
199pub enum DeriveOptionKind {
200    /// Call option.
201    #[serde(rename = "C")]
202    #[strum(serialize = "C")]
203    Call,
204    /// Put option.
205    #[serde(rename = "P")]
206    #[strum(serialize = "P")]
207    Put,
208}
209
210/// Cancel reason attached to a cancelled order. Empty string corresponds to no
211/// reason (order is still open or finished by another transition).
212///
213/// `ValidationFailed` is present in the SDK enum but absent from the public
214/// JSON schema; included here for round-trip fidelity against SDK responses.
215#[derive(
216    Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Display, EnumString, AsRefStr,
217)]
218pub enum DeriveOrderCancelReason {
219    /// No cancel reason (open or finished without cancellation).
220    #[serde(rename = "")]
221    #[strum(serialize = "")]
222    Empty,
223    /// Explicit cancel via `private/cancel*`.
224    #[serde(rename = "user_request")]
225    #[strum(serialize = "user_request")]
226    UserRequest,
227    /// Market Maker Protection tripped.
228    #[serde(rename = "mmp_trigger")]
229    #[strum(serialize = "mmp_trigger")]
230    MmpTrigger,
231    /// Margin check failed at matching.
232    #[serde(rename = "insufficient_margin")]
233    #[strum(serialize = "insufficient_margin")]
234    InsufficientMargin,
235    /// Signed `max_fee` is below the venue's current required fee.
236    #[serde(rename = "signed_max_fee_too_low")]
237    #[strum(serialize = "signed_max_fee_too_low")]
238    SignedMaxFeeTooLow,
239    /// WS disconnect cancelled the session with cancel-on-disconnect enabled.
240    #[serde(rename = "cancel_on_disconnect")]
241    #[strum(serialize = "cancel_on_disconnect")]
242    CancelOnDisconnect,
243    /// Remainder of an IOC or market order auto-cancelled.
244    #[serde(rename = "ioc_or_market_partial_fill")]
245    #[strum(serialize = "ioc_or_market_partial_fill")]
246    IocOrMarketPartialFill,
247    /// Signing session key was deregistered.
248    #[serde(rename = "session_key_deregistered")]
249    #[strum(serialize = "session_key_deregistered")]
250    SessionKeyDeregistered,
251    /// Subaccount fully withdrawn.
252    #[serde(rename = "subaccount_withdrawn")]
253    #[strum(serialize = "subaccount_withdrawn")]
254    SubaccountWithdrawn,
255    /// Cancelled by compliance action.
256    #[serde(rename = "compliance")]
257    #[strum(serialize = "compliance")]
258    Compliance,
259    /// Trigger worker could not submit the child order.
260    #[serde(rename = "trigger_failed")]
261    #[strum(serialize = "trigger_failed")]
262    TriggerFailed,
263    /// Pre-engine validation failure (SDK-only, not in the public schema).
264    #[serde(rename = "validation_failed")]
265    #[strum(serialize = "validation_failed")]
266    ValidationFailed,
267    /// Algorithmic parent completed after finishing its slices.
268    #[serde(rename = "algo_completed")]
269    #[strum(serialize = "algo_completed")]
270    AlgoCompleted,
271    /// Post-only order would cross the market.
272    #[serde(rename = "Post only order cannot cross the market")]
273    #[strum(serialize = "Post only order cannot cross the market")]
274    PostOnlyCrossMarket,
275}
276
277/// Cancel reason attached to a cancelled RFQ quote. Similar to
278/// [`DeriveOrderCancelReason`] but emits `rfq_no_longer_open` instead of
279/// `ioc_or_market_partial_fill`.
280#[derive(
281    Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Display, EnumString, AsRefStr,
282)]
283pub enum DeriveRfqCancelReason {
284    /// No cancel reason.
285    #[serde(rename = "")]
286    #[strum(serialize = "")]
287    Empty,
288    /// Explicit cancel via `private/cancel_quote`.
289    #[serde(rename = "user_request")]
290    #[strum(serialize = "user_request")]
291    UserRequest,
292    /// Margin check failed at quote execution.
293    #[serde(rename = "insufficient_margin")]
294    #[strum(serialize = "insufficient_margin")]
295    InsufficientMargin,
296    /// Signed `max_fee` is below the venue's current required fee.
297    #[serde(rename = "signed_max_fee_too_low")]
298    #[strum(serialize = "signed_max_fee_too_low")]
299    SignedMaxFeeTooLow,
300    /// MMP tripped.
301    #[serde(rename = "mmp_trigger")]
302    #[strum(serialize = "mmp_trigger")]
303    MmpTrigger,
304    /// WS disconnect cancelled the session with cancel-on-disconnect enabled.
305    #[serde(rename = "cancel_on_disconnect")]
306    #[strum(serialize = "cancel_on_disconnect")]
307    CancelOnDisconnect,
308    /// Signing session key was deregistered.
309    #[serde(rename = "session_key_deregistered")]
310    #[strum(serialize = "session_key_deregistered")]
311    SessionKeyDeregistered,
312    /// Subaccount fully withdrawn.
313    #[serde(rename = "subaccount_withdrawn")]
314    #[strum(serialize = "subaccount_withdrawn")]
315    SubaccountWithdrawn,
316    /// Underlying RFQ was filled or cancelled.
317    #[serde(rename = "rfq_no_longer_open")]
318    #[strum(serialize = "rfq_no_longer_open")]
319    RfqNoLongerOpen,
320    /// Cancelled by compliance action.
321    #[serde(rename = "compliance")]
322    #[strum(serialize = "compliance")]
323    Compliance,
324}
325
326/// Role of the user in a trade (`liquidity_role` field).
327#[derive(
328    Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Display, EnumString, AsRefStr,
329)]
330#[serde(rename_all = "lowercase")]
331#[strum(serialize_all = "lowercase")]
332pub enum DeriveLiquidityRole {
333    /// Resting side of the trade.
334    Maker,
335    /// Aggressing side of the trade.
336    Taker,
337}
338
339/// Blockchain transaction lifecycle status (`tx_status` field on trades,
340/// deposits, withdrawals, transfers, and liquidation history).
341#[derive(
342    Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Display, EnumString, AsRefStr,
343)]
344#[serde(rename_all = "lowercase")]
345#[strum(serialize_all = "lowercase")]
346pub enum DeriveTxStatus {
347    /// Transaction queued, not yet broadcast.
348    Requested,
349    /// Broadcast on-chain, awaiting confirmation.
350    Pending,
351    /// Confirmed and applied.
352    Settled,
353    /// Reverted on-chain.
354    Reverted,
355    /// Superseded or dropped without being applied.
356    Ignored,
357}
358
359/// Subaccount margining mode. Returned by `private/get_subaccount`. Subaccount
360/// creation accepts only [`Sm`](Self::Sm) and [`Pm`](Self::Pm); `Pm2` appears
361/// only as a read value when an account has migrated to PMRM v2.
362#[derive(
363    Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Display, EnumString, AsRefStr,
364)]
365pub enum DeriveMarginType {
366    /// Standard Margin.
367    #[serde(rename = "SM")]
368    #[strum(serialize = "SM")]
369    Sm,
370    /// Portfolio Margin (legacy PMRM).
371    #[serde(rename = "PM")]
372    #[strum(serialize = "PM")]
373    Pm,
374    /// Portfolio Margin v2 (PMRM_2); read-only on `private/get_subaccount`.
375    #[serde(rename = "PM2")]
376    #[strum(serialize = "PM2")]
377    Pm2,
378}
379
380/// Liquidation auction type returned by `private/get_liquidation_history`.
381#[derive(
382    Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Display, EnumString, AsRefStr,
383)]
384#[serde(rename_all = "lowercase")]
385#[strum(serialize_all = "lowercase")]
386pub enum DeriveAuctionType {
387    /// Auction on a still-solvent account.
388    Solvent,
389    /// Auction on an insolvent account.
390    Insolvent,
391}
392
393/// Liquidation auction state.
394#[derive(
395    Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Display, EnumString, AsRefStr,
396)]
397#[serde(rename_all = "lowercase")]
398#[strum(serialize_all = "lowercase")]
399pub enum DeriveAuctionState {
400    /// Auction is active.
401    Ongoing,
402    /// Auction has concluded.
403    Ended,
404}
405
406/// Notification acknowledgement state on `private/get_notifications` and
407/// `private/update_notifications`.
408#[derive(
409    Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Display, EnumString, AsRefStr,
410)]
411#[serde(rename_all = "lowercase")]
412#[strum(serialize_all = "lowercase")]
413pub enum DeriveNotificationStatus {
414    /// Not yet viewed.
415    Unseen,
416    /// Viewed but not dismissed.
417    Seen,
418    /// Dismissed from feed.
419    Hidden,
420}
421
422/// Notification category. The `Types` variant is emitted by the upstream
423/// codegen and surfaces in the wire enum; treat it as a generic placeholder
424/// rather than a meaningful category.
425#[derive(
426    Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Display, EnumString, AsRefStr,
427)]
428#[serde(rename_all = "lowercase")]
429#[strum(serialize_all = "lowercase")]
430pub enum DeriveNotificationType {
431    /// Deposit event.
432    Deposit,
433    /// Withdrawal event.
434    Withdraw,
435    /// ERC-20 transfer between subaccounts.
436    Transfer,
437    /// Trade execution.
438    Trade,
439    /// Option or perp settlement.
440    Settlement,
441    /// Liquidation event.
442    Liquidation,
443    /// Codegen placeholder; not a stable category.
444    Types,
445}
446
447/// Cause of a balance row update on the WS `{subaccount_id}.balances`
448/// channel.
449#[derive(
450    Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Display, EnumString, AsRefStr,
451)]
452#[serde(rename_all = "snake_case")]
453#[strum(serialize_all = "snake_case")]
454pub enum DeriveBalanceUpdateType {
455    /// Balance changed because of a trade fill.
456    Trade,
457    /// Asset deposited into the subaccount.
458    AssetDeposit,
459    /// Asset withdrawn from the subaccount.
460    AssetWithdrawal,
461    /// Internal transfer between subaccounts.
462    Transfer,
463    /// Subaccount-level collateral deposit.
464    SubaccountDeposit,
465    /// Subaccount-level collateral withdrawal.
466    SubaccountWithdrawal,
467    /// Balance change from liquidation.
468    Liquidation,
469    /// Reconciliation against on-chain state.
470    OnchainDriftFix,
471    /// Perpetual funding or mark settlement.
472    PerpSettlement,
473    /// Option expiry settlement.
474    OptionSettlement,
475    /// Interest credit or debit.
476    InterestAccrual,
477    /// Earlier balance change reverted on-chain.
478    OnchainRevert,
479    /// Revert of a revert.
480    DoubleRevert,
481}
482
483/// Compliance status returned by `public/change_compliance_status`.
484#[derive(
485    Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Display, EnumString, AsRefStr,
486)]
487#[serde(rename_all = "lowercase")]
488#[strum(serialize_all = "lowercase")]
489pub enum DeriveComplianceStatus {
490    /// Compliance restrictions on.
491    Enabled,
492    /// Compliance restrictions off.
493    Disabled,
494}
495
496/// Discrete depth values allowed in the WS subscription channel name
497/// `orderbook.{instrument_name}.{group}.{depth}`.
498#[derive(
499    Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Display, EnumString, AsRefStr,
500)]
501pub enum DeriveOrderbookDepth {
502    /// Top 1 level per side.
503    #[serde(rename = "1")]
504    #[strum(serialize = "1")]
505    D1,
506    /// Top 10 levels per side.
507    #[serde(rename = "10")]
508    #[strum(serialize = "10")]
509    D10,
510    /// Top 20 levels per side.
511    #[serde(rename = "20")]
512    #[strum(serialize = "20")]
513    D20,
514    /// Top 100 levels per side.
515    #[serde(rename = "100")]
516    #[strum(serialize = "100")]
517    D100,
518}
519
520/// Discrete price-grouping values for `orderbook.{instrument_name}.{group}.{depth}`.
521#[derive(
522    Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Display, EnumString, AsRefStr,
523)]
524pub enum DeriveOrderbookGroup {
525    /// No grouping.
526    #[serde(rename = "1")]
527    #[strum(serialize = "1")]
528    G1,
529    /// Group prices to 10x the tick.
530    #[serde(rename = "10")]
531    #[strum(serialize = "10")]
532    G10,
533    /// Group prices to 100x the tick.
534    #[serde(rename = "100")]
535    #[strum(serialize = "100")]
536    G100,
537}
538
539/// Discrete ticker push intervals (milliseconds) for the WS
540/// `ticker_slim.{instrument_name}.{interval}` channel.
541#[derive(
542    Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Display, EnumString, AsRefStr,
543)]
544pub enum DeriveTickerInterval {
545    /// 100ms ticker updates.
546    #[serde(rename = "100")]
547    #[strum(serialize = "100")]
548    Ms100,
549    /// 1000ms ticker updates.
550    #[serde(rename = "1000")]
551    #[strum(serialize = "1000")]
552    Ms1000,
553}
554
555#[cfg(test)]
556mod tests {
557    use std::str::FromStr;
558
559    use rstest::rstest;
560
561    use super::*;
562
563    #[rstest]
564    fn test_environment_default_is_mainnet() {
565        assert_eq!(DeriveEnvironment::default(), DeriveEnvironment::Mainnet);
566        assert!(!DeriveEnvironment::default().is_testnet());
567        assert!(DeriveEnvironment::Testnet.is_testnet());
568    }
569
570    #[rstest]
571    #[case(DeriveInstrumentType::Erc20, "erc20")]
572    #[case(DeriveInstrumentType::Option, "option")]
573    #[case(DeriveInstrumentType::Perp, "perp")]
574    fn test_instrument_type_display(#[case] variant: DeriveInstrumentType, #[case] expected: &str) {
575        assert_eq!(variant.to_string(), expected);
576    }
577
578    #[rstest]
579    #[case(DeriveTimeInForce::Gtc, "gtc")]
580    #[case(DeriveTimeInForce::PostOnly, "post_only")]
581    #[case(DeriveTimeInForce::Fok, "fok")]
582    #[case(DeriveTimeInForce::Ioc, "ioc")]
583    fn test_time_in_force_wire_strings(#[case] variant: DeriveTimeInForce, #[case] expected: &str) {
584        assert_eq!(variant.to_string(), expected);
585        assert_eq!(DeriveTimeInForce::from_str(expected).unwrap(), variant);
586        assert_eq!(
587            serde_json::to_string(&variant).unwrap(),
588            format!("\"{expected}\""),
589        );
590    }
591
592    #[rstest]
593    fn test_option_kind_serialization_uses_single_letter() {
594        assert_eq!(DeriveOptionKind::Call.to_string(), "C");
595        assert_eq!(DeriveOptionKind::Put.to_string(), "P");
596        assert_eq!(
597            DeriveOptionKind::from_str("C").unwrap(),
598            DeriveOptionKind::Call
599        );
600        assert_eq!(
601            DeriveOptionKind::from_str("P").unwrap(),
602            DeriveOptionKind::Put
603        );
604    }
605
606    #[rstest]
607    #[case(DeriveAssetType::Erc20, "erc20")]
608    #[case(DeriveAssetType::Option, "option")]
609    #[case(DeriveAssetType::Perp, "perp")]
610    fn test_asset_type_wire_strings(#[case] variant: DeriveAssetType, #[case] expected: &str) {
611        assert_eq!(variant.to_string(), expected);
612        assert_eq!(DeriveAssetType::from_str(expected).unwrap(), variant);
613    }
614
615    #[rstest]
616    #[case(DeriveOrderSide::Buy, "buy")]
617    #[case(DeriveOrderSide::Sell, "sell")]
618    fn test_order_side_wire_strings(#[case] variant: DeriveOrderSide, #[case] expected: &str) {
619        assert_eq!(variant.to_string(), expected);
620        assert_eq!(
621            serde_json::from_str::<DeriveOrderSide>(&format!("\"{expected}\"")).unwrap(),
622            variant
623        );
624    }
625
626    #[rstest]
627    #[case(DeriveOrderType::Limit, "limit")]
628    #[case(DeriveOrderType::Market, "market")]
629    fn test_order_type_wire_strings(#[case] variant: DeriveOrderType, #[case] expected: &str) {
630        assert_eq!(variant.to_string(), expected);
631        assert_eq!(DeriveOrderType::from_str(expected).unwrap(), variant);
632    }
633
634    #[rstest]
635    #[case(DeriveTriggerType::Stoploss, "stoploss")]
636    #[case(DeriveTriggerType::Takeprofit, "takeprofit")]
637    fn test_trigger_type_wire_strings(#[case] variant: DeriveTriggerType, #[case] expected: &str) {
638        assert_eq!(variant.to_string(), expected);
639        assert_eq!(DeriveTriggerType::from_str(expected).unwrap(), variant);
640    }
641
642    #[rstest]
643    #[case(DeriveTriggerPriceType::Mark, "mark")]
644    #[case(DeriveTriggerPriceType::Index, "index")]
645    fn test_trigger_price_type_wire_strings(
646        #[case] variant: DeriveTriggerPriceType,
647        #[case] expected: &str,
648    ) {
649        assert_eq!(variant.to_string(), expected);
650        assert_eq!(DeriveTriggerPriceType::from_str(expected).unwrap(), variant);
651    }
652
653    #[rstest]
654    #[case(DeriveOrderStatus::Open, "open")]
655    #[case(DeriveOrderStatus::Filled, "filled")]
656    #[case(DeriveOrderStatus::Rejected, "rejected")]
657    #[case(DeriveOrderStatus::Cancelled, "cancelled")]
658    #[case(DeriveOrderStatus::Expired, "expired")]
659    #[case(DeriveOrderStatus::Untriggered, "untriggered")]
660    #[case(DeriveOrderStatus::AlgoActive, "algo_active")]
661    fn test_order_status_wire_strings(#[case] variant: DeriveOrderStatus, #[case] expected: &str) {
662        assert_eq!(variant.to_string(), expected);
663        assert_eq!(DeriveOrderStatus::from_str(expected).unwrap(), variant);
664    }
665
666    #[rstest]
667    #[case(DeriveOrderCancelReason::Empty, "")]
668    #[case(DeriveOrderCancelReason::UserRequest, "user_request")]
669    #[case(DeriveOrderCancelReason::MmpTrigger, "mmp_trigger")]
670    #[case(DeriveOrderCancelReason::InsufficientMargin, "insufficient_margin")]
671    #[case(DeriveOrderCancelReason::SignedMaxFeeTooLow, "signed_max_fee_too_low")]
672    #[case(DeriveOrderCancelReason::CancelOnDisconnect, "cancel_on_disconnect")]
673    #[case(
674        DeriveOrderCancelReason::IocOrMarketPartialFill,
675        "ioc_or_market_partial_fill"
676    )]
677    #[case(
678        DeriveOrderCancelReason::SessionKeyDeregistered,
679        "session_key_deregistered"
680    )]
681    #[case(DeriveOrderCancelReason::SubaccountWithdrawn, "subaccount_withdrawn")]
682    #[case(DeriveOrderCancelReason::Compliance, "compliance")]
683    #[case(DeriveOrderCancelReason::TriggerFailed, "trigger_failed")]
684    #[case(DeriveOrderCancelReason::ValidationFailed, "validation_failed")]
685    #[case(DeriveOrderCancelReason::AlgoCompleted, "algo_completed")]
686    #[case(
687        DeriveOrderCancelReason::PostOnlyCrossMarket,
688        "Post only order cannot cross the market"
689    )]
690    fn test_order_cancel_reason_wire_strings(
691        #[case] variant: DeriveOrderCancelReason,
692        #[case] expected: &str,
693    ) {
694        assert_eq!(variant.to_string(), expected);
695        assert_eq!(
696            serde_json::from_str::<DeriveOrderCancelReason>(&format!("\"{expected}\"")).unwrap(),
697            variant
698        );
699    }
700
701    #[rstest]
702    #[case(DeriveRfqCancelReason::Empty, "")]
703    #[case(DeriveRfqCancelReason::UserRequest, "user_request")]
704    #[case(DeriveRfqCancelReason::InsufficientMargin, "insufficient_margin")]
705    #[case(DeriveRfqCancelReason::SignedMaxFeeTooLow, "signed_max_fee_too_low")]
706    #[case(DeriveRfqCancelReason::MmpTrigger, "mmp_trigger")]
707    #[case(DeriveRfqCancelReason::CancelOnDisconnect, "cancel_on_disconnect")]
708    #[case(
709        DeriveRfqCancelReason::SessionKeyDeregistered,
710        "session_key_deregistered"
711    )]
712    #[case(DeriveRfqCancelReason::SubaccountWithdrawn, "subaccount_withdrawn")]
713    #[case(DeriveRfqCancelReason::RfqNoLongerOpen, "rfq_no_longer_open")]
714    #[case(DeriveRfqCancelReason::Compliance, "compliance")]
715    fn test_rfq_cancel_reason_wire_strings(
716        #[case] variant: DeriveRfqCancelReason,
717        #[case] expected: &str,
718    ) {
719        assert_eq!(variant.to_string(), expected);
720        assert_eq!(
721            serde_json::from_str::<DeriveRfqCancelReason>(&format!("\"{expected}\"")).unwrap(),
722            variant
723        );
724    }
725
726    #[rstest]
727    fn test_order_cancel_reason_empty_string_round_trips() {
728        let json = serde_json::to_string(&DeriveOrderCancelReason::Empty).unwrap();
729        assert_eq!(json, "\"\"");
730        let parsed: DeriveOrderCancelReason = serde_json::from_str("\"\"").unwrap();
731        assert_eq!(parsed, DeriveOrderCancelReason::Empty);
732    }
733
734    #[rstest]
735    #[case(DeriveLiquidityRole::Maker, "maker")]
736    #[case(DeriveLiquidityRole::Taker, "taker")]
737    fn test_liquidity_role_wire_strings(
738        #[case] variant: DeriveLiquidityRole,
739        #[case] expected: &str,
740    ) {
741        assert_eq!(variant.to_string(), expected);
742        assert_eq!(DeriveLiquidityRole::from_str(expected).unwrap(), variant);
743    }
744
745    #[rstest]
746    #[case(DeriveTxStatus::Requested, "requested")]
747    #[case(DeriveTxStatus::Pending, "pending")]
748    #[case(DeriveTxStatus::Settled, "settled")]
749    #[case(DeriveTxStatus::Reverted, "reverted")]
750    #[case(DeriveTxStatus::Ignored, "ignored")]
751    fn test_tx_status_wire_strings(#[case] variant: DeriveTxStatus, #[case] expected: &str) {
752        assert_eq!(variant.to_string(), expected);
753        assert_eq!(DeriveTxStatus::from_str(expected).unwrap(), variant);
754    }
755
756    #[rstest]
757    #[case(DeriveMarginType::Sm, "SM")]
758    #[case(DeriveMarginType::Pm, "PM")]
759    #[case(DeriveMarginType::Pm2, "PM2")]
760    fn test_margin_type_wire_strings(#[case] variant: DeriveMarginType, #[case] expected: &str) {
761        assert_eq!(variant.to_string(), expected);
762        assert_eq!(DeriveMarginType::from_str(expected).unwrap(), variant);
763    }
764
765    #[rstest]
766    #[case(DeriveAuctionType::Solvent, "solvent")]
767    #[case(DeriveAuctionType::Insolvent, "insolvent")]
768    fn test_auction_type_wire_strings(#[case] variant: DeriveAuctionType, #[case] expected: &str) {
769        assert_eq!(variant.to_string(), expected);
770        assert_eq!(DeriveAuctionType::from_str(expected).unwrap(), variant);
771    }
772
773    #[rstest]
774    #[case(DeriveAuctionState::Ongoing, "ongoing")]
775    #[case(DeriveAuctionState::Ended, "ended")]
776    fn test_auction_state_wire_strings(
777        #[case] variant: DeriveAuctionState,
778        #[case] expected: &str,
779    ) {
780        assert_eq!(variant.to_string(), expected);
781        assert_eq!(DeriveAuctionState::from_str(expected).unwrap(), variant);
782    }
783
784    #[rstest]
785    #[case(DeriveNotificationStatus::Unseen, "unseen")]
786    #[case(DeriveNotificationStatus::Seen, "seen")]
787    #[case(DeriveNotificationStatus::Hidden, "hidden")]
788    fn test_notification_status_wire_strings(
789        #[case] variant: DeriveNotificationStatus,
790        #[case] expected: &str,
791    ) {
792        assert_eq!(variant.to_string(), expected);
793        assert_eq!(
794            DeriveNotificationStatus::from_str(expected).unwrap(),
795            variant
796        );
797    }
798
799    #[rstest]
800    #[case(DeriveNotificationType::Deposit, "deposit")]
801    #[case(DeriveNotificationType::Withdraw, "withdraw")]
802    #[case(DeriveNotificationType::Transfer, "transfer")]
803    #[case(DeriveNotificationType::Trade, "trade")]
804    #[case(DeriveNotificationType::Settlement, "settlement")]
805    #[case(DeriveNotificationType::Liquidation, "liquidation")]
806    #[case(DeriveNotificationType::Types, "types")]
807    fn test_notification_type_wire_strings(
808        #[case] variant: DeriveNotificationType,
809        #[case] expected: &str,
810    ) {
811        assert_eq!(variant.to_string(), expected);
812        assert_eq!(DeriveNotificationType::from_str(expected).unwrap(), variant);
813    }
814
815    #[rstest]
816    #[case(DeriveBalanceUpdateType::Trade, "trade")]
817    #[case(DeriveBalanceUpdateType::AssetDeposit, "asset_deposit")]
818    #[case(DeriveBalanceUpdateType::AssetWithdrawal, "asset_withdrawal")]
819    #[case(DeriveBalanceUpdateType::Transfer, "transfer")]
820    #[case(DeriveBalanceUpdateType::SubaccountDeposit, "subaccount_deposit")]
821    #[case(DeriveBalanceUpdateType::SubaccountWithdrawal, "subaccount_withdrawal")]
822    #[case(DeriveBalanceUpdateType::Liquidation, "liquidation")]
823    #[case(DeriveBalanceUpdateType::OnchainDriftFix, "onchain_drift_fix")]
824    #[case(DeriveBalanceUpdateType::PerpSettlement, "perp_settlement")]
825    #[case(DeriveBalanceUpdateType::OptionSettlement, "option_settlement")]
826    #[case(DeriveBalanceUpdateType::InterestAccrual, "interest_accrual")]
827    #[case(DeriveBalanceUpdateType::OnchainRevert, "onchain_revert")]
828    #[case(DeriveBalanceUpdateType::DoubleRevert, "double_revert")]
829    fn test_balance_update_type_wire_strings(
830        #[case] variant: DeriveBalanceUpdateType,
831        #[case] expected: &str,
832    ) {
833        assert_eq!(variant.to_string(), expected);
834        assert_eq!(
835            DeriveBalanceUpdateType::from_str(expected).unwrap(),
836            variant
837        );
838    }
839
840    #[rstest]
841    #[case(DeriveComplianceStatus::Enabled, "enabled")]
842    #[case(DeriveComplianceStatus::Disabled, "disabled")]
843    fn test_compliance_status_wire_strings(
844        #[case] variant: DeriveComplianceStatus,
845        #[case] expected: &str,
846    ) {
847        assert_eq!(variant.to_string(), expected);
848        assert_eq!(DeriveComplianceStatus::from_str(expected).unwrap(), variant);
849    }
850
851    #[rstest]
852    #[case(DeriveOrderbookDepth::D1, "1")]
853    #[case(DeriveOrderbookDepth::D10, "10")]
854    #[case(DeriveOrderbookDepth::D20, "20")]
855    #[case(DeriveOrderbookDepth::D100, "100")]
856    fn test_orderbook_depth_wire_strings(
857        #[case] variant: DeriveOrderbookDepth,
858        #[case] expected: &str,
859    ) {
860        assert_eq!(variant.to_string(), expected);
861        assert_eq!(DeriveOrderbookDepth::from_str(expected).unwrap(), variant);
862    }
863
864    #[rstest]
865    #[case(DeriveOrderbookGroup::G1, "1")]
866    #[case(DeriveOrderbookGroup::G10, "10")]
867    #[case(DeriveOrderbookGroup::G100, "100")]
868    fn test_orderbook_group_wire_strings(
869        #[case] variant: DeriveOrderbookGroup,
870        #[case] expected: &str,
871    ) {
872        assert_eq!(variant.to_string(), expected);
873        assert_eq!(DeriveOrderbookGroup::from_str(expected).unwrap(), variant);
874    }
875
876    #[rstest]
877    #[case(DeriveTickerInterval::Ms100, "100")]
878    #[case(DeriveTickerInterval::Ms1000, "1000")]
879    fn test_ticker_interval_wire_strings(
880        #[case] variant: DeriveTickerInterval,
881        #[case] expected: &str,
882    ) {
883        assert_eq!(variant.to_string(), expected);
884        assert_eq!(DeriveTickerInterval::from_str(expected).unwrap(), variant);
885    }
886}