Skip to main content

nautilus_coinbase/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
16use serde::{Deserialize, Serialize};
17use strum::{AsRefStr, Display, EnumIter, EnumString};
18
19/// Coinbase environment type.
20#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
21#[cfg_attr(
22    feature = "python",
23    pyo3::pyclass(
24        module = "nautilus_trader.core.nautilus_pyo3.coinbase",
25        eq,
26        from_py_object,
27        rename_all = "SCREAMING_SNAKE_CASE"
28    )
29)]
30#[cfg_attr(
31    feature = "python",
32    pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.adapters.coinbase")
33)]
34pub enum CoinbaseEnvironment {
35    /// Production environment.
36    #[default]
37    Live,
38    /// Sandbox/testing environment.
39    Sandbox,
40}
41
42impl CoinbaseEnvironment {
43    /// Returns true if this is the sandbox environment.
44    #[must_use]
45    pub const fn is_sandbox(self) -> bool {
46        matches!(self, Self::Sandbox)
47    }
48}
49
50/// Coinbase product type.
51#[derive(
52    Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Display, EnumString, EnumIter,
53)]
54#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
55#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
56pub enum CoinbaseProductType {
57    Spot,
58    Future,
59    #[serde(rename = "UNKNOWN_PRODUCT_TYPE")]
60    #[strum(serialize = "UNKNOWN_PRODUCT_TYPE")]
61    Unknown,
62}
63
64/// Coinbase order side.
65#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Display, EnumString)]
66#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
67#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
68pub enum CoinbaseOrderSide {
69    Buy,
70    Sell,
71    #[serde(rename = "UNKNOWN_ORDER_SIDE")]
72    #[strum(serialize = "UNKNOWN_ORDER_SIDE")]
73    Unknown,
74}
75
76/// Coinbase REST order type values.
77#[derive(
78    Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Display, EnumString, AsRefStr,
79)]
80#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
81#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
82pub enum CoinbaseOrderType {
83    #[serde(rename = "UNKNOWN_ORDER_TYPE")]
84    #[strum(serialize = "UNKNOWN_ORDER_TYPE")]
85    Unknown,
86    #[serde(alias = "Market")]
87    Market,
88    #[serde(alias = "Limit")]
89    Limit,
90    #[serde(alias = "Stop")]
91    Stop,
92    #[serde(alias = "StopLimit", alias = "Stop Limit")]
93    StopLimit,
94    #[serde(alias = "Bracket")]
95    Bracket,
96    Twap,
97    #[serde(alias = "Roll Open")]
98    RollOpen,
99    #[serde(alias = "Roll Close")]
100    RollClose,
101    Liquidation,
102    Scaled,
103}
104
105/// Coinbase order status.
106#[derive(
107    Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Display, EnumString, AsRefStr,
108)]
109#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
110#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
111pub enum CoinbaseOrderStatus {
112    Pending,
113    Open,
114    Filled,
115    Cancelled,
116    Expired,
117    Failed,
118    #[serde(rename = "UNKNOWN_ORDER_STATUS")]
119    #[strum(serialize = "UNKNOWN_ORDER_STATUS")]
120    Unknown,
121    Queued,
122    CancelQueued,
123    EditQueued,
124}
125
126impl CoinbaseOrderStatus {
127    /// Returns true when the status represents a terminal lifecycle state
128    /// (no further updates expected from the venue).
129    #[must_use]
130    pub const fn is_terminal(self) -> bool {
131        matches!(
132            self,
133            Self::Filled | Self::Cancelled | Self::Expired | Self::Failed
134        )
135    }
136}
137
138/// Coinbase time in force.
139#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Display, EnumString)]
140#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
141#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
142pub enum CoinbaseTimeInForce {
143    #[serde(rename = "UNKNOWN_TIME_IN_FORCE")]
144    #[strum(serialize = "UNKNOWN_TIME_IN_FORCE")]
145    Unknown,
146    GoodUntilDateTime,
147    GoodUntilCancelled,
148    ImmediateOrCancel,
149    FillOrKill,
150}
151
152/// Coinbase trigger status.
153#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Display, EnumString)]
154#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
155#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
156pub enum CoinbaseTriggerStatus {
157    #[serde(rename = "UNKNOWN_TRIGGER_STATUS")]
158    #[strum(serialize = "UNKNOWN_TRIGGER_STATUS")]
159    Unknown,
160    InvalidOrderType,
161    StopPending,
162    StopTriggered,
163}
164
165/// Coinbase order placement source.
166#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Display, EnumString)]
167#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
168#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
169pub enum CoinbaseOrderPlacementSource {
170    #[serde(rename = "UNKNOWN_PLACEMENT_SOURCE")]
171    #[strum(serialize = "UNKNOWN_PLACEMENT_SOURCE")]
172    Unknown,
173    RetailSimple,
174    RetailAdvanced,
175}
176
177/// Coinbase order margin type.
178#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Display, EnumString)]
179#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
180#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
181#[cfg_attr(
182    feature = "python",
183    pyo3::pyclass(
184        module = "nautilus_trader.core.nautilus_pyo3.coinbase",
185        eq,
186        eq_int,
187        frozen,
188        from_py_object,
189        rename_all = "SCREAMING_SNAKE_CASE"
190    )
191)]
192#[cfg_attr(
193    feature = "python",
194    pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.adapters.coinbase")
195)]
196pub enum CoinbaseMarginType {
197    #[serde(alias = "Cross")]
198    Cross,
199    #[serde(alias = "Isolated")]
200    Isolated,
201}
202
203/// Coinbase product status.
204#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Display, EnumString)]
205#[serde(rename_all = "snake_case")]
206#[strum(serialize_all = "snake_case")]
207pub enum CoinbaseProductStatus {
208    Online,
209    Offline,
210    Delisted,
211    /// Futures products return an empty status string.
212    #[serde(rename = "")]
213    #[strum(serialize = "")]
214    Unset,
215    /// A status value not modeled by this enum.
216    Unknown,
217}
218
219/// Coinbase product venue.
220#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Display, EnumString)]
221#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
222#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
223pub enum CoinbaseProductVenue {
224    /// Coinbase Exchange (spot).
225    Cbe,
226    /// Futures Commission Merchant (futures/perpetuals).
227    Fcm,
228}
229
230/// Coinbase FCM trading session state.
231#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Display, EnumString)]
232pub enum CoinbaseFcmTradingSessionState {
233    #[serde(rename = "FCM_TRADING_SESSION_STATE_UNDEFINED")]
234    #[strum(serialize = "FCM_TRADING_SESSION_STATE_UNDEFINED")]
235    Undefined,
236    #[serde(rename = "FCM_TRADING_SESSION_STATE_OPEN")]
237    #[strum(serialize = "FCM_TRADING_SESSION_STATE_OPEN")]
238    Open,
239}
240
241/// Coinbase FCM trading session closed reason.
242#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Display, EnumString)]
243pub enum CoinbaseFcmTradingSessionClosedReason {
244    #[serde(rename = "FCM_TRADING_SESSION_CLOSED_REASON_UNDEFINED")]
245    #[strum(serialize = "FCM_TRADING_SESSION_CLOSED_REASON_UNDEFINED")]
246    Undefined,
247    #[serde(rename = "FCM_TRADING_SESSION_CLOSED_REASON_EXCHANGE_MAINTENANCE")]
248    #[strum(serialize = "FCM_TRADING_SESSION_CLOSED_REASON_EXCHANGE_MAINTENANCE")]
249    ExchangeMaintenance,
250}
251
252/// Coinbase risk management owner.
253#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Display, EnumString)]
254pub enum CoinbaseRiskManagedBy {
255    #[serde(rename = "UNKNOWN_RISK_MANAGEMENT_TYPE")]
256    #[strum(serialize = "UNKNOWN_RISK_MANAGEMENT_TYPE")]
257    Unknown,
258    #[serde(rename = "MANAGED_BY_FCM")]
259    #[strum(serialize = "MANAGED_BY_FCM")]
260    ManagedByFcm,
261    #[serde(rename = "MANAGED_BY_VENUE")]
262    #[strum(serialize = "MANAGED_BY_VENUE")]
263    ManagedByVenue,
264}
265
266/// Coinbase account type.
267#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Display, EnumString)]
268#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
269#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
270pub enum CoinbaseAccountType {
271    // Production currently returns the fully qualified wire names
272    // (`ACCOUNT_TYPE_CRYPTO`, `ACCOUNT_TYPE_FIAT`); older documented
273    // examples use the short forms. Accept both on deserialize / parse
274    // but keep the short form as the canonical Display value.
275    #[serde(alias = "ACCOUNT_TYPE_CRYPTO")]
276    #[strum(to_string = "CRYPTO", serialize = "ACCOUNT_TYPE_CRYPTO")]
277    Crypto,
278    #[serde(alias = "ACCOUNT_TYPE_FIAT")]
279    #[strum(to_string = "FIAT", serialize = "ACCOUNT_TYPE_FIAT")]
280    Fiat,
281}
282
283/// Coinbase fill trade type.
284#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Display, EnumString)]
285pub enum CoinbaseFillTradeType {
286    #[serde(rename = "FILL")]
287    #[strum(serialize = "FILL")]
288    Fill,
289}
290
291/// Coinbase FCM position side.
292#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Display, EnumString)]
293pub enum CoinbaseFcmPositionSide {
294    #[serde(rename = "FUTURES_POSITION_SIDE_UNSPECIFIED")]
295    #[strum(serialize = "FUTURES_POSITION_SIDE_UNSPECIFIED")]
296    Unspecified,
297    #[serde(rename = "LONG")]
298    #[strum(serialize = "LONG")]
299    Long,
300    #[serde(rename = "SHORT")]
301    #[strum(serialize = "SHORT")]
302    Short,
303}
304
305/// Coinbase futures margin window type.
306#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Display, EnumString)]
307pub enum CoinbaseMarginWindowType {
308    #[serde(rename = "FCM_MARGIN_WINDOW_TYPE_INTRADAY")]
309    #[strum(serialize = "FCM_MARGIN_WINDOW_TYPE_INTRADAY")]
310    Intraday,
311    #[serde(rename = "FCM_MARGIN_WINDOW_TYPE_OVERNIGHT")]
312    #[strum(serialize = "FCM_MARGIN_WINDOW_TYPE_OVERNIGHT")]
313    Overnight,
314}
315
316/// Coinbase margin level.
317#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Display, EnumString)]
318pub enum CoinbaseMarginLevel {
319    #[serde(rename = "MARGIN_LEVEL_TYPE_BASE")]
320    #[strum(serialize = "MARGIN_LEVEL_TYPE_BASE")]
321    Base,
322}
323
324/// Coinbase contract expiry type for futures products.
325#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Display, EnumString)]
326#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
327#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
328pub enum CoinbaseContractExpiryType {
329    #[serde(
330        rename = "UNKNOWN_CONTRACT_EXPIRY_TYPE",
331        alias = "UNKNOWN_CONTRACT_EXPIRY"
332    )]
333    #[strum(
334        serialize = "UNKNOWN_CONTRACT_EXPIRY_TYPE",
335        serialize = "UNKNOWN_CONTRACT_EXPIRY"
336    )]
337    Unknown,
338    Expiring,
339    /// Non-expiring (perpetual)
340    #[serde(rename = "PERPETUAL")]
341    #[strum(serialize = "PERPETUAL")]
342    Perpetual,
343}
344
345/// Coinbase futures asset type.
346#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Display, EnumString)]
347pub enum CoinbaseFuturesAssetType {
348    #[serde(rename = "FUTURES_ASSET_TYPE_CRYPTO")]
349    #[strum(serialize = "FUTURES_ASSET_TYPE_CRYPTO")]
350    Crypto,
351    #[serde(rename = "FUTURES_ASSET_TYPE_ENERGY")]
352    #[strum(serialize = "FUTURES_ASSET_TYPE_ENERGY")]
353    Energy,
354    #[serde(rename = "FUTURES_ASSET_TYPE_METALS")]
355    #[strum(serialize = "FUTURES_ASSET_TYPE_METALS")]
356    Metals,
357    #[serde(rename = "FUTURES_ASSET_TYPE_STOCKS")]
358    #[strum(serialize = "FUTURES_ASSET_TYPE_STOCKS")]
359    Stocks,
360}
361
362/// Coinbase fill liquidity indicator.
363#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Display, EnumString)]
364#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
365#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
366pub enum CoinbaseLiquidityIndicator {
367    Maker,
368    Taker,
369    Unknown,
370}
371
372/// Coinbase stop order direction.
373#[derive(
374    Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Display, EnumString, AsRefStr,
375)]
376pub enum CoinbaseStopDirection {
377    #[serde(rename = "STOP_DIRECTION_STOP_DOWN")]
378    #[strum(serialize = "STOP_DIRECTION_STOP_DOWN")]
379    StopDown,
380    #[serde(rename = "STOP_DIRECTION_STOP_UP")]
381    #[strum(serialize = "STOP_DIRECTION_STOP_UP")]
382    StopUp,
383}
384
385/// Coinbase candle granularity for historical data.
386#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Display, EnumString)]
387#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
388#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
389pub enum CoinbaseGranularity {
390    OneMinute,
391    FiveMinute,
392    FifteenMinute,
393    ThirtyMinute,
394    OneHour,
395    TwoHour,
396    SixHour,
397    OneDay,
398}
399
400/// Coinbase WebSocket channel type.
401#[derive(
402    Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Display, EnumString, AsRefStr,
403)]
404#[serde(rename_all = "snake_case")]
405#[strum(serialize_all = "snake_case")]
406pub enum CoinbaseWsChannel {
407    Level2,
408    MarketTrades,
409    Ticker,
410    TickerBatch,
411    Candles,
412    User,
413    Heartbeats,
414    FuturesBalanceSummary,
415    Status,
416}
417
418impl CoinbaseWsChannel {
419    /// Returns true if this channel requires authentication.
420    #[must_use]
421    pub fn requires_auth(&self) -> bool {
422        matches!(self, Self::User | Self::FuturesBalanceSummary)
423    }
424}
425
426#[cfg(test)]
427mod tests {
428    use std::str::FromStr;
429
430    use rstest::rstest;
431
432    use super::*;
433
434    #[rstest]
435    #[case(CoinbaseProductType::Spot, "SPOT")]
436    #[case(CoinbaseProductType::Future, "FUTURE")]
437    fn test_product_type_display(#[case] variant: CoinbaseProductType, #[case] expected: &str) {
438        assert_eq!(variant.to_string(), expected);
439    }
440
441    #[rstest]
442    #[case("BUY", CoinbaseOrderSide::Buy)]
443    #[case("SELL", CoinbaseOrderSide::Sell)]
444    fn test_order_side_from_str(#[case] input: &str, #[case] expected: CoinbaseOrderSide) {
445        assert_eq!(CoinbaseOrderSide::from_str(input).unwrap(), expected);
446    }
447
448    #[rstest]
449    #[case(CoinbaseOrderStatus::Filled, true)]
450    #[case(CoinbaseOrderStatus::Cancelled, true)]
451    #[case(CoinbaseOrderStatus::Expired, true)]
452    #[case(CoinbaseOrderStatus::Failed, true)]
453    #[case(CoinbaseOrderStatus::Open, false)]
454    #[case(CoinbaseOrderStatus::Pending, false)]
455    #[case(CoinbaseOrderStatus::Queued, false)]
456    #[case(CoinbaseOrderStatus::CancelQueued, false)]
457    #[case(CoinbaseOrderStatus::EditQueued, false)]
458    #[case(CoinbaseOrderStatus::Unknown, false)]
459    fn test_order_status_is_terminal(#[case] status: CoinbaseOrderStatus, #[case] expected: bool) {
460        assert_eq!(status.is_terminal(), expected);
461    }
462
463    #[rstest]
464    fn test_ws_channel_requires_auth() {
465        assert!(CoinbaseWsChannel::User.requires_auth());
466        assert!(CoinbaseWsChannel::FuturesBalanceSummary.requires_auth());
467        assert!(!CoinbaseWsChannel::Level2.requires_auth());
468        assert!(!CoinbaseWsChannel::MarketTrades.requires_auth());
469        assert!(!CoinbaseWsChannel::Ticker.requires_auth());
470    }
471
472    #[rstest]
473    fn test_order_status_serialization() {
474        let status = CoinbaseOrderStatus::Open;
475        let json = serde_json::to_string(&status).unwrap();
476        assert_eq!(json, "\"OPEN\"");
477
478        let deserialized: CoinbaseOrderStatus = serde_json::from_str("\"CANCELLED\"").unwrap();
479        assert_eq!(deserialized, CoinbaseOrderStatus::Cancelled);
480    }
481
482    #[rstest]
483    fn test_screaming_snake_case_multi_word() {
484        let json = serde_json::to_string(&CoinbaseOrderType::StopLimit).unwrap();
485        assert_eq!(json, "\"STOP_LIMIT\"");
486
487        let json = serde_json::to_string(&CoinbaseOrderStatus::CancelQueued).unwrap();
488        assert_eq!(json, "\"CANCEL_QUEUED\"");
489
490        let json = serde_json::to_string(&CoinbaseTimeInForce::GoodUntilDateTime).unwrap();
491        assert_eq!(json, "\"GOOD_UNTIL_DATE_TIME\"");
492
493        let json = serde_json::to_string(&CoinbaseGranularity::FifteenMinute).unwrap();
494        assert_eq!(json, "\"FIFTEEN_MINUTE\"");
495    }
496
497    #[rstest]
498    fn test_order_type_accepts_title_case_aliases() {
499        let order_type: CoinbaseOrderType = serde_json::from_str("\"Limit\"").unwrap();
500        assert_eq!(order_type, CoinbaseOrderType::Limit);
501
502        let order_type: CoinbaseOrderType = serde_json::from_str("\"StopLimit\"").unwrap();
503        assert_eq!(order_type, CoinbaseOrderType::StopLimit);
504
505        let order_type: CoinbaseOrderType = serde_json::from_str("\"Stop Limit\"").unwrap();
506        assert_eq!(order_type, CoinbaseOrderType::StopLimit);
507
508        let order_type = CoinbaseOrderType::from_str("STOP_LIMIT").unwrap();
509        assert_eq!(order_type, CoinbaseOrderType::StopLimit);
510
511        let order_type = CoinbaseOrderType::from_str("TWAP").unwrap();
512        assert_eq!(order_type, CoinbaseOrderType::Twap);
513
514        let order_type = CoinbaseOrderType::from_str("LIQUIDATION").unwrap();
515        assert_eq!(order_type, CoinbaseOrderType::Liquidation);
516    }
517
518    #[rstest]
519    fn test_account_type_accepts_current_wire_values() {
520        let account_type: CoinbaseAccountType = serde_json::from_str("\"FIAT\"").unwrap();
521        assert_eq!(account_type, CoinbaseAccountType::Fiat);
522
523        let account_type = CoinbaseAccountType::from_str("CRYPTO").unwrap();
524        assert_eq!(account_type, CoinbaseAccountType::Crypto);
525    }
526
527    // Production `/accounts` currently returns the fully qualified wire
528    // names (`ACCOUNT_TYPE_CRYPTO` / `ACCOUNT_TYPE_FIAT`). Both shapes
529    // must parse so account-state bootstrap doesn't fail with
530    // "unknown variant".
531    #[rstest]
532    fn test_account_type_accepts_qualified_wire_values() {
533        let account_type: CoinbaseAccountType =
534            serde_json::from_str("\"ACCOUNT_TYPE_CRYPTO\"").unwrap();
535        assert_eq!(account_type, CoinbaseAccountType::Crypto);
536
537        let account_type: CoinbaseAccountType =
538            serde_json::from_str("\"ACCOUNT_TYPE_FIAT\"").unwrap();
539        assert_eq!(account_type, CoinbaseAccountType::Fiat);
540
541        let account_type = CoinbaseAccountType::from_str("ACCOUNT_TYPE_CRYPTO").unwrap();
542        assert_eq!(account_type, CoinbaseAccountType::Crypto);
543    }
544
545    // Display must keep emitting the short form regardless of input
546    // wire shape; the qualified form is an input-only alias.
547    #[rstest]
548    fn test_account_type_display_uses_short_form() {
549        assert_eq!(CoinbaseAccountType::Crypto.to_string(), "CRYPTO");
550        assert_eq!(CoinbaseAccountType::Fiat.to_string(), "FIAT");
551        assert_eq!(
552            serde_json::to_string(&CoinbaseAccountType::Crypto).unwrap(),
553            "\"CRYPTO\""
554        );
555    }
556
557    #[rstest]
558    fn test_margin_type_accepts_request_and_ws_spellings() {
559        let margin_type: CoinbaseMarginType = serde_json::from_str("\"CROSS\"").unwrap();
560        assert_eq!(margin_type, CoinbaseMarginType::Cross);
561
562        let margin_type: CoinbaseMarginType = serde_json::from_str("\"Cross\"").unwrap();
563        assert_eq!(margin_type, CoinbaseMarginType::Cross);
564    }
565
566    #[rstest]
567    fn test_contract_expiry_type_accepts_websocket_alias() {
568        let expiry_type: CoinbaseContractExpiryType =
569            serde_json::from_str("\"UNKNOWN_CONTRACT_EXPIRY\"").unwrap();
570        assert_eq!(expiry_type, CoinbaseContractExpiryType::Unknown);
571    }
572
573    #[rstest]
574    fn test_ws_channel_snake_case() {
575        let json = serde_json::to_string(&CoinbaseWsChannel::Level2).unwrap();
576        assert_eq!(json, "\"level2\"");
577
578        let json = serde_json::to_string(&CoinbaseWsChannel::MarketTrades).unwrap();
579        assert_eq!(json, "\"market_trades\"");
580
581        let json = serde_json::to_string(&CoinbaseWsChannel::FuturesBalanceSummary).unwrap();
582        assert_eq!(json, "\"futures_balance_summary\"");
583    }
584
585    #[rstest]
586    fn test_unknown_variants_have_qualified_names() {
587        let json = serde_json::to_string(&CoinbaseProductType::Unknown).unwrap();
588        assert_eq!(json, "\"UNKNOWN_PRODUCT_TYPE\"");
589
590        let json = serde_json::to_string(&CoinbaseOrderSide::Unknown).unwrap();
591        assert_eq!(json, "\"UNKNOWN_ORDER_SIDE\"");
592
593        let json = serde_json::to_string(&CoinbaseOrderType::Unknown).unwrap();
594        assert_eq!(json, "\"UNKNOWN_ORDER_TYPE\"");
595
596        let json = serde_json::to_string(&CoinbaseOrderStatus::Unknown).unwrap();
597        assert_eq!(json, "\"UNKNOWN_ORDER_STATUS\"");
598
599        let json = serde_json::to_string(&CoinbaseTimeInForce::Unknown).unwrap();
600        assert_eq!(json, "\"UNKNOWN_TIME_IN_FORCE\"");
601
602        let json = serde_json::to_string(&CoinbaseTriggerStatus::Unknown).unwrap();
603        assert_eq!(json, "\"UNKNOWN_TRIGGER_STATUS\"");
604
605        let json = serde_json::to_string(&CoinbaseOrderPlacementSource::Unknown).unwrap();
606        assert_eq!(json, "\"UNKNOWN_PLACEMENT_SOURCE\"");
607
608        let json = serde_json::to_string(&CoinbaseContractExpiryType::Unknown).unwrap();
609        assert_eq!(json, "\"UNKNOWN_CONTRACT_EXPIRY_TYPE\"");
610
611        let json = serde_json::to_string(&CoinbaseRiskManagedBy::Unknown).unwrap();
612        assert_eq!(json, "\"UNKNOWN_RISK_MANAGEMENT_TYPE\"");
613    }
614}