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.adapters.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_all_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    /// Catch-all for unmodeled instrument types; skipped at instrument parse time.
87    #[serde(other)]
88    Unknown,
89}
90
91/// Wire-level asset type as returned in subaccount and collateral responses
92/// (`asset_type` field on `private/get_subaccount`, `public/get_assets`,
93/// `private/get_collaterals`). The variants line up byte-for-byte with
94/// [`DeriveInstrumentType`] but the venue treats it as a distinct field.
95#[derive(
96    Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Display, EnumString, EnumIter,
97)]
98#[serde(rename_all = "lowercase")]
99#[strum(serialize_all = "lowercase")]
100pub enum DeriveAssetType {
101    /// ERC-20 collateral asset.
102    Erc20,
103    /// Option position asset.
104    Option,
105    /// Perpetual position asset.
106    Perp,
107    /// Catch-all for unmodeled asset types; collateral parsing does not depend on this value.
108    #[serde(other)]
109    Unknown,
110}
111
112/// Order side (`direction` field on the venue API).
113#[derive(
114    Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Display, EnumString, AsRefStr,
115)]
116#[serde(rename_all = "lowercase")]
117#[strum(serialize_all = "lowercase")]
118pub enum DeriveOrderSide {
119    Buy,
120    Sell,
121}
122
123/// Order type accepted by `private/order`.
124#[derive(
125    Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Display, EnumString, AsRefStr,
126)]
127#[serde(rename_all = "lowercase")]
128#[strum(serialize_all = "lowercase")]
129pub enum DeriveOrderType {
130    Limit,
131    Market,
132    /// Catch-all for unmodeled order types on responses; never sent on requests.
133    #[serde(other)]
134    Unknown,
135}
136
137/// Trigger side accepted by `private/order`.
138#[derive(
139    Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Display, EnumString, AsRefStr,
140)]
141#[serde(rename_all = "lowercase")]
142#[strum(serialize_all = "lowercase")]
143pub enum DeriveTriggerType {
144    /// Stop-loss trigger.
145    Stoploss,
146    /// Take-profit trigger.
147    Takeprofit,
148    /// Catch-all for unmodeled trigger types on responses; never sent on requests.
149    #[serde(other)]
150    Unknown,
151}
152
153/// Trigger price source accepted by `private/order`.
154#[derive(
155    Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Display, EnumString, AsRefStr,
156)]
157#[serde(rename_all = "lowercase")]
158#[strum(serialize_all = "lowercase")]
159pub enum DeriveTriggerPriceType {
160    /// Derive mark price.
161    Mark,
162    /// Derive index price. Present in schemas, but currently rejected by the venue.
163    Index,
164    /// Catch-all for unmodeled trigger price sources on responses; never sent on requests.
165    #[serde(other)]
166    Unknown,
167}
168
169/// Order lifecycle status reported by `private/get_orders`,
170/// `private/get_order_history`, and the WS `{subaccount_id}.orders` channel.
171#[derive(
172    Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Display, EnumString, AsRefStr,
173)]
174#[serde(rename_all = "snake_case")]
175#[strum(serialize_all = "snake_case")]
176pub enum DeriveOrderStatus {
177    /// Resting in the book or accepted by the matching engine.
178    Open,
179    /// Fully filled.
180    Filled,
181    /// Rejected by matching engine or risk checks.
182    Rejected,
183    /// Cancelled by user, system, or expiry; see `cancel_reason`.
184    Cancelled,
185    /// `signature_expiry_sec` was reached.
186    Expired,
187    /// Trigger order saved but not yet fired.
188    Untriggered,
189    /// Algorithmic parent order active on the venue.
190    AlgoActive,
191    /// Unmodeled response status; domain conversion rejects it.
192    #[serde(other)]
193    Unknown,
194}
195
196/// Time-in-force flag accepted by `private/order`.
197#[derive(
198    Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Display, EnumString, AsRefStr,
199)]
200#[serde(rename_all = "snake_case")]
201#[strum(serialize_all = "snake_case")]
202pub enum DeriveTimeInForce {
203    /// Good-till-cancelled.
204    Gtc,
205    /// Post-only: reject if it would cross the spread.
206    PostOnly,
207    /// Fill-or-kill: fill in full immediately or cancel.
208    Fok,
209    /// Immediate-or-cancel: fill any tradable quantity, cancel the rest.
210    Ioc,
211    /// Catch-all for unmodeled time-in-force flags on responses; never sent on requests.
212    #[serde(other)]
213    Unknown,
214}
215
216/// Option kind parsed from the `instrument_name` suffix (e.g. `-C` or `-P`).
217#[derive(
218    Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Display, EnumString, AsRefStr,
219)]
220pub enum DeriveOptionKind {
221    /// Call option.
222    #[serde(rename = "C")]
223    #[strum(serialize = "C")]
224    Call,
225    /// Put option.
226    #[serde(rename = "P")]
227    #[strum(serialize = "P")]
228    Put,
229}
230
231/// Cancel reason attached to a cancelled order. Empty string corresponds to no
232/// reason (order is still open or finished by another transition).
233///
234/// `ValidationFailed` is present in the SDK enum but absent from the public
235/// JSON schema; included here for round-trip fidelity against SDK responses.
236#[derive(
237    Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Display, EnumString, AsRefStr,
238)]
239pub enum DeriveOrderCancelReason {
240    /// No cancel reason (open or finished without cancellation).
241    #[serde(rename = "")]
242    #[strum(serialize = "")]
243    Empty,
244    /// Explicit cancel via `private/cancel*`.
245    #[serde(rename = "user_request")]
246    #[strum(serialize = "user_request")]
247    UserRequest,
248    /// Market Maker Protection tripped.
249    #[serde(rename = "mmp_trigger")]
250    #[strum(serialize = "mmp_trigger")]
251    MmpTrigger,
252    /// Margin check failed at matching.
253    #[serde(rename = "insufficient_margin")]
254    #[strum(serialize = "insufficient_margin")]
255    InsufficientMargin,
256    /// Signed `max_fee` is below the venue's current required fee.
257    #[serde(rename = "signed_max_fee_too_low")]
258    #[strum(serialize = "signed_max_fee_too_low")]
259    SignedMaxFeeTooLow,
260    /// WS disconnect cancelled the session with cancel-on-disconnect enabled.
261    #[serde(rename = "cancel_on_disconnect")]
262    #[strum(serialize = "cancel_on_disconnect")]
263    CancelOnDisconnect,
264    /// Remainder of an IOC or market order auto-cancelled.
265    #[serde(rename = "ioc_or_market_partial_fill")]
266    #[strum(serialize = "ioc_or_market_partial_fill")]
267    IocOrMarketPartialFill,
268    /// Signing session key was deregistered.
269    #[serde(rename = "session_key_deregistered")]
270    #[strum(serialize = "session_key_deregistered")]
271    SessionKeyDeregistered,
272    /// Subaccount fully withdrawn.
273    #[serde(rename = "subaccount_withdrawn")]
274    #[strum(serialize = "subaccount_withdrawn")]
275    SubaccountWithdrawn,
276    /// Cancelled by compliance action.
277    #[serde(rename = "compliance")]
278    #[strum(serialize = "compliance")]
279    Compliance,
280    /// Trigger worker could not submit the child order.
281    #[serde(rename = "trigger_failed")]
282    #[strum(serialize = "trigger_failed")]
283    TriggerFailed,
284    /// Pre-engine validation failure (SDK-only, not in the public schema).
285    #[serde(rename = "validation_failed")]
286    #[strum(serialize = "validation_failed")]
287    ValidationFailed,
288    /// Algorithmic parent completed after finishing its slices.
289    #[serde(rename = "algo_completed")]
290    #[strum(serialize = "algo_completed")]
291    AlgoCompleted,
292    /// Post-only order would cross the market.
293    #[serde(rename = "Post only order cannot cross the market")]
294    #[strum(serialize = "Post only order cannot cross the market")]
295    PostOnlyCrossMarket,
296    /// Catch-all for unmodeled cancel reasons; the venue mixes snake_case codes
297    /// with full English sentences, so the set is expected to drift.
298    #[serde(rename = "unknown", other)]
299    #[strum(serialize = "unknown")]
300    Unknown,
301}
302
303/// Cancel reason attached to a cancelled RFQ quote. Similar to
304/// [`DeriveOrderCancelReason`] but emits `rfq_no_longer_open` instead of
305/// `ioc_or_market_partial_fill`.
306#[derive(
307    Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Display, EnumString, AsRefStr,
308)]
309pub enum DeriveRfqCancelReason {
310    /// No cancel reason.
311    #[serde(rename = "")]
312    #[strum(serialize = "")]
313    Empty,
314    /// Explicit cancel via `private/cancel_quote`.
315    #[serde(rename = "user_request")]
316    #[strum(serialize = "user_request")]
317    UserRequest,
318    /// Margin check failed at quote execution.
319    #[serde(rename = "insufficient_margin")]
320    #[strum(serialize = "insufficient_margin")]
321    InsufficientMargin,
322    /// Signed `max_fee` is below the venue's current required fee.
323    #[serde(rename = "signed_max_fee_too_low")]
324    #[strum(serialize = "signed_max_fee_too_low")]
325    SignedMaxFeeTooLow,
326    /// MMP tripped.
327    #[serde(rename = "mmp_trigger")]
328    #[strum(serialize = "mmp_trigger")]
329    MmpTrigger,
330    /// WS disconnect cancelled the session with cancel-on-disconnect enabled.
331    #[serde(rename = "cancel_on_disconnect")]
332    #[strum(serialize = "cancel_on_disconnect")]
333    CancelOnDisconnect,
334    /// Signing session key was deregistered.
335    #[serde(rename = "session_key_deregistered")]
336    #[strum(serialize = "session_key_deregistered")]
337    SessionKeyDeregistered,
338    /// Subaccount fully withdrawn.
339    #[serde(rename = "subaccount_withdrawn")]
340    #[strum(serialize = "subaccount_withdrawn")]
341    SubaccountWithdrawn,
342    /// Underlying RFQ was filled or cancelled.
343    #[serde(rename = "rfq_no_longer_open")]
344    #[strum(serialize = "rfq_no_longer_open")]
345    RfqNoLongerOpen,
346    /// Cancelled by compliance action.
347    #[serde(rename = "compliance")]
348    #[strum(serialize = "compliance")]
349    Compliance,
350}
351
352/// Role of the user in a trade (`liquidity_role` field).
353#[derive(
354    Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Display, EnumString, AsRefStr,
355)]
356#[serde(rename_all = "lowercase")]
357#[strum(serialize_all = "lowercase")]
358pub enum DeriveLiquidityRole {
359    /// Resting side of the trade.
360    Maker,
361    /// Aggressing side of the trade.
362    Taker,
363    /// Catch-all for unmodeled liquidity roles.
364    #[serde(other)]
365    Unknown,
366}
367
368/// Known Derive margin model labels. Subaccount snapshots preserve the
369/// returned margin type as an open string.
370#[derive(
371    Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Display, EnumString, AsRefStr,
372)]
373pub enum DeriveMarginType {
374    /// Standard Margin.
375    #[serde(rename = "SM")]
376    #[strum(serialize = "SM")]
377    Sm,
378    /// Portfolio Margin (legacy PMRM).
379    #[serde(rename = "PM")]
380    #[strum(serialize = "PM")]
381    Pm,
382    /// Portfolio Margin v2 (PMRM_2).
383    #[serde(rename = "PM2")]
384    #[strum(serialize = "PM2")]
385    Pm2,
386    /// Catch-all for unmodeled margin types; `PM2` itself arrived after `SM`/`PM`.
387    #[serde(rename = "unknown", other)]
388    #[strum(serialize = "unknown")]
389    Unknown,
390}
391
392/// Liquidation auction type returned by `private/get_liquidation_history`.
393#[derive(
394    Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Display, EnumString, AsRefStr,
395)]
396#[serde(rename_all = "lowercase")]
397#[strum(serialize_all = "lowercase")]
398pub enum DeriveAuctionType {
399    /// Auction on a still-solvent account.
400    Solvent,
401    /// Auction on an insolvent account.
402    Insolvent,
403}
404
405/// Liquidation auction state.
406#[derive(
407    Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Display, EnumString, AsRefStr,
408)]
409#[serde(rename_all = "lowercase")]
410#[strum(serialize_all = "lowercase")]
411pub enum DeriveAuctionState {
412    /// Auction is active.
413    Ongoing,
414    /// Auction has concluded.
415    Ended,
416}
417
418/// Notification acknowledgement state on `private/get_notifications` and
419/// `private/update_notifications`.
420#[derive(
421    Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Display, EnumString, AsRefStr,
422)]
423#[serde(rename_all = "lowercase")]
424#[strum(serialize_all = "lowercase")]
425pub enum DeriveNotificationStatus {
426    /// Not yet viewed.
427    Unseen,
428    /// Viewed but not dismissed.
429    Seen,
430    /// Dismissed from feed.
431    Hidden,
432}
433
434/// Notification category. The `Types` variant is emitted by the upstream
435/// codegen and surfaces in the wire enum; treat it as a generic placeholder
436/// rather than a meaningful category.
437#[derive(
438    Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Display, EnumString, AsRefStr,
439)]
440#[serde(rename_all = "lowercase")]
441#[strum(serialize_all = "lowercase")]
442pub enum DeriveNotificationType {
443    /// Deposit event.
444    Deposit,
445    /// Withdrawal event.
446    Withdraw,
447    /// ERC-20 transfer between subaccounts.
448    Transfer,
449    /// Trade execution.
450    Trade,
451    /// Option or perp settlement.
452    Settlement,
453    /// Liquidation event.
454    Liquidation,
455    /// Codegen placeholder; not a stable category.
456    Types,
457}
458
459/// Cause of a balance row update on the WS `{subaccount_id}.balances`
460/// channel.
461#[derive(
462    Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Display, EnumString, AsRefStr,
463)]
464#[serde(rename_all = "snake_case")]
465#[strum(serialize_all = "snake_case")]
466pub enum DeriveBalanceUpdateType {
467    /// Balance changed because of a trade fill.
468    Trade,
469    /// Asset deposited into the subaccount.
470    AssetDeposit,
471    /// Asset withdrawn from the subaccount.
472    AssetWithdrawal,
473    /// Internal transfer between subaccounts.
474    Transfer,
475    /// Subaccount-level collateral deposit.
476    SubaccountDeposit,
477    /// Subaccount-level collateral withdrawal.
478    SubaccountWithdrawal,
479    /// Balance change from liquidation.
480    Liquidation,
481    /// Reconciliation against on-chain state.
482    OnchainDriftFix,
483    /// Perpetual funding or mark settlement.
484    PerpSettlement,
485    /// Option expiry settlement.
486    OptionSettlement,
487    /// Interest credit or debit.
488    InterestAccrual,
489    /// Earlier balance change reverted on-chain.
490    OnchainRevert,
491    /// Revert of a revert.
492    DoubleRevert,
493}
494
495/// Compliance status returned by `public/change_compliance_status`.
496#[derive(
497    Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Display, EnumString, AsRefStr,
498)]
499#[serde(rename_all = "lowercase")]
500#[strum(serialize_all = "lowercase")]
501pub enum DeriveComplianceStatus {
502    /// Compliance restrictions on.
503    Enabled,
504    /// Compliance restrictions off.
505    Disabled,
506}
507
508/// Discrete depth values allowed in the WS subscription channel name
509/// `orderbook.{instrument_name}.{group}.{depth}`.
510#[derive(
511    Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Display, EnumString, AsRefStr,
512)]
513pub enum DeriveOrderbookDepth {
514    /// Top 1 level per side.
515    #[serde(rename = "1")]
516    #[strum(serialize = "1")]
517    D1,
518    /// Top 10 levels per side.
519    #[serde(rename = "10")]
520    #[strum(serialize = "10")]
521    D10,
522    /// Top 20 levels per side.
523    #[serde(rename = "20")]
524    #[strum(serialize = "20")]
525    D20,
526    /// Top 100 levels per side.
527    #[serde(rename = "100")]
528    #[strum(serialize = "100")]
529    D100,
530}
531
532/// Discrete price-grouping values for `orderbook.{instrument_name}.{group}.{depth}`.
533#[derive(
534    Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Display, EnumString, AsRefStr,
535)]
536pub enum DeriveOrderbookGroup {
537    /// No grouping.
538    #[serde(rename = "1")]
539    #[strum(serialize = "1")]
540    G1,
541    /// Group prices to 10x the tick.
542    #[serde(rename = "10")]
543    #[strum(serialize = "10")]
544    G10,
545    /// Group prices to 100x the tick.
546    #[serde(rename = "100")]
547    #[strum(serialize = "100")]
548    G100,
549}
550
551/// Discrete ticker push intervals (milliseconds) for the WS
552/// `ticker_slim.{instrument_name}.{interval}` channel.
553#[derive(
554    Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Display, EnumString, AsRefStr,
555)]
556pub enum DeriveTickerInterval {
557    /// 100ms ticker updates.
558    #[serde(rename = "100")]
559    #[strum(serialize = "100")]
560    Ms100,
561    /// 1000ms ticker updates.
562    #[serde(rename = "1000")]
563    #[strum(serialize = "1000")]
564    Ms1000,
565}
566
567#[cfg(test)]
568mod tests {
569    use std::str::FromStr;
570
571    use rstest::rstest;
572
573    use super::*;
574
575    #[rstest]
576    fn test_environment_default_is_mainnet() {
577        assert_eq!(DeriveEnvironment::default(), DeriveEnvironment::Mainnet);
578        assert!(!DeriveEnvironment::default().is_testnet());
579        assert!(DeriveEnvironment::Testnet.is_testnet());
580    }
581
582    #[rstest]
583    #[case(DeriveInstrumentType::Erc20, "erc20")]
584    #[case(DeriveInstrumentType::Option, "option")]
585    #[case(DeriveInstrumentType::Perp, "perp")]
586    #[case(DeriveInstrumentType::Unknown, "unknown")]
587    fn test_instrument_type_display(#[case] variant: DeriveInstrumentType, #[case] expected: &str) {
588        assert_eq!(variant.to_string(), expected);
589    }
590
591    #[rstest]
592    #[case(DeriveTimeInForce::Gtc, "gtc")]
593    #[case(DeriveTimeInForce::PostOnly, "post_only")]
594    #[case(DeriveTimeInForce::Fok, "fok")]
595    #[case(DeriveTimeInForce::Ioc, "ioc")]
596    #[case(DeriveTimeInForce::Unknown, "unknown")]
597    fn test_time_in_force_wire_strings(#[case] variant: DeriveTimeInForce, #[case] expected: &str) {
598        assert_eq!(variant.to_string(), expected);
599        assert_eq!(DeriveTimeInForce::from_str(expected).unwrap(), variant);
600        assert_eq!(
601            serde_json::to_string(&variant).unwrap(),
602            format!("\"{expected}\""),
603        );
604    }
605
606    #[rstest]
607    fn test_option_kind_serialization_uses_single_letter() {
608        assert_eq!(DeriveOptionKind::Call.to_string(), "C");
609        assert_eq!(DeriveOptionKind::Put.to_string(), "P");
610        assert_eq!(
611            DeriveOptionKind::from_str("C").unwrap(),
612            DeriveOptionKind::Call
613        );
614        assert_eq!(
615            DeriveOptionKind::from_str("P").unwrap(),
616            DeriveOptionKind::Put
617        );
618    }
619
620    #[rstest]
621    #[case(DeriveAssetType::Erc20, "erc20")]
622    #[case(DeriveAssetType::Option, "option")]
623    #[case(DeriveAssetType::Perp, "perp")]
624    #[case(DeriveAssetType::Unknown, "unknown")]
625    fn test_asset_type_wire_strings(#[case] variant: DeriveAssetType, #[case] expected: &str) {
626        assert_eq!(variant.to_string(), expected);
627        assert_eq!(DeriveAssetType::from_str(expected).unwrap(), variant);
628    }
629
630    #[rstest]
631    #[case(DeriveOrderSide::Buy, "buy")]
632    #[case(DeriveOrderSide::Sell, "sell")]
633    fn test_order_side_wire_strings(#[case] variant: DeriveOrderSide, #[case] expected: &str) {
634        assert_eq!(variant.to_string(), expected);
635        assert_eq!(
636            serde_json::from_str::<DeriveOrderSide>(&format!("\"{expected}\"")).unwrap(),
637            variant
638        );
639    }
640
641    #[rstest]
642    #[case(DeriveOrderType::Limit, "limit")]
643    #[case(DeriveOrderType::Market, "market")]
644    #[case(DeriveOrderType::Unknown, "unknown")]
645    fn test_order_type_wire_strings(#[case] variant: DeriveOrderType, #[case] expected: &str) {
646        assert_eq!(variant.to_string(), expected);
647        assert_eq!(DeriveOrderType::from_str(expected).unwrap(), variant);
648    }
649
650    #[rstest]
651    #[case(DeriveTriggerType::Stoploss, "stoploss")]
652    #[case(DeriveTriggerType::Takeprofit, "takeprofit")]
653    #[case(DeriveTriggerType::Unknown, "unknown")]
654    fn test_trigger_type_wire_strings(#[case] variant: DeriveTriggerType, #[case] expected: &str) {
655        assert_eq!(variant.to_string(), expected);
656        assert_eq!(DeriveTriggerType::from_str(expected).unwrap(), variant);
657    }
658
659    #[rstest]
660    #[case(DeriveTriggerPriceType::Mark, "mark")]
661    #[case(DeriveTriggerPriceType::Index, "index")]
662    #[case(DeriveTriggerPriceType::Unknown, "unknown")]
663    fn test_trigger_price_type_wire_strings(
664        #[case] variant: DeriveTriggerPriceType,
665        #[case] expected: &str,
666    ) {
667        assert_eq!(variant.to_string(), expected);
668        assert_eq!(DeriveTriggerPriceType::from_str(expected).unwrap(), variant);
669    }
670
671    #[rstest]
672    #[case(DeriveOrderStatus::Open, "open")]
673    #[case(DeriveOrderStatus::Filled, "filled")]
674    #[case(DeriveOrderStatus::Rejected, "rejected")]
675    #[case(DeriveOrderStatus::Cancelled, "cancelled")]
676    #[case(DeriveOrderStatus::Expired, "expired")]
677    #[case(DeriveOrderStatus::Untriggered, "untriggered")]
678    #[case(DeriveOrderStatus::AlgoActive, "algo_active")]
679    fn test_order_status_wire_strings(#[case] variant: DeriveOrderStatus, #[case] expected: &str) {
680        assert_eq!(variant.to_string(), expected);
681        assert_eq!(DeriveOrderStatus::from_str(expected).unwrap(), variant);
682    }
683
684    #[rstest]
685    #[case(DeriveOrderCancelReason::Empty, "")]
686    #[case(DeriveOrderCancelReason::UserRequest, "user_request")]
687    #[case(DeriveOrderCancelReason::MmpTrigger, "mmp_trigger")]
688    #[case(DeriveOrderCancelReason::InsufficientMargin, "insufficient_margin")]
689    #[case(DeriveOrderCancelReason::SignedMaxFeeTooLow, "signed_max_fee_too_low")]
690    #[case(DeriveOrderCancelReason::CancelOnDisconnect, "cancel_on_disconnect")]
691    #[case(
692        DeriveOrderCancelReason::IocOrMarketPartialFill,
693        "ioc_or_market_partial_fill"
694    )]
695    #[case(
696        DeriveOrderCancelReason::SessionKeyDeregistered,
697        "session_key_deregistered"
698    )]
699    #[case(DeriveOrderCancelReason::SubaccountWithdrawn, "subaccount_withdrawn")]
700    #[case(DeriveOrderCancelReason::Compliance, "compliance")]
701    #[case(DeriveOrderCancelReason::TriggerFailed, "trigger_failed")]
702    #[case(DeriveOrderCancelReason::ValidationFailed, "validation_failed")]
703    #[case(DeriveOrderCancelReason::AlgoCompleted, "algo_completed")]
704    #[case(
705        DeriveOrderCancelReason::PostOnlyCrossMarket,
706        "Post only order cannot cross the market"
707    )]
708    #[case(DeriveOrderCancelReason::Unknown, "unknown")]
709    fn test_order_cancel_reason_wire_strings(
710        #[case] variant: DeriveOrderCancelReason,
711        #[case] expected: &str,
712    ) {
713        assert_eq!(variant.to_string(), expected);
714        assert_eq!(
715            serde_json::from_str::<DeriveOrderCancelReason>(&format!("\"{expected}\"")).unwrap(),
716            variant
717        );
718    }
719
720    #[rstest]
721    #[case(DeriveRfqCancelReason::Empty, "")]
722    #[case(DeriveRfqCancelReason::UserRequest, "user_request")]
723    #[case(DeriveRfqCancelReason::InsufficientMargin, "insufficient_margin")]
724    #[case(DeriveRfqCancelReason::SignedMaxFeeTooLow, "signed_max_fee_too_low")]
725    #[case(DeriveRfqCancelReason::MmpTrigger, "mmp_trigger")]
726    #[case(DeriveRfqCancelReason::CancelOnDisconnect, "cancel_on_disconnect")]
727    #[case(
728        DeriveRfqCancelReason::SessionKeyDeregistered,
729        "session_key_deregistered"
730    )]
731    #[case(DeriveRfqCancelReason::SubaccountWithdrawn, "subaccount_withdrawn")]
732    #[case(DeriveRfqCancelReason::RfqNoLongerOpen, "rfq_no_longer_open")]
733    #[case(DeriveRfqCancelReason::Compliance, "compliance")]
734    fn test_rfq_cancel_reason_wire_strings(
735        #[case] variant: DeriveRfqCancelReason,
736        #[case] expected: &str,
737    ) {
738        assert_eq!(variant.to_string(), expected);
739        assert_eq!(
740            serde_json::from_str::<DeriveRfqCancelReason>(&format!("\"{expected}\"")).unwrap(),
741            variant
742        );
743    }
744
745    #[rstest]
746    fn test_response_enums_decode_unknown_wire_strings_leniently() {
747        assert_eq!(
748            serde_json::from_str::<DeriveInstrumentType>("\"structured\"").unwrap(),
749            DeriveInstrumentType::Unknown,
750        );
751        assert_eq!(
752            serde_json::from_str::<DeriveAssetType>("\"structured\"").unwrap(),
753            DeriveAssetType::Unknown,
754        );
755        assert_eq!(
756            serde_json::from_str::<DeriveOrderType>("\"twap\"").unwrap(),
757            DeriveOrderType::Unknown,
758        );
759        assert_eq!(
760            serde_json::from_str::<DeriveTriggerType>("\"trailing_stop\"").unwrap(),
761            DeriveTriggerType::Unknown,
762        );
763        assert_eq!(
764            serde_json::from_str::<DeriveTriggerPriceType>("\"last\"").unwrap(),
765            DeriveTriggerPriceType::Unknown,
766        );
767        assert_eq!(
768            serde_json::from_str::<DeriveTimeInForce>("\"day\"").unwrap(),
769            DeriveTimeInForce::Unknown,
770        );
771        assert_eq!(
772            serde_json::from_str::<DeriveOrderCancelReason>("\"Trigger worker restarted\"")
773                .unwrap(),
774            DeriveOrderCancelReason::Unknown,
775        );
776        assert_eq!(
777            serde_json::from_str::<DeriveLiquidityRole>("\"auction\"").unwrap(),
778            DeriveLiquidityRole::Unknown,
779        );
780        assert_eq!(
781            serde_json::from_str::<DeriveMarginType>("\"PM3\"").unwrap(),
782            DeriveMarginType::Unknown,
783        );
784    }
785
786    #[rstest]
787    fn test_order_cancel_reason_empty_string_round_trips() {
788        let json = serde_json::to_string(&DeriveOrderCancelReason::Empty).unwrap();
789        assert_eq!(json, "\"\"");
790        let parsed: DeriveOrderCancelReason = serde_json::from_str("\"\"").unwrap();
791        assert_eq!(parsed, DeriveOrderCancelReason::Empty);
792    }
793
794    #[rstest]
795    #[case(DeriveLiquidityRole::Maker, "maker")]
796    #[case(DeriveLiquidityRole::Taker, "taker")]
797    #[case(DeriveLiquidityRole::Unknown, "unknown")]
798    fn test_liquidity_role_wire_strings(
799        #[case] variant: DeriveLiquidityRole,
800        #[case] expected: &str,
801    ) {
802        assert_eq!(variant.to_string(), expected);
803        assert_eq!(DeriveLiquidityRole::from_str(expected).unwrap(), variant);
804    }
805
806    #[rstest]
807    #[case(DeriveMarginType::Sm, "SM")]
808    #[case(DeriveMarginType::Pm, "PM")]
809    #[case(DeriveMarginType::Pm2, "PM2")]
810    #[case(DeriveMarginType::Unknown, "unknown")]
811    fn test_margin_type_wire_strings(#[case] variant: DeriveMarginType, #[case] expected: &str) {
812        assert_eq!(variant.to_string(), expected);
813        assert_eq!(DeriveMarginType::from_str(expected).unwrap(), variant);
814    }
815
816    #[rstest]
817    #[case(DeriveAuctionType::Solvent, "solvent")]
818    #[case(DeriveAuctionType::Insolvent, "insolvent")]
819    fn test_auction_type_wire_strings(#[case] variant: DeriveAuctionType, #[case] expected: &str) {
820        assert_eq!(variant.to_string(), expected);
821        assert_eq!(DeriveAuctionType::from_str(expected).unwrap(), variant);
822    }
823
824    #[rstest]
825    #[case(DeriveAuctionState::Ongoing, "ongoing")]
826    #[case(DeriveAuctionState::Ended, "ended")]
827    fn test_auction_state_wire_strings(
828        #[case] variant: DeriveAuctionState,
829        #[case] expected: &str,
830    ) {
831        assert_eq!(variant.to_string(), expected);
832        assert_eq!(DeriveAuctionState::from_str(expected).unwrap(), variant);
833    }
834
835    #[rstest]
836    #[case(DeriveNotificationStatus::Unseen, "unseen")]
837    #[case(DeriveNotificationStatus::Seen, "seen")]
838    #[case(DeriveNotificationStatus::Hidden, "hidden")]
839    fn test_notification_status_wire_strings(
840        #[case] variant: DeriveNotificationStatus,
841        #[case] expected: &str,
842    ) {
843        assert_eq!(variant.to_string(), expected);
844        assert_eq!(
845            DeriveNotificationStatus::from_str(expected).unwrap(),
846            variant
847        );
848    }
849
850    #[rstest]
851    #[case(DeriveNotificationType::Deposit, "deposit")]
852    #[case(DeriveNotificationType::Withdraw, "withdraw")]
853    #[case(DeriveNotificationType::Transfer, "transfer")]
854    #[case(DeriveNotificationType::Trade, "trade")]
855    #[case(DeriveNotificationType::Settlement, "settlement")]
856    #[case(DeriveNotificationType::Liquidation, "liquidation")]
857    #[case(DeriveNotificationType::Types, "types")]
858    fn test_notification_type_wire_strings(
859        #[case] variant: DeriveNotificationType,
860        #[case] expected: &str,
861    ) {
862        assert_eq!(variant.to_string(), expected);
863        assert_eq!(DeriveNotificationType::from_str(expected).unwrap(), variant);
864    }
865
866    #[rstest]
867    #[case(DeriveBalanceUpdateType::Trade, "trade")]
868    #[case(DeriveBalanceUpdateType::AssetDeposit, "asset_deposit")]
869    #[case(DeriveBalanceUpdateType::AssetWithdrawal, "asset_withdrawal")]
870    #[case(DeriveBalanceUpdateType::Transfer, "transfer")]
871    #[case(DeriveBalanceUpdateType::SubaccountDeposit, "subaccount_deposit")]
872    #[case(DeriveBalanceUpdateType::SubaccountWithdrawal, "subaccount_withdrawal")]
873    #[case(DeriveBalanceUpdateType::Liquidation, "liquidation")]
874    #[case(DeriveBalanceUpdateType::OnchainDriftFix, "onchain_drift_fix")]
875    #[case(DeriveBalanceUpdateType::PerpSettlement, "perp_settlement")]
876    #[case(DeriveBalanceUpdateType::OptionSettlement, "option_settlement")]
877    #[case(DeriveBalanceUpdateType::InterestAccrual, "interest_accrual")]
878    #[case(DeriveBalanceUpdateType::OnchainRevert, "onchain_revert")]
879    #[case(DeriveBalanceUpdateType::DoubleRevert, "double_revert")]
880    fn test_balance_update_type_wire_strings(
881        #[case] variant: DeriveBalanceUpdateType,
882        #[case] expected: &str,
883    ) {
884        assert_eq!(variant.to_string(), expected);
885        assert_eq!(
886            DeriveBalanceUpdateType::from_str(expected).unwrap(),
887            variant
888        );
889    }
890
891    #[rstest]
892    #[case(DeriveComplianceStatus::Enabled, "enabled")]
893    #[case(DeriveComplianceStatus::Disabled, "disabled")]
894    fn test_compliance_status_wire_strings(
895        #[case] variant: DeriveComplianceStatus,
896        #[case] expected: &str,
897    ) {
898        assert_eq!(variant.to_string(), expected);
899        assert_eq!(DeriveComplianceStatus::from_str(expected).unwrap(), variant);
900    }
901
902    #[rstest]
903    #[case(DeriveOrderbookDepth::D1, "1")]
904    #[case(DeriveOrderbookDepth::D10, "10")]
905    #[case(DeriveOrderbookDepth::D20, "20")]
906    #[case(DeriveOrderbookDepth::D100, "100")]
907    fn test_orderbook_depth_wire_strings(
908        #[case] variant: DeriveOrderbookDepth,
909        #[case] expected: &str,
910    ) {
911        assert_eq!(variant.to_string(), expected);
912        assert_eq!(DeriveOrderbookDepth::from_str(expected).unwrap(), variant);
913    }
914
915    #[rstest]
916    #[case(DeriveOrderbookGroup::G1, "1")]
917    #[case(DeriveOrderbookGroup::G10, "10")]
918    #[case(DeriveOrderbookGroup::G100, "100")]
919    fn test_orderbook_group_wire_strings(
920        #[case] variant: DeriveOrderbookGroup,
921        #[case] expected: &str,
922    ) {
923        assert_eq!(variant.to_string(), expected);
924        assert_eq!(DeriveOrderbookGroup::from_str(expected).unwrap(), variant);
925    }
926
927    #[rstest]
928    #[case(DeriveTickerInterval::Ms100, "100")]
929    #[case(DeriveTickerInterval::Ms1000, "1000")]
930    fn test_ticker_interval_wire_strings(
931        #[case] variant: DeriveTickerInterval,
932        #[case] expected: &str,
933    ) {
934        assert_eq!(variant.to_string(), expected);
935        assert_eq!(DeriveTickerInterval::from_str(expected).unwrap(), variant);
936    }
937}