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