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