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::{MarketStatusAction, 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.adapters.binance",
33        eq,
34        from_py_object,
35        rename_all = "SCREAMING_SNAKE_CASE"
36    )
37)]
38#[cfg_attr(
39    feature = "python",
40    pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.adapters.binance")
41)]
42pub enum BinanceProductType {
43    /// Spot trading (api.binance.com).
44    #[default]
45    Spot,
46    /// Spot Margin trading (uses Spot API with margin endpoints).
47    Margin,
48    /// USD-M Futures - linear perpetuals and delivery futures (fapi.binance.com).
49    UsdM,
50    /// COIN-M Futures - inverse perpetuals and delivery futures (dapi.binance.com).
51    CoinM,
52    /// European Options (eapi.binance.com).
53    Options,
54}
55
56impl BinanceProductType {
57    /// Returns the string representation used in API requests.
58    #[must_use]
59    pub const fn as_str(self) -> &'static str {
60        match self {
61            Self::Spot => "SPOT",
62            Self::Margin => "MARGIN",
63            Self::UsdM => "USD_M",
64            Self::CoinM => "COIN_M",
65            Self::Options => "OPTIONS",
66        }
67    }
68
69    /// Returns the instrument ID suffix for this product type.
70    #[must_use]
71    pub const fn suffix(self) -> &'static str {
72        match self {
73            Self::Spot => "-SPOT",
74            Self::Margin => "-MARGIN",
75            Self::UsdM => "-LINEAR",
76            Self::CoinM => "-INVERSE",
77            Self::Options => "-OPTION",
78        }
79    }
80
81    /// Returns true if this is a spot product (Spot or Margin).
82    #[must_use]
83    pub const fn is_spot(self) -> bool {
84        matches!(self, Self::Spot | Self::Margin)
85    }
86
87    /// Returns true if this is a futures product (USD-M or COIN-M).
88    #[must_use]
89    pub const fn is_futures(self) -> bool {
90        matches!(self, Self::UsdM | Self::CoinM)
91    }
92
93    /// Returns true if this is a linear product (Spot, Margin, or USD-M).
94    #[must_use]
95    pub const fn is_linear(self) -> bool {
96        matches!(self, Self::Spot | Self::Margin | Self::UsdM)
97    }
98
99    /// Returns true if this is an inverse product (COIN-M).
100    #[must_use]
101    pub const fn is_inverse(self) -> bool {
102        matches!(self, Self::CoinM)
103    }
104
105    /// Returns true if this is an options product.
106    #[must_use]
107    pub const fn is_options(self) -> bool {
108        matches!(self, Self::Options)
109    }
110}
111
112impl Display for BinanceProductType {
113    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
114        write!(f, "{}", self.as_str())
115    }
116}
117
118/// Binance environment type.
119#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
120#[cfg_attr(
121    feature = "python",
122    pyo3::pyclass(
123        module = "nautilus_trader.adapters.binance",
124        eq,
125        from_py_object,
126        rename_all = "SCREAMING_SNAKE_CASE"
127    )
128)]
129#[cfg_attr(
130    feature = "python",
131    pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.adapters.binance")
132)]
133pub enum BinanceEnvironment {
134    /// Live exchange environment.
135    #[default]
136    Live,
137    /// Testnet environment.
138    Testnet,
139    /// Demo trading environment.
140    Demo,
141}
142
143impl BinanceEnvironment {
144    /// Returns true if this is the testnet environment.
145    #[must_use]
146    pub const fn is_testnet(self) -> bool {
147        matches!(self, Self::Testnet)
148    }
149
150    /// Returns true for any non-production environment.
151    #[must_use]
152    pub const fn is_sandbox(self) -> bool {
153        matches!(self, Self::Testnet | Self::Demo)
154    }
155}
156
157/// Order side for Binance orders and trades.
158#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
159#[serde(rename_all = "UPPERCASE")]
160pub enum BinanceSide {
161    /// Buy side.
162    Buy,
163    /// Sell side.
164    Sell,
165}
166
167impl TryFrom<OrderSide> for BinanceSide {
168    type Error = anyhow::Error;
169
170    fn try_from(value: OrderSide) -> Result<Self, Self::Error> {
171        match value {
172            OrderSide::Buy => Ok(Self::Buy),
173            OrderSide::Sell => Ok(Self::Sell),
174        }
175    }
176}
177
178impl From<BinanceSide> for OrderSide {
179    fn from(value: BinanceSide) -> Self {
180        match value {
181            BinanceSide::Buy => Self::Buy,
182            BinanceSide::Sell => Self::Sell,
183        }
184    }
185}
186
187/// Position side for dual-side position mode.
188#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
189#[serde(rename_all = "UPPERCASE")]
190#[cfg_attr(
191    feature = "python",
192    pyo3::pyclass(module = "nautilus_trader.adapters.binance", eq, from_py_object)
193)]
194#[cfg_attr(
195    feature = "python",
196    pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.adapters.binance")
197)]
198pub enum BinancePositionSide {
199    /// Single position mode (both).
200    Both,
201    /// Long position.
202    Long,
203    /// Short position.
204    Short,
205    /// Unknown or undocumented value.
206    #[serde(other)]
207    Unknown,
208}
209
210/// Margin type applied to a position.
211///
212/// Serializes to the POST format (`CROSSED`/`ISOLATED`) expected by
213/// `/fapi/v1/marginType`. Deserializes from both POST and GET/WS
214/// formats (`cross`/`isolated`) via serde aliases.
215#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
216#[cfg_attr(
217    feature = "python",
218    pyo3::pyclass(
219        module = "nautilus_trader.adapters.binance",
220        eq,
221        from_py_object,
222        rename_all = "SCREAMING_SNAKE_CASE"
223    )
224)]
225#[cfg_attr(
226    feature = "python",
227    pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.adapters.binance")
228)]
229pub enum BinanceMarginType {
230    /// Cross margin.
231    #[serde(rename = "CROSSED", alias = "cross")]
232    Cross,
233    /// Isolated margin.
234    #[serde(rename = "ISOLATED", alias = "isolated")]
235    Isolated,
236    /// Unknown or undocumented value.
237    #[default]
238    #[serde(other)]
239    Unknown,
240}
241
242/// Working type for trigger price evaluation.
243#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
244#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
245pub enum BinanceWorkingType {
246    /// Use the contract price.
247    ContractPrice,
248    /// Use the mark price.
249    MarkPrice,
250    /// Unknown or undocumented value.
251    #[serde(other)]
252    Unknown,
253}
254
255/// Order status lifecycle values.
256#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
257#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
258pub enum BinanceOrderStatus {
259    /// Order accepted and working.
260    New,
261    /// Pending new (order list accepted but not yet on book).
262    PendingNew,
263    /// Partially filled.
264    PartiallyFilled,
265    /// Fully filled.
266    Filled,
267    /// Canceled by user or system.
268    Canceled,
269    /// Pending cancel (not commonly used).
270    PendingCancel,
271    /// Rejected by exchange.
272    Rejected,
273    /// Expired.
274    Expired,
275    /// Expired in match (IOC/FOK not executed).
276    ExpiredInMatch,
277    /// Liquidation with insurance fund.
278    NewInsurance,
279    /// Counterparty liquidation (Auto-Deleveraging).
280    NewAdl,
281    /// Unknown or undocumented value.
282    #[serde(other)]
283    Unknown,
284}
285
286/// Algo order status lifecycle values (Binance Futures Algo Service).
287///
288/// These statuses are specific to conditional orders submitted via the
289/// `/fapi/v1/algoOrder` endpoint (STOP_MARKET, STOP_LIMIT, TAKE_PROFIT,
290/// TAKE_PROFIT_MARKET, TRAILING_STOP_MARKET).
291#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
292#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
293pub enum BinanceAlgoStatus {
294    /// Algo order accepted and waiting for trigger condition.
295    New,
296    /// Algo order trigger condition met, forwarding to matching engine.
297    Triggering,
298    /// Algo order successfully placed in matching engine.
299    Triggered,
300    /// Algo order lifecycle completed (check executed qty for fill status).
301    Finished,
302    /// Algo order canceled by user.
303    Canceled,
304    /// Algo order expired (GTD expiration).
305    Expired,
306    /// Algo order rejected by exchange.
307    Rejected,
308    /// Unknown or undocumented value.
309    #[serde(other)]
310    Unknown,
311}
312
313/// Algo order type for Binance Futures Algo Service.
314///
315/// Currently only `Conditional` is supported by Binance.
316#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
317#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
318pub enum BinanceAlgoType {
319    /// Conditional algo order (stop, take-profit, trailing stop).
320    #[default]
321    Conditional,
322    /// Unknown or undocumented value.
323    #[serde(other)]
324    Unknown,
325}
326
327/// Futures order type enumeration.
328#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
329#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
330pub enum BinanceFuturesOrderType {
331    /// Limit order.
332    Limit,
333    /// Market order.
334    Market,
335    /// Stop (stop-limit) order.
336    Stop,
337    /// Stop market order.
338    StopMarket,
339    /// Take profit (limit) order.
340    TakeProfit,
341    /// Take profit market order.
342    TakeProfitMarket,
343    /// Trailing stop market order.
344    TrailingStopMarket,
345    /// Liquidation order created by exchange.
346    Liquidation,
347    /// Auto-deleveraging order created by exchange.
348    Adl,
349    /// Unknown or undocumented value.
350    #[serde(other)]
351    Unknown,
352}
353
354impl TryFrom<BinanceFuturesOrderType> for OrderType {
355    type Error = anyhow::Error;
356
357    fn try_from(value: BinanceFuturesOrderType) -> Result<Self, Self::Error> {
358        Ok(match value {
359            BinanceFuturesOrderType::Limit => Self::Limit,
360            BinanceFuturesOrderType::Market => Self::Market,
361            BinanceFuturesOrderType::Stop => Self::StopLimit,
362            BinanceFuturesOrderType::StopMarket => Self::StopMarket,
363            BinanceFuturesOrderType::TakeProfit => Self::LimitIfTouched,
364            BinanceFuturesOrderType::TakeProfitMarket => Self::MarketIfTouched,
365            BinanceFuturesOrderType::TrailingStopMarket => Self::TrailingStopMarket,
366            BinanceFuturesOrderType::Liquidation | BinanceFuturesOrderType::Adl => Self::Market,
367            BinanceFuturesOrderType::Unknown => anyhow::bail!("unknown Binance Futures order type"),
368        })
369    }
370}
371
372/// Time in force options.
373#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
374#[serde(rename_all = "UPPERCASE")]
375pub enum BinanceTimeInForce {
376    /// Good till canceled.
377    Gtc,
378    /// Immediate or cancel.
379    Ioc,
380    /// Fill or kill.
381    Fok,
382    /// Good till crossing (post-only).
383    Gtx,
384    /// Good till date.
385    Gtd,
386    /// Retail Price Improvement (USD-M Futures).
387    Rpi,
388    /// Unknown or undocumented value.
389    #[serde(other)]
390    Unknown,
391}
392
393impl TryFrom<TimeInForce> for BinanceTimeInForce {
394    type Error = anyhow::Error;
395
396    fn try_from(value: TimeInForce) -> Result<Self, Self::Error> {
397        match value {
398            TimeInForce::Gtc => Ok(Self::Gtc),
399            TimeInForce::Ioc => Ok(Self::Ioc),
400            TimeInForce::Fok => Ok(Self::Fok),
401            TimeInForce::Gtd => Ok(Self::Gtd),
402            _ => anyhow::bail!("Unsupported `TimeInForce` for Binance: {value:?}"),
403        }
404    }
405}
406
407/// Income type for account income history.
408#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
409#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
410pub enum BinanceIncomeType {
411    /// Internal transfers.
412    Transfer,
413    /// Welcome bonus.
414    WelcomeBonus,
415    /// Realized profit and loss.
416    RealizedPnl,
417    /// Funding fee payments/receipts.
418    FundingFee,
419    /// Trading commission.
420    Commission,
421    /// Commission rebate.
422    CommissionRebate,
423    /// API rebate.
424    ApiRebate,
425    /// Insurance clear.
426    InsuranceClear,
427    /// Referral kickback.
428    ReferralKickback,
429    /// Contest reward.
430    ContestReward,
431    /// Cross collateral transfer.
432    CrossCollateralTransfer,
433    /// Options premium fee.
434    OptionsPremiumFee,
435    /// Options settle profit.
436    OptionsSettleProfit,
437    /// Internal transfer.
438    InternalTransfer,
439    /// Auto exchange.
440    AutoExchange,
441    /// Delivered settlement.
442    #[serde(rename = "DELIVERED_SETTELMENT")]
443    DeliveredSettlement,
444    /// Coin swap deposit.
445    CoinSwapDeposit,
446    /// Coin swap withdraw.
447    CoinSwapWithdraw,
448    /// Position limit increase fee.
449    PositionLimitIncreaseFee,
450    /// Strategy UM futures transfer.
451    StrategyUmfuturesTransfer,
452    /// Fee return.
453    FeeReturn,
454    /// BFUSD reward.
455    BfusdReward,
456    /// Unknown or undocumented value.
457    #[serde(other)]
458    Unknown,
459}
460
461/// Price match mode for futures maker orders.
462#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
463#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
464pub enum BinancePriceMatch {
465    /// No price match (default).
466    None,
467    /// Match opposing side.
468    Opponent,
469    /// Match opposing side with 5 tick offset.
470    #[serde(rename = "OPPONENT_5")]
471    Opponent5,
472    /// Match opposing side with 10 tick offset.
473    #[serde(rename = "OPPONENT_10")]
474    Opponent10,
475    /// Match opposing side with 20 tick offset.
476    #[serde(rename = "OPPONENT_20")]
477    Opponent20,
478    /// Join current queue on same side.
479    Queue,
480    /// Join queue with 5 tick offset.
481    #[serde(rename = "QUEUE_5")]
482    Queue5,
483    /// Join queue with 10 tick offset.
484    #[serde(rename = "QUEUE_10")]
485    Queue10,
486    /// Join queue with 20 tick offset.
487    #[serde(rename = "QUEUE_20")]
488    Queue20,
489    /// Unknown or undocumented value.
490    #[serde(other)]
491    Unknown,
492}
493
494impl BinancePriceMatch {
495    /// Parses a price match mode from a string param value.
496    ///
497    /// Accepts uppercase Binance API values like `"OPPONENT"`, `"OPPONENT_5"`, `"QUEUE_10"`.
498    ///
499    /// # Errors
500    ///
501    /// Returns an error if the value is not a recognized price match mode.
502    pub fn from_param(s: &str) -> anyhow::Result<Self> {
503        let value = s.to_uppercase();
504        serde_json::from_value(serde_json::Value::String(value))
505            .map_err(|_| anyhow::anyhow!("Invalid price_match value: {s:?}"))
506            .and_then(|pm: Self| {
507                if pm == Self::None || pm == Self::Unknown {
508                    anyhow::bail!("Invalid price_match value: {s:?}")
509                }
510                Ok(pm)
511            })
512    }
513}
514
515/// Self-trade prevention mode.
516#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
517#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
518pub enum BinanceSelfTradePreventionMode {
519    /// No self-trade prevention.
520    None,
521    /// Expire maker orders on self-trade.
522    ExpireMaker,
523    /// Expire taker orders on self-trade.
524    ExpireTaker,
525    /// Expire both sides on self-trade.
526    ExpireBoth,
527    /// Decrement and cancel (spot).
528    Decrement,
529    /// Transfer to sub-account (spot).
530    Transfer,
531    /// Unknown or undocumented value.
532    #[serde(other)]
533    Unknown,
534}
535
536/// Trading status for symbols.
537#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
538#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
539pub enum BinanceTradingStatus {
540    /// Trading is active.
541    Trading,
542    /// Pending activation.
543    PendingTrading,
544    /// Pre-trading session.
545    PreTrading,
546    /// Post-trading session.
547    PostTrading,
548    /// End of day.
549    EndOfDay,
550    /// Trading halted.
551    Halt,
552    /// Auction match.
553    AuctionMatch,
554    /// Break period.
555    Break,
556    /// Pre-delivering.
557    PreDelivering,
558    /// Delivering.
559    Delivering,
560    /// Delivered.
561    Delivered,
562    /// Pre-settlement.
563    PreSettle,
564    /// Settling.
565    Settling,
566    /// Closed.
567    Close,
568    /// Trading is halted for an otherwise active contract.
569    TradingHalt,
570    /// New orders are blocked while cancellation remains available.
571    TradingCancelOnly,
572    /// Unknown or undocumented value.
573    #[serde(other)]
574    Unknown,
575}
576
577impl From<BinanceTradingStatus> for MarketStatusAction {
578    fn from(status: BinanceTradingStatus) -> Self {
579        match status {
580            BinanceTradingStatus::Trading => Self::Trading,
581            BinanceTradingStatus::PendingTrading | BinanceTradingStatus::PreTrading => {
582                Self::PreOpen
583            }
584            BinanceTradingStatus::PostTrading => Self::PostClose,
585            BinanceTradingStatus::EndOfDay => Self::Close,
586            BinanceTradingStatus::Halt => Self::Halt,
587            BinanceTradingStatus::AuctionMatch => Self::Cross,
588            BinanceTradingStatus::Break => Self::Pause,
589            BinanceTradingStatus::PreDelivering | BinanceTradingStatus::PreSettle => Self::PreClose,
590            BinanceTradingStatus::Delivering
591            | BinanceTradingStatus::Delivered
592            | BinanceTradingStatus::Settling
593            | BinanceTradingStatus::Close => Self::Close,
594            BinanceTradingStatus::TradingHalt | BinanceTradingStatus::TradingCancelOnly => {
595                Self::Halt
596            }
597            BinanceTradingStatus::Unknown => Self::NotAvailableForTrading,
598        }
599    }
600}
601
602/// Contract status for coin-margined futures.
603#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
604#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
605pub enum BinanceContractStatus {
606    /// Trading is active.
607    Trading,
608    /// Trading is halted for an otherwise active contract.
609    TradingHalt,
610    /// Pending trading.
611    PendingTrading,
612    /// Pre-delivering.
613    PreDelivering,
614    /// Delivering.
615    Delivering,
616    /// Delivered.
617    Delivered,
618    /// Pre-settle.
619    PreSettle,
620    /// Settling.
621    Settling,
622    /// Closed.
623    Close,
624    /// Pre-delist.
625    PreDelisting,
626    /// Delisting in progress.
627    Delisting,
628    /// Contract down.
629    Down,
630    /// New orders are blocked while cancellation remains available.
631    TradingCancelOnly,
632    /// Unknown or undocumented value.
633    #[serde(other)]
634    Unknown,
635}
636
637impl From<BinanceContractStatus> for MarketStatusAction {
638    fn from(status: BinanceContractStatus) -> Self {
639        match status {
640            BinanceContractStatus::Trading => Self::Trading,
641            BinanceContractStatus::TradingHalt | BinanceContractStatus::TradingCancelOnly => {
642                Self::Halt
643            }
644            BinanceContractStatus::PendingTrading => Self::PreOpen,
645            BinanceContractStatus::PreDelivering
646            | BinanceContractStatus::PreDelisting
647            | BinanceContractStatus::PreSettle => Self::PreClose,
648            BinanceContractStatus::Delivering
649            | BinanceContractStatus::Delivered
650            | BinanceContractStatus::Settling
651            | BinanceContractStatus::Close => Self::Close,
652            BinanceContractStatus::Delisting => Self::Suspend,
653            BinanceContractStatus::Down | BinanceContractStatus::Unknown => {
654                Self::NotAvailableForTrading
655            }
656        }
657    }
658}
659
660/// WebSocket stream event types.
661///
662/// These are the "e" field values in WebSocket JSON messages.
663#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
664#[serde(rename_all = "camelCase")]
665pub enum BinanceWsEventType {
666    /// Aggregate trade event.
667    AggTrade,
668    /// Individual trade event.
669    Trade,
670    /// Book ticker (best bid/ask) event.
671    BookTicker,
672    /// Depth update (order book delta) event.
673    DepthUpdate,
674    /// Mark price update event.
675    MarkPriceUpdate,
676    /// Kline/candlestick event.
677    Kline,
678    /// Forced liquidation order event.
679    ForceOrder,
680    /// 24-hour rolling ticker event.
681    #[serde(rename = "24hrTicker")]
682    Ticker24Hr,
683    /// 24-hour rolling mini ticker event.
684    #[serde(rename = "24hrMiniTicker")]
685    MiniTicker24Hr,
686
687    // User data stream events
688    /// Account update (balance and position changes).
689    #[serde(rename = "ACCOUNT_UPDATE")]
690    AccountUpdate,
691    /// Order/trade update event.
692    #[serde(rename = "ORDER_TRADE_UPDATE")]
693    OrderTradeUpdate,
694    /// Trade Lite event (low-latency fill notification).
695    #[serde(rename = "TRADE_LITE")]
696    TradeLite,
697    /// Algo order update event (Binance Futures Algo Service).
698    #[serde(rename = "ALGO_UPDATE")]
699    AlgoUpdate,
700    /// Margin call warning event.
701    #[serde(rename = "MARGIN_CALL")]
702    MarginCall,
703    /// Account configuration update (leverage change).
704    #[serde(rename = "ACCOUNT_CONFIG_UPDATE")]
705    AccountConfigUpdate,
706    /// Listen key expired event.
707    #[serde(rename = "listenKeyExpired")]
708    ListenKeyExpired,
709
710    /// Unknown or undocumented event type.
711    #[serde(other)]
712    Unknown,
713}
714
715impl BinanceWsEventType {
716    /// Returns the wire format string for this event type.
717    #[must_use]
718    pub const fn as_str(self) -> &'static str {
719        match self {
720            Self::AggTrade => "aggTrade",
721            Self::Trade => "trade",
722            Self::BookTicker => "bookTicker",
723            Self::DepthUpdate => "depthUpdate",
724            Self::MarkPriceUpdate => "markPriceUpdate",
725            Self::Kline => "kline",
726            Self::ForceOrder => "forceOrder",
727            Self::Ticker24Hr => "24hrTicker",
728            Self::MiniTicker24Hr => "24hrMiniTicker",
729            Self::AccountUpdate => "ACCOUNT_UPDATE",
730            Self::OrderTradeUpdate => "ORDER_TRADE_UPDATE",
731            Self::TradeLite => "TRADE_LITE",
732            Self::AlgoUpdate => "ALGO_UPDATE",
733            Self::MarginCall => "MARGIN_CALL",
734            Self::AccountConfigUpdate => "ACCOUNT_CONFIG_UPDATE",
735            Self::ListenKeyExpired => "listenKeyExpired",
736            Self::Unknown => "unknown",
737        }
738    }
739}
740
741impl Display for BinanceWsEventType {
742    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
743        write!(f, "{}", self.as_str())
744    }
745}
746
747/// WebSocket request method (operation type).
748///
749/// Used for subscription requests on both Spot and Futures WebSocket APIs.
750#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
751#[serde(rename_all = "UPPERCASE")]
752pub enum BinanceWsMethod {
753    /// Subscribe to streams.
754    Subscribe,
755    /// Unsubscribe from streams.
756    Unsubscribe,
757}
758
759/// Filter type identifiers returned in exchange info.
760#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
761#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
762pub enum BinanceFilterType {
763    /// Price filter.
764    PriceFilter,
765    /// Percent price filter.
766    PercentPrice,
767    /// Percent price by side filter (spot).
768    PercentPriceBySide,
769    /// Lot size filter.
770    LotSize,
771    /// Market lot size filter.
772    MarketLotSize,
773    /// Notional filter (spot).
774    Notional,
775    /// Min notional filter (futures).
776    MinNotional,
777    /// Iceberg parts filter (spot).
778    IcebergParts,
779    /// Maximum number of orders filter.
780    MaxNumOrders,
781    /// Maximum number of algo orders filter.
782    MaxNumAlgoOrders,
783    /// Maximum number of iceberg orders filter (spot).
784    MaxNumIcebergOrders,
785    /// Maximum position filter (spot).
786    MaxPosition,
787    /// Trailing delta filter (spot).
788    TrailingDelta,
789    /// Maximum number of order amends filter (spot).
790    MaxNumOrderAmends,
791    /// Maximum number of order lists filter (spot).
792    MaxNumOrderLists,
793    /// Maximum asset filter (spot).
794    MaxAsset,
795    /// Exchange-level maximum number of orders.
796    ExchangeMaxNumOrders,
797    /// Exchange-level maximum number of algo orders.
798    ExchangeMaxNumAlgoOrders,
799    /// Exchange-level maximum number of iceberg orders.
800    ExchangeMaxNumIcebergOrders,
801    /// Exchange-level maximum number of order lists.
802    ExchangeMaxNumOrderLists,
803    /// T+1 sell restriction filter (spot).
804    TPlusSell,
805    /// Unknown or undocumented value.
806    #[serde(other)]
807    Unknown,
808}
809
810impl Display for BinanceEnvironment {
811    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
812        match self {
813            Self::Live => write!(f, "Live"),
814            Self::Testnet => write!(f, "Testnet"),
815            Self::Demo => write!(f, "Demo"),
816        }
817    }
818}
819
820/// Rate limit type for API request quotas.
821#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
822#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
823pub enum BinanceRateLimitType {
824    /// Weighted request limit.
825    RequestWeight,
826    /// Order placement limit.
827    Orders,
828    /// Raw request count limit (spot).
829    RawRequests,
830    /// Unknown or undocumented value.
831    #[serde(other)]
832    Unknown,
833}
834
835/// Rate limit time interval.
836#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
837#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
838pub enum BinanceRateLimitInterval {
839    /// One second interval.
840    Second,
841    /// One minute interval.
842    Minute,
843    /// One day interval.
844    Day,
845    /// Unknown or undocumented value.
846    #[serde(other)]
847    Unknown,
848}
849
850/// Kline (candlestick) interval.
851///
852/// # References
853/// - <https://developers.binance.com/docs/binance-spot-api-docs/rest-api/market-data-endpoints>
854#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
855pub enum BinanceKlineInterval {
856    /// 1 second (only for spot).
857    #[serde(rename = "1s")]
858    Second1,
859    /// 1 minute.
860    #[default]
861    #[serde(rename = "1m")]
862    Minute1,
863    /// 3 minutes.
864    #[serde(rename = "3m")]
865    Minute3,
866    /// 5 minutes.
867    #[serde(rename = "5m")]
868    Minute5,
869    /// 15 minutes.
870    #[serde(rename = "15m")]
871    Minute15,
872    /// 30 minutes.
873    #[serde(rename = "30m")]
874    Minute30,
875    /// 1 hour.
876    #[serde(rename = "1h")]
877    Hour1,
878    /// 2 hours.
879    #[serde(rename = "2h")]
880    Hour2,
881    /// 4 hours.
882    #[serde(rename = "4h")]
883    Hour4,
884    /// 6 hours.
885    #[serde(rename = "6h")]
886    Hour6,
887    /// 8 hours.
888    #[serde(rename = "8h")]
889    Hour8,
890    /// 12 hours.
891    #[serde(rename = "12h")]
892    Hour12,
893    /// 1 day.
894    #[serde(rename = "1d")]
895    Day1,
896    /// 3 days.
897    #[serde(rename = "3d")]
898    Day3,
899    /// 1 week.
900    #[serde(rename = "1w")]
901    Week1,
902    /// 1 month.
903    #[serde(rename = "1M")]
904    Month1,
905}
906
907impl BinanceKlineInterval {
908    /// Returns the string representation used by Binance API.
909    #[must_use]
910    pub const fn as_str(&self) -> &'static str {
911        match self {
912            Self::Second1 => "1s",
913            Self::Minute1 => "1m",
914            Self::Minute3 => "3m",
915            Self::Minute5 => "5m",
916            Self::Minute15 => "15m",
917            Self::Minute30 => "30m",
918            Self::Hour1 => "1h",
919            Self::Hour2 => "2h",
920            Self::Hour4 => "4h",
921            Self::Hour6 => "6h",
922            Self::Hour8 => "8h",
923            Self::Hour12 => "12h",
924            Self::Day1 => "1d",
925            Self::Day3 => "3d",
926            Self::Week1 => "1w",
927            Self::Month1 => "1M",
928        }
929    }
930}
931
932#[cfg(test)]
933mod tests {
934    use rstest::rstest;
935    use serde_json::json;
936
937    use super::*;
938
939    #[rstest]
940    fn test_product_type_as_str() {
941        assert_eq!(BinanceProductType::Spot.as_str(), "SPOT");
942        assert_eq!(BinanceProductType::Margin.as_str(), "MARGIN");
943        assert_eq!(BinanceProductType::UsdM.as_str(), "USD_M");
944        assert_eq!(BinanceProductType::CoinM.as_str(), "COIN_M");
945        assert_eq!(BinanceProductType::Options.as_str(), "OPTIONS");
946    }
947
948    #[rstest]
949    fn test_product_type_suffix() {
950        assert_eq!(BinanceProductType::Spot.suffix(), "-SPOT");
951        assert_eq!(BinanceProductType::Margin.suffix(), "-MARGIN");
952        assert_eq!(BinanceProductType::UsdM.suffix(), "-LINEAR");
953        assert_eq!(BinanceProductType::CoinM.suffix(), "-INVERSE");
954        assert_eq!(BinanceProductType::Options.suffix(), "-OPTION");
955    }
956
957    #[rstest]
958    fn test_product_type_predicates() {
959        assert!(BinanceProductType::Spot.is_spot());
960        assert!(BinanceProductType::Margin.is_spot());
961        assert!(!BinanceProductType::UsdM.is_spot());
962
963        assert!(BinanceProductType::UsdM.is_futures());
964        assert!(BinanceProductType::CoinM.is_futures());
965        assert!(!BinanceProductType::Spot.is_futures());
966
967        assert!(BinanceProductType::CoinM.is_inverse());
968        assert!(!BinanceProductType::UsdM.is_inverse());
969
970        assert!(BinanceProductType::Options.is_options());
971        assert!(!BinanceProductType::Spot.is_options());
972    }
973
974    #[rstest]
975    #[case("\"REQUEST_WEIGHT\"", BinanceRateLimitType::RequestWeight)]
976    #[case("\"ORDERS\"", BinanceRateLimitType::Orders)]
977    #[case("\"RAW_REQUESTS\"", BinanceRateLimitType::RawRequests)]
978    #[case("\"UNDOCUMENTED\"", BinanceRateLimitType::Unknown)]
979    fn test_rate_limit_type_deserializes(
980        #[case] raw: &str,
981        #[case] expected: BinanceRateLimitType,
982    ) {
983        let value: BinanceRateLimitType = serde_json::from_str(raw).unwrap();
984        assert_eq!(value, expected);
985    }
986
987    #[rstest]
988    #[case("\"SECOND\"", BinanceRateLimitInterval::Second)]
989    #[case("\"MINUTE\"", BinanceRateLimitInterval::Minute)]
990    #[case("\"DAY\"", BinanceRateLimitInterval::Day)]
991    #[case("\"WEEK\"", BinanceRateLimitInterval::Unknown)]
992    fn test_rate_limit_interval_deserializes(
993        #[case] raw: &str,
994        #[case] expected: BinanceRateLimitInterval,
995    ) {
996        let value: BinanceRateLimitInterval = serde_json::from_str(raw).unwrap();
997        assert_eq!(value, expected);
998    }
999
1000    #[rstest]
1001    #[case(BinanceMarginType::Cross, "CROSSED", "cross")]
1002    #[case(BinanceMarginType::Isolated, "ISOLATED", "isolated")]
1003    fn test_margin_type_serde_roundtrip(
1004        #[case] variant: BinanceMarginType,
1005        #[case] post_format: &str,
1006        #[case] get_format: &str,
1007    ) {
1008        let serialized = serde_json::to_value(variant).unwrap();
1009        assert_eq!(serialized, json!(post_format));
1010
1011        let from_post: BinanceMarginType =
1012            serde_json::from_str(&format!("\"{post_format}\"")).unwrap();
1013        assert_eq!(from_post, variant);
1014
1015        let from_get: BinanceMarginType =
1016            serde_json::from_str(&format!("\"{get_format}\"")).unwrap();
1017        assert_eq!(from_get, variant);
1018    }
1019
1020    #[rstest]
1021    fn test_margin_type_unknown_fallback() {
1022        let value: BinanceMarginType = serde_json::from_str("\"SOMETHING_NEW\"").unwrap();
1023        assert_eq!(value, BinanceMarginType::Unknown);
1024    }
1025
1026    #[rstest]
1027    fn test_contract_status_trading_halt_deserializes_and_maps() {
1028        // Binance reports `TRADING_HALT` for a temporarily halted active contract.
1029        // It must deserialize to the explicit variant (not the `Unknown` fallback)
1030        // and map deliberately to `Halt`.
1031        let status: BinanceContractStatus = serde_json::from_str("\"TRADING_HALT\"").unwrap();
1032        assert_eq!(status, BinanceContractStatus::TradingHalt);
1033        assert_eq!(MarketStatusAction::from(status), MarketStatusAction::Halt);
1034    }
1035
1036    #[rstest]
1037    fn test_rate_limit_enums_serialize_to_binance_strings() {
1038        assert_eq!(
1039            serde_json::to_value(BinanceRateLimitType::RequestWeight).unwrap(),
1040            json!("REQUEST_WEIGHT")
1041        );
1042        assert_eq!(
1043            serde_json::to_value(BinanceRateLimitInterval::Minute).unwrap(),
1044            json!("MINUTE")
1045        );
1046    }
1047
1048    #[rstest]
1049    #[case("\"NONE\"", BinancePriceMatch::None)]
1050    #[case("\"OPPONENT\"", BinancePriceMatch::Opponent)]
1051    #[case("\"OPPONENT_5\"", BinancePriceMatch::Opponent5)]
1052    #[case("\"OPPONENT_10\"", BinancePriceMatch::Opponent10)]
1053    #[case("\"OPPONENT_20\"", BinancePriceMatch::Opponent20)]
1054    #[case("\"QUEUE\"", BinancePriceMatch::Queue)]
1055    #[case("\"QUEUE_5\"", BinancePriceMatch::Queue5)]
1056    #[case("\"QUEUE_10\"", BinancePriceMatch::Queue10)]
1057    #[case("\"QUEUE_20\"", BinancePriceMatch::Queue20)]
1058    #[case("\"SOMETHING_NEW\"", BinancePriceMatch::Unknown)]
1059    fn test_price_match_deserializes(#[case] raw: &str, #[case] expected: BinancePriceMatch) {
1060        let value: BinancePriceMatch = serde_json::from_str(raw).unwrap();
1061        assert_eq!(value, expected);
1062    }
1063
1064    #[rstest]
1065    #[case(BinancePriceMatch::None, "NONE")]
1066    #[case(BinancePriceMatch::Opponent, "OPPONENT")]
1067    #[case(BinancePriceMatch::Opponent5, "OPPONENT_5")]
1068    #[case(BinancePriceMatch::Opponent10, "OPPONENT_10")]
1069    #[case(BinancePriceMatch::Opponent20, "OPPONENT_20")]
1070    #[case(BinancePriceMatch::Queue, "QUEUE")]
1071    #[case(BinancePriceMatch::Queue5, "QUEUE_5")]
1072    #[case(BinancePriceMatch::Queue10, "QUEUE_10")]
1073    #[case(BinancePriceMatch::Queue20, "QUEUE_20")]
1074    fn test_price_match_serializes(#[case] variant: BinancePriceMatch, #[case] expected: &str) {
1075        let serialized = serde_json::to_value(variant).unwrap();
1076        assert_eq!(serialized, json!(expected));
1077    }
1078
1079    #[rstest]
1080    #[case("OPPONENT", BinancePriceMatch::Opponent)]
1081    #[case("opponent", BinancePriceMatch::Opponent)]
1082    #[case("OPPONENT_5", BinancePriceMatch::Opponent5)]
1083    #[case("opponent_5", BinancePriceMatch::Opponent5)]
1084    #[case("QUEUE_20", BinancePriceMatch::Queue20)]
1085    #[case("queue_20", BinancePriceMatch::Queue20)]
1086    fn test_price_match_from_param_valid(#[case] input: &str, #[case] expected: BinancePriceMatch) {
1087        let result = BinancePriceMatch::from_param(input).unwrap();
1088        assert_eq!(result, expected);
1089    }
1090
1091    #[rstest]
1092    #[case("NONE")]
1093    #[case("invalid")]
1094    #[case("")]
1095    fn test_price_match_from_param_invalid(#[case] input: &str) {
1096        BinancePriceMatch::from_param(input).unwrap_err();
1097    }
1098}