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