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.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.binance")
132)]
133pub enum BinanceEnvironment {
134    /// Production/mainnet environment.
135    #[default]
136    Mainnet,
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.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.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    /// Pending trading.
589    PendingTrading,
590    /// Pre-delivering.
591    PreDelivering,
592    /// Delivering.
593    Delivering,
594    /// Delivered.
595    Delivered,
596    /// Pre-settle.
597    PreSettle,
598    /// Settling.
599    Settling,
600    /// Closed.
601    Close,
602    /// Pre-delist.
603    PreDelisting,
604    /// Delisting in progress.
605    Delisting,
606    /// Contract down.
607    Down,
608    /// Unknown or undocumented value.
609    #[serde(other)]
610    Unknown,
611}
612
613impl From<BinanceContractStatus> for MarketStatusAction {
614    fn from(status: BinanceContractStatus) -> Self {
615        match status {
616            BinanceContractStatus::Trading => Self::Trading,
617            BinanceContractStatus::PendingTrading => Self::PreOpen,
618            BinanceContractStatus::PreDelivering
619            | BinanceContractStatus::PreDelisting
620            | BinanceContractStatus::PreSettle => Self::PreClose,
621            BinanceContractStatus::Delivering
622            | BinanceContractStatus::Delivered
623            | BinanceContractStatus::Settling
624            | BinanceContractStatus::Close => Self::Close,
625            BinanceContractStatus::Delisting => Self::Suspend,
626            BinanceContractStatus::Down | BinanceContractStatus::Unknown => {
627                Self::NotAvailableForTrading
628            }
629        }
630    }
631}
632
633/// WebSocket stream event types.
634///
635/// These are the "e" field values in WebSocket JSON messages.
636#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
637#[serde(rename_all = "camelCase")]
638pub enum BinanceWsEventType {
639    /// Aggregate trade event.
640    AggTrade,
641    /// Individual trade event.
642    Trade,
643    /// Book ticker (best bid/ask) event.
644    BookTicker,
645    /// Depth update (order book delta) event.
646    DepthUpdate,
647    /// Mark price update event.
648    MarkPriceUpdate,
649    /// Kline/candlestick event.
650    Kline,
651    /// Forced liquidation order event.
652    ForceOrder,
653    /// 24-hour rolling ticker event.
654    #[serde(rename = "24hrTicker")]
655    Ticker24Hr,
656    /// 24-hour rolling mini ticker event.
657    #[serde(rename = "24hrMiniTicker")]
658    MiniTicker24Hr,
659
660    // User data stream events
661    /// Account update (balance and position changes).
662    #[serde(rename = "ACCOUNT_UPDATE")]
663    AccountUpdate,
664    /// Order/trade update event.
665    #[serde(rename = "ORDER_TRADE_UPDATE")]
666    OrderTradeUpdate,
667    /// Trade Lite event (low-latency fill notification).
668    #[serde(rename = "TRADE_LITE")]
669    TradeLite,
670    /// Algo order update event (Binance Futures Algo Service).
671    #[serde(rename = "ALGO_UPDATE")]
672    AlgoUpdate,
673    /// Margin call warning event.
674    #[serde(rename = "MARGIN_CALL")]
675    MarginCall,
676    /// Account configuration update (leverage change).
677    #[serde(rename = "ACCOUNT_CONFIG_UPDATE")]
678    AccountConfigUpdate,
679    /// Listen key expired event.
680    #[serde(rename = "listenKeyExpired")]
681    ListenKeyExpired,
682
683    /// Unknown or undocumented event type.
684    #[serde(other)]
685    Unknown,
686}
687
688impl BinanceWsEventType {
689    /// Returns the wire format string for this event type.
690    #[must_use]
691    pub const fn as_str(self) -> &'static str {
692        match self {
693            Self::AggTrade => "aggTrade",
694            Self::Trade => "trade",
695            Self::BookTicker => "bookTicker",
696            Self::DepthUpdate => "depthUpdate",
697            Self::MarkPriceUpdate => "markPriceUpdate",
698            Self::Kline => "kline",
699            Self::ForceOrder => "forceOrder",
700            Self::Ticker24Hr => "24hrTicker",
701            Self::MiniTicker24Hr => "24hrMiniTicker",
702            Self::AccountUpdate => "ACCOUNT_UPDATE",
703            Self::OrderTradeUpdate => "ORDER_TRADE_UPDATE",
704            Self::TradeLite => "TRADE_LITE",
705            Self::AlgoUpdate => "ALGO_UPDATE",
706            Self::MarginCall => "MARGIN_CALL",
707            Self::AccountConfigUpdate => "ACCOUNT_CONFIG_UPDATE",
708            Self::ListenKeyExpired => "listenKeyExpired",
709            Self::Unknown => "unknown",
710        }
711    }
712}
713
714impl Display for BinanceWsEventType {
715    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
716        write!(f, "{}", self.as_str())
717    }
718}
719
720/// WebSocket request method (operation type).
721///
722/// Used for subscription requests on both Spot and Futures WebSocket APIs.
723#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
724#[serde(rename_all = "UPPERCASE")]
725pub enum BinanceWsMethod {
726    /// Subscribe to streams.
727    Subscribe,
728    /// Unsubscribe from streams.
729    Unsubscribe,
730}
731
732/// Filter type identifiers returned in exchange info.
733#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
734#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
735pub enum BinanceFilterType {
736    /// Price filter.
737    PriceFilter,
738    /// Percent price filter.
739    PercentPrice,
740    /// Percent price by side filter (spot).
741    PercentPriceBySide,
742    /// Lot size filter.
743    LotSize,
744    /// Market lot size filter.
745    MarketLotSize,
746    /// Notional filter (spot).
747    Notional,
748    /// Min notional filter (futures).
749    MinNotional,
750    /// Iceberg parts filter (spot).
751    IcebergParts,
752    /// Maximum number of orders filter.
753    MaxNumOrders,
754    /// Maximum number of algo orders filter.
755    MaxNumAlgoOrders,
756    /// Maximum number of iceberg orders filter (spot).
757    MaxNumIcebergOrders,
758    /// Maximum position filter (spot).
759    MaxPosition,
760    /// Trailing delta filter (spot).
761    TrailingDelta,
762    /// Maximum number of order amends filter (spot).
763    MaxNumOrderAmends,
764    /// Maximum number of order lists filter (spot).
765    MaxNumOrderLists,
766    /// Maximum asset filter (spot).
767    MaxAsset,
768    /// Exchange-level maximum number of orders.
769    ExchangeMaxNumOrders,
770    /// Exchange-level maximum number of algo orders.
771    ExchangeMaxNumAlgoOrders,
772    /// Exchange-level maximum number of iceberg orders.
773    ExchangeMaxNumIcebergOrders,
774    /// Exchange-level maximum number of order lists.
775    ExchangeMaxNumOrderLists,
776    /// T+1 sell restriction filter (spot).
777    TPlusSell,
778    /// Unknown or undocumented value.
779    #[serde(other)]
780    Unknown,
781}
782
783impl Display for BinanceEnvironment {
784    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
785        match self {
786            Self::Mainnet => write!(f, "Mainnet"),
787            Self::Testnet => write!(f, "Testnet"),
788            Self::Demo => write!(f, "Demo"),
789        }
790    }
791}
792
793/// Rate limit type for API request quotas.
794#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
795#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
796pub enum BinanceRateLimitType {
797    /// Weighted request limit.
798    RequestWeight,
799    /// Order placement limit.
800    Orders,
801    /// Raw request count limit (spot).
802    RawRequests,
803    /// Unknown or undocumented value.
804    #[serde(other)]
805    Unknown,
806}
807
808/// Rate limit time interval.
809#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
810#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
811pub enum BinanceRateLimitInterval {
812    /// One second interval.
813    Second,
814    /// One minute interval.
815    Minute,
816    /// One day interval.
817    Day,
818    /// Unknown or undocumented value.
819    #[serde(other)]
820    Unknown,
821}
822
823/// Kline (candlestick) interval.
824///
825/// # References
826/// - <https://developers.binance.com/docs/binance-spot-api-docs/rest-api/market-data-endpoints>
827#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
828pub enum BinanceKlineInterval {
829    /// 1 second (only for spot).
830    #[serde(rename = "1s")]
831    Second1,
832    /// 1 minute.
833    #[default]
834    #[serde(rename = "1m")]
835    Minute1,
836    /// 3 minutes.
837    #[serde(rename = "3m")]
838    Minute3,
839    /// 5 minutes.
840    #[serde(rename = "5m")]
841    Minute5,
842    /// 15 minutes.
843    #[serde(rename = "15m")]
844    Minute15,
845    /// 30 minutes.
846    #[serde(rename = "30m")]
847    Minute30,
848    /// 1 hour.
849    #[serde(rename = "1h")]
850    Hour1,
851    /// 2 hours.
852    #[serde(rename = "2h")]
853    Hour2,
854    /// 4 hours.
855    #[serde(rename = "4h")]
856    Hour4,
857    /// 6 hours.
858    #[serde(rename = "6h")]
859    Hour6,
860    /// 8 hours.
861    #[serde(rename = "8h")]
862    Hour8,
863    /// 12 hours.
864    #[serde(rename = "12h")]
865    Hour12,
866    /// 1 day.
867    #[serde(rename = "1d")]
868    Day1,
869    /// 3 days.
870    #[serde(rename = "3d")]
871    Day3,
872    /// 1 week.
873    #[serde(rename = "1w")]
874    Week1,
875    /// 1 month.
876    #[serde(rename = "1M")]
877    Month1,
878}
879
880impl BinanceKlineInterval {
881    /// Returns the string representation used by Binance API.
882    #[must_use]
883    pub const fn as_str(&self) -> &'static str {
884        match self {
885            Self::Second1 => "1s",
886            Self::Minute1 => "1m",
887            Self::Minute3 => "3m",
888            Self::Minute5 => "5m",
889            Self::Minute15 => "15m",
890            Self::Minute30 => "30m",
891            Self::Hour1 => "1h",
892            Self::Hour2 => "2h",
893            Self::Hour4 => "4h",
894            Self::Hour6 => "6h",
895            Self::Hour8 => "8h",
896            Self::Hour12 => "12h",
897            Self::Day1 => "1d",
898            Self::Day3 => "3d",
899            Self::Week1 => "1w",
900            Self::Month1 => "1M",
901        }
902    }
903}
904
905#[cfg(test)]
906mod tests {
907    use rstest::rstest;
908    use serde_json::json;
909
910    use super::*;
911
912    #[rstest]
913    fn test_product_type_as_str() {
914        assert_eq!(BinanceProductType::Spot.as_str(), "SPOT");
915        assert_eq!(BinanceProductType::Margin.as_str(), "MARGIN");
916        assert_eq!(BinanceProductType::UsdM.as_str(), "USD_M");
917        assert_eq!(BinanceProductType::CoinM.as_str(), "COIN_M");
918        assert_eq!(BinanceProductType::Options.as_str(), "OPTIONS");
919    }
920
921    #[rstest]
922    fn test_product_type_suffix() {
923        assert_eq!(BinanceProductType::Spot.suffix(), "-SPOT");
924        assert_eq!(BinanceProductType::Margin.suffix(), "-MARGIN");
925        assert_eq!(BinanceProductType::UsdM.suffix(), "-LINEAR");
926        assert_eq!(BinanceProductType::CoinM.suffix(), "-INVERSE");
927        assert_eq!(BinanceProductType::Options.suffix(), "-OPTION");
928    }
929
930    #[rstest]
931    fn test_product_type_predicates() {
932        assert!(BinanceProductType::Spot.is_spot());
933        assert!(BinanceProductType::Margin.is_spot());
934        assert!(!BinanceProductType::UsdM.is_spot());
935
936        assert!(BinanceProductType::UsdM.is_futures());
937        assert!(BinanceProductType::CoinM.is_futures());
938        assert!(!BinanceProductType::Spot.is_futures());
939
940        assert!(BinanceProductType::CoinM.is_inverse());
941        assert!(!BinanceProductType::UsdM.is_inverse());
942
943        assert!(BinanceProductType::Options.is_options());
944        assert!(!BinanceProductType::Spot.is_options());
945    }
946
947    #[rstest]
948    #[case("\"REQUEST_WEIGHT\"", BinanceRateLimitType::RequestWeight)]
949    #[case("\"ORDERS\"", BinanceRateLimitType::Orders)]
950    #[case("\"RAW_REQUESTS\"", BinanceRateLimitType::RawRequests)]
951    #[case("\"UNDOCUMENTED\"", BinanceRateLimitType::Unknown)]
952    fn test_rate_limit_type_deserializes(
953        #[case] raw: &str,
954        #[case] expected: BinanceRateLimitType,
955    ) {
956        let value: BinanceRateLimitType = serde_json::from_str(raw).unwrap();
957        assert_eq!(value, expected);
958    }
959
960    #[rstest]
961    #[case("\"SECOND\"", BinanceRateLimitInterval::Second)]
962    #[case("\"MINUTE\"", BinanceRateLimitInterval::Minute)]
963    #[case("\"DAY\"", BinanceRateLimitInterval::Day)]
964    #[case("\"WEEK\"", BinanceRateLimitInterval::Unknown)]
965    fn test_rate_limit_interval_deserializes(
966        #[case] raw: &str,
967        #[case] expected: BinanceRateLimitInterval,
968    ) {
969        let value: BinanceRateLimitInterval = serde_json::from_str(raw).unwrap();
970        assert_eq!(value, expected);
971    }
972
973    #[rstest]
974    #[case(BinanceMarginType::Cross, "CROSSED", "cross")]
975    #[case(BinanceMarginType::Isolated, "ISOLATED", "isolated")]
976    fn test_margin_type_serde_roundtrip(
977        #[case] variant: BinanceMarginType,
978        #[case] post_format: &str,
979        #[case] get_format: &str,
980    ) {
981        let serialized = serde_json::to_value(variant).unwrap();
982        assert_eq!(serialized, json!(post_format));
983
984        let from_post: BinanceMarginType =
985            serde_json::from_str(&format!("\"{post_format}\"")).unwrap();
986        assert_eq!(from_post, variant);
987
988        let from_get: BinanceMarginType =
989            serde_json::from_str(&format!("\"{get_format}\"")).unwrap();
990        assert_eq!(from_get, variant);
991    }
992
993    #[rstest]
994    fn test_margin_type_unknown_fallback() {
995        let value: BinanceMarginType = serde_json::from_str("\"SOMETHING_NEW\"").unwrap();
996        assert_eq!(value, BinanceMarginType::Unknown);
997    }
998
999    #[rstest]
1000    fn test_rate_limit_enums_serialize_to_binance_strings() {
1001        assert_eq!(
1002            serde_json::to_value(BinanceRateLimitType::RequestWeight).unwrap(),
1003            json!("REQUEST_WEIGHT")
1004        );
1005        assert_eq!(
1006            serde_json::to_value(BinanceRateLimitInterval::Minute).unwrap(),
1007            json!("MINUTE")
1008        );
1009    }
1010
1011    #[rstest]
1012    #[case("\"NONE\"", BinancePriceMatch::None)]
1013    #[case("\"OPPONENT\"", BinancePriceMatch::Opponent)]
1014    #[case("\"OPPONENT_5\"", BinancePriceMatch::Opponent5)]
1015    #[case("\"OPPONENT_10\"", BinancePriceMatch::Opponent10)]
1016    #[case("\"OPPONENT_20\"", BinancePriceMatch::Opponent20)]
1017    #[case("\"QUEUE\"", BinancePriceMatch::Queue)]
1018    #[case("\"QUEUE_5\"", BinancePriceMatch::Queue5)]
1019    #[case("\"QUEUE_10\"", BinancePriceMatch::Queue10)]
1020    #[case("\"QUEUE_20\"", BinancePriceMatch::Queue20)]
1021    #[case("\"SOMETHING_NEW\"", BinancePriceMatch::Unknown)]
1022    fn test_price_match_deserializes(#[case] raw: &str, #[case] expected: BinancePriceMatch) {
1023        let value: BinancePriceMatch = serde_json::from_str(raw).unwrap();
1024        assert_eq!(value, expected);
1025    }
1026
1027    #[rstest]
1028    #[case(BinancePriceMatch::None, "NONE")]
1029    #[case(BinancePriceMatch::Opponent, "OPPONENT")]
1030    #[case(BinancePriceMatch::Opponent5, "OPPONENT_5")]
1031    #[case(BinancePriceMatch::Opponent10, "OPPONENT_10")]
1032    #[case(BinancePriceMatch::Opponent20, "OPPONENT_20")]
1033    #[case(BinancePriceMatch::Queue, "QUEUE")]
1034    #[case(BinancePriceMatch::Queue5, "QUEUE_5")]
1035    #[case(BinancePriceMatch::Queue10, "QUEUE_10")]
1036    #[case(BinancePriceMatch::Queue20, "QUEUE_20")]
1037    fn test_price_match_serializes(#[case] variant: BinancePriceMatch, #[case] expected: &str) {
1038        let serialized = serde_json::to_value(variant).unwrap();
1039        assert_eq!(serialized, json!(expected));
1040    }
1041
1042    #[rstest]
1043    #[case("OPPONENT", BinancePriceMatch::Opponent)]
1044    #[case("opponent", BinancePriceMatch::Opponent)]
1045    #[case("OPPONENT_5", BinancePriceMatch::Opponent5)]
1046    #[case("opponent_5", BinancePriceMatch::Opponent5)]
1047    #[case("QUEUE_20", BinancePriceMatch::Queue20)]
1048    #[case("queue_20", BinancePriceMatch::Queue20)]
1049    fn test_price_match_from_param_valid(#[case] input: &str, #[case] expected: BinancePriceMatch) {
1050        let result = BinancePriceMatch::from_param(input).unwrap();
1051        assert_eq!(result, expected);
1052    }
1053
1054    #[rstest]
1055    #[case("NONE")]
1056    #[case("invalid")]
1057    #[case("")]
1058    fn test_price_match_from_param_invalid(#[case] input: &str) {
1059        assert!(BinancePriceMatch::from_param(input).is_err());
1060    }
1061}