Skip to main content

nautilus_binance/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//! Binance enumeration types for product types and environments.
17
18use std::fmt::Display;
19
20use nautilus_model::enums::{OrderSide, OrderType, TimeInForce};
21use serde::{Deserialize, Serialize};
22
23/// Binance product type identifier.
24///
25/// Each product type corresponds to a different Binance API domain and
26/// has distinct trading rules and instrument specifications.
27#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
28#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
29#[cfg_attr(
30    feature = "python",
31    pyo3::pyclass(
32        module = "nautilus_trader.core.nautilus_pyo3.binance",
33        eq,
34        from_py_object,
35        rename_all = "SCREAMING_SNAKE_CASE"
36    )
37)]
38pub enum BinanceProductType {
39    /// Spot trading (api.binance.com).
40    #[default]
41    Spot,
42    /// Spot Margin trading (uses Spot API with margin endpoints).
43    Margin,
44    /// USD-M Futures - linear perpetuals and delivery futures (fapi.binance.com).
45    UsdM,
46    /// COIN-M Futures - inverse perpetuals and delivery futures (dapi.binance.com).
47    CoinM,
48    /// European Options (eapi.binance.com).
49    Options,
50}
51
52impl BinanceProductType {
53    /// Returns the string representation used in API requests.
54    #[must_use]
55    pub const fn as_str(self) -> &'static str {
56        match self {
57            Self::Spot => "SPOT",
58            Self::Margin => "MARGIN",
59            Self::UsdM => "USD_M",
60            Self::CoinM => "COIN_M",
61            Self::Options => "OPTIONS",
62        }
63    }
64
65    /// Returns the instrument ID suffix for this product type.
66    #[must_use]
67    pub const fn suffix(self) -> &'static str {
68        match self {
69            Self::Spot => "-SPOT",
70            Self::Margin => "-MARGIN",
71            Self::UsdM => "-LINEAR",
72            Self::CoinM => "-INVERSE",
73            Self::Options => "-OPTION",
74        }
75    }
76
77    /// Returns true if this is a spot product (Spot or Margin).
78    #[must_use]
79    pub const fn is_spot(self) -> bool {
80        matches!(self, Self::Spot | Self::Margin)
81    }
82
83    /// Returns true if this is a futures product (USD-M or COIN-M).
84    #[must_use]
85    pub const fn is_futures(self) -> bool {
86        matches!(self, Self::UsdM | Self::CoinM)
87    }
88
89    /// Returns true if this is a linear product (Spot, Margin, or USD-M).
90    #[must_use]
91    pub const fn is_linear(self) -> bool {
92        matches!(self, Self::Spot | Self::Margin | Self::UsdM)
93    }
94
95    /// Returns true if this is an inverse product (COIN-M).
96    #[must_use]
97    pub const fn is_inverse(self) -> bool {
98        matches!(self, Self::CoinM)
99    }
100
101    /// Returns true if this is an options product.
102    #[must_use]
103    pub const fn is_options(self) -> bool {
104        matches!(self, Self::Options)
105    }
106}
107
108impl Display for BinanceProductType {
109    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
110        write!(f, "{}", self.as_str())
111    }
112}
113
114/// Binance environment type.
115#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
116#[cfg_attr(
117    feature = "python",
118    pyo3::pyclass(
119        module = "nautilus_trader.core.nautilus_pyo3.binance",
120        eq,
121        from_py_object,
122        rename_all = "SCREAMING_SNAKE_CASE"
123    )
124)]
125pub enum BinanceEnvironment {
126    /// Production/mainnet environment.
127    #[default]
128    Mainnet,
129    /// Testnet environment.
130    Testnet,
131    /// Demo trading environment.
132    Demo,
133}
134
135impl BinanceEnvironment {
136    /// Returns true if this is the testnet environment.
137    #[must_use]
138    pub const fn is_testnet(self) -> bool {
139        matches!(self, Self::Testnet)
140    }
141
142    /// Returns true for any non-production environment.
143    #[must_use]
144    pub const fn is_sandbox(self) -> bool {
145        matches!(self, Self::Testnet | Self::Demo)
146    }
147}
148
149/// Order side for Binance orders and trades.
150#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
151#[serde(rename_all = "UPPERCASE")]
152pub enum BinanceSide {
153    /// Buy side.
154    Buy,
155    /// Sell side.
156    Sell,
157}
158
159impl TryFrom<OrderSide> for BinanceSide {
160    type Error = anyhow::Error;
161
162    fn try_from(value: OrderSide) -> Result<Self, Self::Error> {
163        match value {
164            OrderSide::Buy => Ok(Self::Buy),
165            OrderSide::Sell => Ok(Self::Sell),
166            _ => anyhow::bail!("Unsupported `OrderSide` for Binance: {value:?}"),
167        }
168    }
169}
170
171impl From<BinanceSide> for OrderSide {
172    fn from(value: BinanceSide) -> Self {
173        match value {
174            BinanceSide::Buy => Self::Buy,
175            BinanceSide::Sell => Self::Sell,
176        }
177    }
178}
179
180/// Position side for dual-side position mode.
181#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
182#[serde(rename_all = "UPPERCASE")]
183#[cfg_attr(
184    feature = "python",
185    pyo3::pyclass(
186        module = "nautilus_trader.core.nautilus_pyo3.binance",
187        eq,
188        from_py_object
189    )
190)]
191pub enum BinancePositionSide {
192    /// Single position mode (both).
193    Both,
194    /// Long position.
195    Long,
196    /// Short position.
197    Short,
198    /// Unknown or undocumented value.
199    #[serde(other)]
200    Unknown,
201}
202
203/// Margin type applied to a position.
204#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
205#[serde(rename_all = "lowercase")]
206pub enum BinanceMarginType {
207    /// Cross margin.
208    Cross,
209    /// Isolated margin.
210    Isolated,
211    /// Unknown or undocumented value.
212    #[serde(other)]
213    Unknown,
214}
215
216/// Working type for trigger price evaluation.
217#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
218#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
219pub enum BinanceWorkingType {
220    /// Use the contract price.
221    ContractPrice,
222    /// Use the mark price.
223    MarkPrice,
224    /// Unknown or undocumented value.
225    #[serde(other)]
226    Unknown,
227}
228
229/// Order status lifecycle values.
230#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
231#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
232pub enum BinanceOrderStatus {
233    /// Order accepted and working.
234    New,
235    /// Partially filled.
236    PartiallyFilled,
237    /// Fully filled.
238    Filled,
239    /// Canceled by user or system.
240    Canceled,
241    /// Pending cancel (not commonly used).
242    PendingCancel,
243    /// Rejected by exchange.
244    Rejected,
245    /// Expired.
246    Expired,
247    /// Expired in match (IOC/FOK not executed).
248    ExpiredInMatch,
249    /// Unknown or undocumented value.
250    #[serde(other)]
251    Unknown,
252}
253
254/// Algo order status lifecycle values (Binance Futures Algo Service).
255///
256/// These statuses are specific to conditional orders submitted via the
257/// `/fapi/v1/algoOrder` endpoint (STOP_MARKET, STOP_LIMIT, TAKE_PROFIT,
258/// TAKE_PROFIT_MARKET, TRAILING_STOP_MARKET).
259#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
260#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
261pub enum BinanceAlgoStatus {
262    /// Algo order accepted and waiting for trigger condition.
263    New,
264    /// Algo order trigger condition met, forwarding to matching engine.
265    Triggering,
266    /// Algo order successfully placed in matching engine.
267    Triggered,
268    /// Algo order lifecycle completed (check executed qty for fill status).
269    Finished,
270    /// Algo order canceled by user.
271    Canceled,
272    /// Algo order expired (GTD expiration).
273    Expired,
274    /// Algo order rejected by exchange.
275    Rejected,
276    /// Unknown or undocumented value.
277    #[serde(other)]
278    Unknown,
279}
280
281/// Algo order type for Binance Futures Algo Service.
282///
283/// Currently only `Conditional` is supported by Binance.
284#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
285#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
286pub enum BinanceAlgoType {
287    /// Conditional algo order (stop, take-profit, trailing stop).
288    #[default]
289    Conditional,
290    /// Unknown or undocumented value.
291    #[serde(other)]
292    Unknown,
293}
294
295/// Futures order type enumeration.
296#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
297#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
298pub enum BinanceFuturesOrderType {
299    /// Limit order.
300    Limit,
301    /// Market order.
302    Market,
303    /// Stop (stop-limit) order.
304    Stop,
305    /// Stop market order.
306    StopMarket,
307    /// Take profit (limit) order.
308    TakeProfit,
309    /// Take profit market order.
310    TakeProfitMarket,
311    /// Trailing stop market order.
312    TrailingStopMarket,
313    /// Liquidation order created by exchange.
314    Liquidation,
315    /// Auto-deleveraging order created by exchange.
316    Adl,
317    /// Unknown or undocumented value.
318    #[serde(other)]
319    Unknown,
320}
321
322impl From<BinanceFuturesOrderType> for OrderType {
323    fn from(value: BinanceFuturesOrderType) -> Self {
324        match value {
325            BinanceFuturesOrderType::Limit => Self::Limit,
326            BinanceFuturesOrderType::Market => Self::Market,
327            BinanceFuturesOrderType::Stop => Self::StopLimit,
328            BinanceFuturesOrderType::StopMarket => Self::StopMarket,
329            BinanceFuturesOrderType::TakeProfit => Self::LimitIfTouched,
330            BinanceFuturesOrderType::TakeProfitMarket => Self::MarketIfTouched,
331            BinanceFuturesOrderType::TrailingStopMarket => Self::TrailingStopMarket,
332            BinanceFuturesOrderType::Liquidation
333            | BinanceFuturesOrderType::Adl
334            | BinanceFuturesOrderType::Unknown => Self::Market, // Exchange-generated orders
335        }
336    }
337}
338
339/// Time in force options.
340#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
341#[serde(rename_all = "UPPERCASE")]
342pub enum BinanceTimeInForce {
343    /// Good till canceled.
344    Gtc,
345    /// Immediate or cancel.
346    Ioc,
347    /// Fill or kill.
348    Fok,
349    /// Good till crossing (post-only).
350    Gtx,
351    /// Good till date.
352    Gtd,
353    /// Unknown or undocumented value.
354    #[serde(other)]
355    Unknown,
356}
357
358impl TryFrom<TimeInForce> for BinanceTimeInForce {
359    type Error = anyhow::Error;
360
361    fn try_from(value: TimeInForce) -> Result<Self, Self::Error> {
362        match value {
363            TimeInForce::Gtc => Ok(Self::Gtc),
364            TimeInForce::Ioc => Ok(Self::Ioc),
365            TimeInForce::Fok => Ok(Self::Fok),
366            TimeInForce::Gtd => Ok(Self::Gtd),
367            _ => anyhow::bail!("Unsupported `TimeInForce` for Binance: {value:?}"),
368        }
369    }
370}
371
372/// Income type for account income history.
373#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
374#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
375pub enum BinanceIncomeType {
376    /// Internal transfers.
377    Transfer,
378    /// Welcome bonus.
379    WelcomeBonus,
380    /// Realized profit and loss.
381    RealizedPnl,
382    /// Funding fee payments/receipts.
383    FundingFee,
384    /// Trading commission.
385    Commission,
386    /// Insurance clear.
387    InsuranceClear,
388    /// Referral kickback.
389    ReferralKickback,
390    /// Unknown or undocumented value.
391    #[serde(other)]
392    Unknown,
393}
394
395/// Price match mode for futures maker orders.
396#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
397#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
398pub enum BinancePriceMatch {
399    /// Match opposing side (default).
400    Opponent,
401    /// Match opposing side with 5 tick offset.
402    Opponent5,
403    /// Match opposing side with 10 tick offset.
404    Opponent10,
405    /// Match opposing side with 20 tick offset.
406    Opponent20,
407    /// Join current queue on same side.
408    Queue,
409    /// Join queue with 5 tick offset.
410    Queue5,
411    /// Join queue with 10 tick offset.
412    Queue10,
413    /// Join queue with 20 tick offset.
414    Queue20,
415    /// Unknown or undocumented value.
416    #[serde(other)]
417    Unknown,
418}
419
420/// Self-trade prevention mode.
421#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
422#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
423pub enum BinanceSelfTradePreventionMode {
424    /// Expire maker orders on self-trade.
425    ExpireMaker,
426    /// Expire taker orders on self-trade.
427    ExpireTaker,
428    /// Expire both sides on self-trade.
429    ExpireBoth,
430    /// Unknown or undocumented value.
431    #[serde(other)]
432    Unknown,
433}
434
435/// Trading status for symbols.
436#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
437#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
438pub enum BinanceTradingStatus {
439    /// Trading is active.
440    Trading,
441    /// Pending activation.
442    PendingTrading,
443    /// Pre-trading session.
444    PreTrading,
445    /// Post-trading session.
446    PostTrading,
447    /// End of day.
448    EndOfDay,
449    /// Trading halted.
450    Halt,
451    /// Auction match.
452    AuctionMatch,
453    /// Break period.
454    Break,
455    /// Unknown or undocumented value.
456    #[serde(other)]
457    Unknown,
458}
459
460/// Contract status for coin-margined futures.
461#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
462#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
463pub enum BinanceContractStatus {
464    /// Trading is active.
465    Trading,
466    /// Pending trading.
467    PendingTrading,
468    /// Pre-delivering.
469    PreDelivering,
470    /// Delivering.
471    Delivering,
472    /// Delivered.
473    Delivered,
474    /// Pre-delist.
475    PreDelisting,
476    /// Delisting in progress.
477    Delisting,
478    /// Contract down.
479    Down,
480    /// Unknown or undocumented value.
481    #[serde(other)]
482    Unknown,
483}
484
485/// WebSocket stream event types.
486///
487/// These are the "e" field values in WebSocket JSON messages.
488#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
489#[serde(rename_all = "camelCase")]
490pub enum BinanceWsEventType {
491    /// Aggregate trade event.
492    AggTrade,
493    /// Individual trade event.
494    Trade,
495    /// Book ticker (best bid/ask) event.
496    BookTicker,
497    /// Depth update (order book delta) event.
498    DepthUpdate,
499    /// Mark price update event.
500    MarkPriceUpdate,
501    /// Kline/candlestick event.
502    Kline,
503    /// Forced liquidation order event.
504    ForceOrder,
505    /// 24-hour rolling ticker event.
506    #[serde(rename = "24hrTicker")]
507    Ticker24Hr,
508    /// 24-hour rolling mini ticker event.
509    #[serde(rename = "24hrMiniTicker")]
510    MiniTicker24Hr,
511
512    // User data stream events
513    /// Account update (balance and position changes).
514    #[serde(rename = "ACCOUNT_UPDATE")]
515    AccountUpdate,
516    /// Order/trade update event.
517    #[serde(rename = "ORDER_TRADE_UPDATE")]
518    OrderTradeUpdate,
519    /// Algo order update event (Binance Futures Algo Service).
520    #[serde(rename = "ALGO_UPDATE")]
521    AlgoUpdate,
522    /// Margin call warning event.
523    #[serde(rename = "MARGIN_CALL")]
524    MarginCall,
525    /// Account configuration update (leverage change).
526    #[serde(rename = "ACCOUNT_CONFIG_UPDATE")]
527    AccountConfigUpdate,
528    /// Listen key expired event.
529    #[serde(rename = "listenKeyExpired")]
530    ListenKeyExpired,
531
532    /// Unknown or undocumented event type.
533    #[serde(other)]
534    Unknown,
535}
536
537impl BinanceWsEventType {
538    /// Returns the wire format string for this event type.
539    #[must_use]
540    pub const fn as_str(self) -> &'static str {
541        match self {
542            Self::AggTrade => "aggTrade",
543            Self::Trade => "trade",
544            Self::BookTicker => "bookTicker",
545            Self::DepthUpdate => "depthUpdate",
546            Self::MarkPriceUpdate => "markPriceUpdate",
547            Self::Kline => "kline",
548            Self::ForceOrder => "forceOrder",
549            Self::Ticker24Hr => "24hrTicker",
550            Self::MiniTicker24Hr => "24hrMiniTicker",
551            Self::AccountUpdate => "ACCOUNT_UPDATE",
552            Self::OrderTradeUpdate => "ORDER_TRADE_UPDATE",
553            Self::AlgoUpdate => "ALGO_UPDATE",
554            Self::MarginCall => "MARGIN_CALL",
555            Self::AccountConfigUpdate => "ACCOUNT_CONFIG_UPDATE",
556            Self::ListenKeyExpired => "listenKeyExpired",
557            Self::Unknown => "unknown",
558        }
559    }
560}
561
562impl Display for BinanceWsEventType {
563    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
564        write!(f, "{}", self.as_str())
565    }
566}
567
568/// WebSocket request method (operation type).
569///
570/// Used for subscription requests on both Spot and Futures WebSocket APIs.
571#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
572#[serde(rename_all = "UPPERCASE")]
573pub enum BinanceWsMethod {
574    /// Subscribe to streams.
575    Subscribe,
576    /// Unsubscribe from streams.
577    Unsubscribe,
578}
579
580/// Filter type identifiers returned in exchange info.
581#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
582#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
583pub enum BinanceFilterType {
584    /// Price filter.
585    PriceFilter,
586    /// Percent price filter.
587    PercentPrice,
588    /// Lot size filter.
589    LotSize,
590    /// Market lot size filter.
591    MarketLotSize,
592    /// Notional filter (spot).
593    Notional,
594    /// Min notional filter (futures).
595    MinNotional,
596    /// Maximum number of orders filter.
597    MaxNumOrders,
598    /// Maximum number of algo orders filter.
599    MaxNumAlgoOrders,
600    /// Unknown or undocumented value.
601    #[serde(other)]
602    Unknown,
603}
604
605impl Display for BinanceEnvironment {
606    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
607        match self {
608            Self::Mainnet => write!(f, "Mainnet"),
609            Self::Testnet => write!(f, "Testnet"),
610            Self::Demo => write!(f, "Demo"),
611        }
612    }
613}
614
615/// Rate limit type for API request quotas.
616#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
617pub enum BinanceRateLimitType {
618    RequestWeight,
619    Orders,
620}
621
622/// Rate limit time interval.
623#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
624pub enum BinanceRateLimitInterval {
625    Second,
626    Minute,
627    Day,
628}
629
630/// Kline (candlestick) interval.
631///
632/// # References
633/// - <https://developers.binance.com/docs/binance-spot-api-docs/rest-api/market-data-endpoints>
634#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
635pub enum BinanceKlineInterval {
636    /// 1 second (only for spot).
637    #[serde(rename = "1s")]
638    Second1,
639    /// 1 minute.
640    #[default]
641    #[serde(rename = "1m")]
642    Minute1,
643    /// 3 minutes.
644    #[serde(rename = "3m")]
645    Minute3,
646    /// 5 minutes.
647    #[serde(rename = "5m")]
648    Minute5,
649    /// 15 minutes.
650    #[serde(rename = "15m")]
651    Minute15,
652    /// 30 minutes.
653    #[serde(rename = "30m")]
654    Minute30,
655    /// 1 hour.
656    #[serde(rename = "1h")]
657    Hour1,
658    /// 2 hours.
659    #[serde(rename = "2h")]
660    Hour2,
661    /// 4 hours.
662    #[serde(rename = "4h")]
663    Hour4,
664    /// 6 hours.
665    #[serde(rename = "6h")]
666    Hour6,
667    /// 8 hours.
668    #[serde(rename = "8h")]
669    Hour8,
670    /// 12 hours.
671    #[serde(rename = "12h")]
672    Hour12,
673    /// 1 day.
674    #[serde(rename = "1d")]
675    Day1,
676    /// 3 days.
677    #[serde(rename = "3d")]
678    Day3,
679    /// 1 week.
680    #[serde(rename = "1w")]
681    Week1,
682    /// 1 month.
683    #[serde(rename = "1M")]
684    Month1,
685}
686
687impl BinanceKlineInterval {
688    /// Returns the string representation used by Binance API.
689    #[must_use]
690    pub const fn as_str(&self) -> &'static str {
691        match self {
692            Self::Second1 => "1s",
693            Self::Minute1 => "1m",
694            Self::Minute3 => "3m",
695            Self::Minute5 => "5m",
696            Self::Minute15 => "15m",
697            Self::Minute30 => "30m",
698            Self::Hour1 => "1h",
699            Self::Hour2 => "2h",
700            Self::Hour4 => "4h",
701            Self::Hour6 => "6h",
702            Self::Hour8 => "8h",
703            Self::Hour12 => "12h",
704            Self::Day1 => "1d",
705            Self::Day3 => "3d",
706            Self::Week1 => "1w",
707            Self::Month1 => "1M",
708        }
709    }
710}
711
712#[cfg(test)]
713mod tests {
714    use rstest::rstest;
715
716    use super::*;
717
718    #[rstest]
719    fn test_product_type_as_str() {
720        assert_eq!(BinanceProductType::Spot.as_str(), "SPOT");
721        assert_eq!(BinanceProductType::Margin.as_str(), "MARGIN");
722        assert_eq!(BinanceProductType::UsdM.as_str(), "USD_M");
723        assert_eq!(BinanceProductType::CoinM.as_str(), "COIN_M");
724        assert_eq!(BinanceProductType::Options.as_str(), "OPTIONS");
725    }
726
727    #[rstest]
728    fn test_product_type_suffix() {
729        assert_eq!(BinanceProductType::Spot.suffix(), "-SPOT");
730        assert_eq!(BinanceProductType::Margin.suffix(), "-MARGIN");
731        assert_eq!(BinanceProductType::UsdM.suffix(), "-LINEAR");
732        assert_eq!(BinanceProductType::CoinM.suffix(), "-INVERSE");
733        assert_eq!(BinanceProductType::Options.suffix(), "-OPTION");
734    }
735
736    #[rstest]
737    fn test_product_type_predicates() {
738        assert!(BinanceProductType::Spot.is_spot());
739        assert!(BinanceProductType::Margin.is_spot());
740        assert!(!BinanceProductType::UsdM.is_spot());
741
742        assert!(BinanceProductType::UsdM.is_futures());
743        assert!(BinanceProductType::CoinM.is_futures());
744        assert!(!BinanceProductType::Spot.is_futures());
745
746        assert!(BinanceProductType::CoinM.is_inverse());
747        assert!(!BinanceProductType::UsdM.is_inverse());
748
749        assert!(BinanceProductType::Options.is_options());
750        assert!(!BinanceProductType::Spot.is_options());
751    }
752}