Skip to main content

nautilus_binance/spot/http/
query.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//! Query parameter builders for Binance Spot HTTP requests.
17
18#[cfg(test)]
19use nautilus_core::string::secret::REDACTED;
20use nautilus_core::string::secret::SecretString;
21use serde::Serialize;
22use zeroize::Zeroize;
23
24use crate::{
25    common::enums::{BinanceSelfTradePreventionMode, BinanceSide, BinanceTimeInForce},
26    spot::enums::{BinanceCancelReplaceMode, BinanceOrderResponseType, BinanceSpotOrderType},
27};
28
29/// Query parameters for the depth endpoint.
30#[derive(Debug, Clone, Serialize)]
31pub struct DepthParams {
32    /// Trading pair symbol (e.g., "BTCUSDT").
33    pub symbol: String,
34    /// Number of price levels to return (default 100, max 5000).
35    #[serde(skip_serializing_if = "Option::is_none")]
36    pub limit: Option<u32>,
37}
38
39impl DepthParams {
40    /// Create new depth query params.
41    #[must_use]
42    pub fn new(symbol: impl Into<String>) -> Self {
43        Self {
44            symbol: symbol.into(),
45            limit: None,
46        }
47    }
48
49    /// Set the limit.
50    #[must_use]
51    pub fn with_limit(mut self, limit: u32) -> Self {
52        self.limit = Some(limit);
53        self
54    }
55}
56
57/// Query parameters for the trades endpoint.
58#[derive(Debug, Clone, Serialize)]
59pub struct TradesParams {
60    /// Trading pair symbol (e.g., "BTCUSDT").
61    pub symbol: String,
62    /// Number of trades to return (default 500, max 1000).
63    #[serde(skip_serializing_if = "Option::is_none")]
64    pub limit: Option<u32>,
65}
66
67impl TradesParams {
68    /// Create new trades query params.
69    #[must_use]
70    pub fn new(symbol: impl Into<String>) -> Self {
71        Self {
72            symbol: symbol.into(),
73            limit: None,
74        }
75    }
76
77    /// Set the limit.
78    #[must_use]
79    pub fn with_limit(mut self, limit: u32) -> Self {
80        self.limit = Some(limit);
81        self
82    }
83}
84
85/// Query parameters for the aggregate trades endpoint.
86#[derive(Debug, Clone, Serialize)]
87pub struct AggTradesParams {
88    /// Trading pair symbol.
89    pub symbol: String,
90    /// Aggregate trade ID to begin from, inclusive.
91    #[serde(skip_serializing_if = "Option::is_none", rename = "fromId")]
92    pub from_id: Option<i64>,
93    /// Start time in milliseconds, inclusive.
94    #[serde(skip_serializing_if = "Option::is_none", rename = "startTime")]
95    pub start_time: Option<i64>,
96    /// End time in milliseconds, inclusive.
97    #[serde(skip_serializing_if = "Option::is_none", rename = "endTime")]
98    pub end_time: Option<i64>,
99    /// Number of aggregate trades to return (default 500, max 1000).
100    #[serde(skip_serializing_if = "Option::is_none")]
101    pub limit: Option<u32>,
102}
103
104#[cfg(test)]
105mod market_data_tests {
106    use rstest::rstest;
107
108    use super::*;
109
110    #[rstest]
111    fn test_agg_trades_params_serialization() {
112        let params = AggTradesParams {
113            symbol: "ETHUSDT".to_string(),
114            from_id: Some(123),
115            start_time: Some(1_700_000_000_001),
116            end_time: Some(1_700_000_000_999),
117            limit: Some(456),
118        };
119
120        let serialized = serde_urlencoded::to_string(&params).unwrap();
121
122        assert_eq!(
123            serialized,
124            "symbol=ETHUSDT&fromId=123&startTime=1700000000001&endTime=1700000000999&limit=456"
125        );
126    }
127}
128
129/// Query parameters for new order submission.
130#[derive(Debug, Clone, Serialize)]
131pub struct NewOrderParams {
132    /// Trading pair symbol.
133    pub symbol: String,
134    /// Order side (BUY or SELL).
135    pub side: BinanceSide,
136    /// Order type.
137    #[serde(rename = "type")]
138    pub order_type: BinanceSpotOrderType,
139    /// Time in force.
140    #[serde(skip_serializing_if = "Option::is_none", rename = "timeInForce")]
141    pub time_in_force: Option<BinanceTimeInForce>,
142    /// Order quantity.
143    #[serde(skip_serializing_if = "Option::is_none")]
144    pub quantity: Option<String>,
145    /// Quote order quantity (for market orders).
146    #[serde(skip_serializing_if = "Option::is_none", rename = "quoteOrderQty")]
147    pub quote_order_qty: Option<String>,
148    /// Limit price.
149    #[serde(skip_serializing_if = "Option::is_none")]
150    pub price: Option<String>,
151    /// Client order ID.
152    #[serde(skip_serializing_if = "Option::is_none", rename = "newClientOrderId")]
153    pub new_client_order_id: Option<String>,
154    /// Stop price for stop orders.
155    #[serde(skip_serializing_if = "Option::is_none", rename = "stopPrice")]
156    pub stop_price: Option<String>,
157    /// Trailing delta for trailing stop orders.
158    #[serde(skip_serializing_if = "Option::is_none", rename = "trailingDelta")]
159    pub trailing_delta: Option<i64>,
160    /// Iceberg quantity.
161    #[serde(skip_serializing_if = "Option::is_none", rename = "icebergQty")]
162    pub iceberg_qty: Option<String>,
163    /// Response type (ACK, RESULT, or FULL).
164    #[serde(skip_serializing_if = "Option::is_none", rename = "newOrderRespType")]
165    pub new_order_resp_type: Option<BinanceOrderResponseType>,
166    /// Self-trade prevention mode.
167    #[serde(
168        skip_serializing_if = "Option::is_none",
169        rename = "selfTradePreventionMode"
170    )]
171    pub self_trade_prevention_mode: Option<BinanceSelfTradePreventionMode>,
172    /// Strategy ID for order tracking.
173    #[serde(skip_serializing_if = "Option::is_none", rename = "strategyId")]
174    pub strategy_id: Option<i64>,
175    /// Strategy type for order tracking.
176    #[serde(skip_serializing_if = "Option::is_none", rename = "strategyType")]
177    pub strategy_type: Option<i64>,
178}
179
180impl NewOrderParams {
181    /// Create new order params for a limit order.
182    #[must_use]
183    pub fn limit(
184        symbol: impl Into<String>,
185        side: BinanceSide,
186        quantity: impl Into<String>,
187        price: impl Into<String>,
188    ) -> Self {
189        Self {
190            symbol: symbol.into(),
191            side,
192            order_type: BinanceSpotOrderType::Limit,
193            time_in_force: Some(BinanceTimeInForce::Gtc),
194            quantity: Some(quantity.into()),
195            quote_order_qty: None,
196            price: Some(price.into()),
197            new_client_order_id: None,
198            stop_price: None,
199            trailing_delta: None,
200            iceberg_qty: None,
201            new_order_resp_type: Some(BinanceOrderResponseType::Full),
202            self_trade_prevention_mode: None,
203            strategy_id: None,
204            strategy_type: None,
205        }
206    }
207
208    /// Create new order params for a market order.
209    #[must_use]
210    pub fn market(
211        symbol: impl Into<String>,
212        side: BinanceSide,
213        quantity: impl Into<String>,
214    ) -> Self {
215        Self {
216            symbol: symbol.into(),
217            side,
218            order_type: BinanceSpotOrderType::Market,
219            time_in_force: None,
220            quantity: Some(quantity.into()),
221            quote_order_qty: None,
222            price: None,
223            new_client_order_id: None,
224            stop_price: None,
225            trailing_delta: None,
226            iceberg_qty: None,
227            new_order_resp_type: Some(BinanceOrderResponseType::Full),
228            self_trade_prevention_mode: None,
229            strategy_id: None,
230            strategy_type: None,
231        }
232    }
233
234    /// Set the client order ID.
235    #[must_use]
236    pub fn with_client_order_id(mut self, id: impl Into<String>) -> Self {
237        self.new_client_order_id = Some(id.into());
238        self
239    }
240
241    /// Set the time in force.
242    #[must_use]
243    pub fn with_time_in_force(mut self, tif: BinanceTimeInForce) -> Self {
244        self.time_in_force = Some(tif);
245        self
246    }
247
248    /// Set the stop price.
249    #[must_use]
250    pub fn with_stop_price(mut self, price: impl Into<String>) -> Self {
251        self.stop_price = Some(price.into());
252        self
253    }
254
255    /// Set the self-trade prevention mode.
256    #[must_use]
257    pub fn with_stp_mode(mut self, mode: BinanceSelfTradePreventionMode) -> Self {
258        self.self_trade_prevention_mode = Some(mode);
259        self
260    }
261}
262
263/// Query parameters for canceling an order.
264#[derive(Debug, Clone, Serialize)]
265pub struct CancelOrderParams {
266    /// Trading pair symbol.
267    pub symbol: String,
268    /// Order ID to cancel.
269    #[serde(skip_serializing_if = "Option::is_none", rename = "orderId")]
270    pub order_id: Option<i64>,
271    /// Original client order ID.
272    #[serde(skip_serializing_if = "Option::is_none", rename = "origClientOrderId")]
273    pub orig_client_order_id: Option<String>,
274    /// New client order ID for the cancel request.
275    #[serde(skip_serializing_if = "Option::is_none", rename = "newClientOrderId")]
276    pub new_client_order_id: Option<String>,
277}
278
279impl CancelOrderParams {
280    /// Create cancel params by order ID.
281    #[must_use]
282    pub fn by_order_id(symbol: impl Into<String>, order_id: i64) -> Self {
283        Self {
284            symbol: symbol.into(),
285            order_id: Some(order_id),
286            orig_client_order_id: None,
287            new_client_order_id: None,
288        }
289    }
290
291    /// Create cancel params by client order ID.
292    #[must_use]
293    pub fn by_client_order_id(
294        symbol: impl Into<String>,
295        client_order_id: impl Into<String>,
296    ) -> Self {
297        Self {
298            symbol: symbol.into(),
299            order_id: None,
300            orig_client_order_id: Some(client_order_id.into()),
301            new_client_order_id: None,
302        }
303    }
304}
305
306/// Query parameters for canceling all open orders on a symbol.
307#[derive(Debug, Clone, Serialize)]
308pub struct CancelOpenOrdersParams {
309    /// Trading pair symbol.
310    pub symbol: String,
311}
312
313impl CancelOpenOrdersParams {
314    /// Create new cancel open orders params.
315    #[must_use]
316    pub fn new(symbol: impl Into<String>) -> Self {
317        Self {
318            symbol: symbol.into(),
319        }
320    }
321}
322
323/// Query parameters for cancel and replace order.
324#[derive(Debug, Clone, Serialize)]
325pub struct CancelReplaceOrderParams {
326    /// Trading pair symbol.
327    pub symbol: String,
328    /// Order side.
329    pub side: BinanceSide,
330    /// Order type.
331    #[serde(rename = "type")]
332    pub order_type: BinanceSpotOrderType,
333    /// Cancel/replace mode.
334    #[serde(rename = "cancelReplaceMode")]
335    pub cancel_replace_mode: BinanceCancelReplaceMode,
336    /// Time in force.
337    #[serde(skip_serializing_if = "Option::is_none", rename = "timeInForce")]
338    pub time_in_force: Option<BinanceTimeInForce>,
339    /// Order quantity.
340    #[serde(skip_serializing_if = "Option::is_none")]
341    pub quantity: Option<String>,
342    /// Quote order quantity.
343    #[serde(skip_serializing_if = "Option::is_none", rename = "quoteOrderQty")]
344    pub quote_order_qty: Option<String>,
345    /// Limit price.
346    #[serde(skip_serializing_if = "Option::is_none")]
347    pub price: Option<String>,
348    /// Order ID to cancel.
349    #[serde(skip_serializing_if = "Option::is_none", rename = "cancelOrderId")]
350    pub cancel_order_id: Option<i64>,
351    /// Client order ID to cancel.
352    #[serde(
353        skip_serializing_if = "Option::is_none",
354        rename = "cancelOrigClientOrderId"
355    )]
356    pub cancel_orig_client_order_id: Option<String>,
357    /// Client order ID for the cancel half of the request.
358    #[serde(
359        skip_serializing_if = "Option::is_none",
360        rename = "cancelNewClientOrderId"
361    )]
362    pub cancel_new_client_order_id: Option<String>,
363    /// New client order ID.
364    #[serde(skip_serializing_if = "Option::is_none", rename = "newClientOrderId")]
365    pub new_client_order_id: Option<String>,
366    /// Stop price.
367    #[serde(skip_serializing_if = "Option::is_none", rename = "stopPrice")]
368    pub stop_price: Option<String>,
369    /// Trailing delta.
370    #[serde(skip_serializing_if = "Option::is_none", rename = "trailingDelta")]
371    pub trailing_delta: Option<i64>,
372    /// Iceberg quantity.
373    #[serde(skip_serializing_if = "Option::is_none", rename = "icebergQty")]
374    pub iceberg_qty: Option<String>,
375    /// Response type.
376    #[serde(skip_serializing_if = "Option::is_none", rename = "newOrderRespType")]
377    pub new_order_resp_type: Option<BinanceOrderResponseType>,
378    /// Self-trade prevention mode.
379    #[serde(
380        skip_serializing_if = "Option::is_none",
381        rename = "selfTradePreventionMode"
382    )]
383    pub self_trade_prevention_mode: Option<BinanceSelfTradePreventionMode>,
384}
385
386/// Query parameters for querying a single order.
387#[derive(Debug, Clone, Serialize)]
388pub struct QueryOrderParams {
389    /// Trading pair symbol.
390    pub symbol: String,
391    /// Order ID.
392    #[serde(skip_serializing_if = "Option::is_none", rename = "orderId")]
393    pub order_id: Option<i64>,
394    /// Original client order ID.
395    #[serde(skip_serializing_if = "Option::is_none", rename = "origClientOrderId")]
396    pub orig_client_order_id: Option<String>,
397}
398
399impl QueryOrderParams {
400    /// Create query params by order ID.
401    #[must_use]
402    pub fn by_order_id(symbol: impl Into<String>, order_id: i64) -> Self {
403        Self {
404            symbol: symbol.into(),
405            order_id: Some(order_id),
406            orig_client_order_id: None,
407        }
408    }
409
410    /// Create query params by client order ID.
411    #[must_use]
412    pub fn by_client_order_id(
413        symbol: impl Into<String>,
414        client_order_id: impl Into<String>,
415    ) -> Self {
416        Self {
417            symbol: symbol.into(),
418            order_id: None,
419            orig_client_order_id: Some(client_order_id.into()),
420        }
421    }
422}
423
424/// Query parameters for querying open orders.
425#[derive(Debug, Clone, Default, Serialize)]
426pub struct OpenOrdersParams {
427    /// Trading pair symbol (optional, omit for all symbols).
428    #[serde(skip_serializing_if = "Option::is_none")]
429    pub symbol: Option<String>,
430}
431
432impl OpenOrdersParams {
433    /// Create new open orders params for all symbols.
434    #[must_use]
435    pub fn all() -> Self {
436        Self { symbol: None }
437    }
438
439    /// Create new open orders params for a specific symbol.
440    #[must_use]
441    pub fn for_symbol(symbol: impl Into<String>) -> Self {
442        Self {
443            symbol: Some(symbol.into()),
444        }
445    }
446}
447
448/// Query parameters for querying all orders (includes filled/canceled).
449#[derive(Debug, Clone, Serialize)]
450pub struct AllOrdersParams {
451    /// Trading pair symbol.
452    pub symbol: String,
453    /// Filter by order ID (returns orders >= this ID).
454    #[serde(skip_serializing_if = "Option::is_none", rename = "orderId")]
455    pub order_id: Option<i64>,
456    /// Filter by start time.
457    #[serde(skip_serializing_if = "Option::is_none", rename = "startTime")]
458    pub start_time: Option<i64>,
459    /// Filter by end time.
460    #[serde(skip_serializing_if = "Option::is_none", rename = "endTime")]
461    pub end_time: Option<i64>,
462    /// Maximum number of orders to return (default 500, max 1000).
463    #[serde(skip_serializing_if = "Option::is_none")]
464    pub limit: Option<u32>,
465}
466
467impl AllOrdersParams {
468    /// Create new all orders params.
469    #[must_use]
470    pub fn new(symbol: impl Into<String>) -> Self {
471        Self {
472            symbol: symbol.into(),
473            order_id: None,
474            start_time: None,
475            end_time: None,
476            limit: None,
477        }
478    }
479
480    /// Set the limit.
481    #[must_use]
482    pub fn with_limit(mut self, limit: u32) -> Self {
483        self.limit = Some(limit);
484        self
485    }
486
487    /// Set the time range.
488    #[must_use]
489    pub fn with_time_range(mut self, start: i64, end: i64) -> Self {
490        self.start_time = Some(start);
491        self.end_time = Some(end);
492        self
493    }
494}
495
496/// Query parameters for new OCO order.
497#[derive(Debug, Clone, Serialize)]
498pub struct NewOcoOrderParams {
499    /// Trading pair symbol.
500    pub symbol: String,
501    /// Order side.
502    pub side: BinanceSide,
503    /// Order quantity.
504    pub quantity: String,
505    /// Limit price (above-market for sell, below-market for buy).
506    pub price: String,
507    /// Stop price trigger.
508    #[serde(rename = "stopPrice")]
509    pub stop_price: String,
510    /// Stop limit price (optional, creates stop-limit if provided).
511    #[serde(skip_serializing_if = "Option::is_none", rename = "stopLimitPrice")]
512    pub stop_limit_price: Option<String>,
513    /// Client order ID for the entire list.
514    #[serde(skip_serializing_if = "Option::is_none", rename = "listClientOrderId")]
515    pub list_client_order_id: Option<String>,
516    /// Client order ID for the limit order.
517    #[serde(skip_serializing_if = "Option::is_none", rename = "limitClientOrderId")]
518    pub limit_client_order_id: Option<String>,
519    /// Client order ID for the stop order.
520    #[serde(skip_serializing_if = "Option::is_none", rename = "stopClientOrderId")]
521    pub stop_client_order_id: Option<String>,
522    /// Iceberg quantity for the limit leg.
523    #[serde(skip_serializing_if = "Option::is_none", rename = "limitIcebergQty")]
524    pub limit_iceberg_qty: Option<String>,
525    /// Iceberg quantity for the stop leg.
526    #[serde(skip_serializing_if = "Option::is_none", rename = "stopIcebergQty")]
527    pub stop_iceberg_qty: Option<String>,
528    /// Time in force for the stop-limit leg.
529    #[serde(
530        skip_serializing_if = "Option::is_none",
531        rename = "stopLimitTimeInForce"
532    )]
533    pub stop_limit_time_in_force: Option<BinanceTimeInForce>,
534    /// Response type.
535    #[serde(skip_serializing_if = "Option::is_none", rename = "newOrderRespType")]
536    pub new_order_resp_type: Option<BinanceOrderResponseType>,
537    /// Self-trade prevention mode.
538    #[serde(
539        skip_serializing_if = "Option::is_none",
540        rename = "selfTradePreventionMode"
541    )]
542    pub self_trade_prevention_mode: Option<BinanceSelfTradePreventionMode>,
543}
544
545impl NewOcoOrderParams {
546    /// Create new OCO order params.
547    #[must_use]
548    pub fn new(
549        symbol: impl Into<String>,
550        side: BinanceSide,
551        quantity: impl Into<String>,
552        price: impl Into<String>,
553        stop_price: impl Into<String>,
554    ) -> Self {
555        Self {
556            symbol: symbol.into(),
557            side,
558            quantity: quantity.into(),
559            price: price.into(),
560            stop_price: stop_price.into(),
561            stop_limit_price: None,
562            list_client_order_id: None,
563            limit_client_order_id: None,
564            stop_client_order_id: None,
565            limit_iceberg_qty: None,
566            stop_iceberg_qty: None,
567            stop_limit_time_in_force: None,
568            new_order_resp_type: Some(BinanceOrderResponseType::Full),
569            self_trade_prevention_mode: None,
570        }
571    }
572
573    /// Set stop limit price (makes stop leg a stop-limit order).
574    #[must_use]
575    pub fn with_stop_limit_price(mut self, price: impl Into<String>) -> Self {
576        self.stop_limit_price = Some(price.into());
577        self.stop_limit_time_in_force = Some(BinanceTimeInForce::Gtc);
578        self
579    }
580}
581
582/// Query parameters for new OCO order list.
583#[derive(Debug, Clone, Serialize)]
584#[serde(rename_all = "camelCase")]
585pub struct NewOcoOrderListParams {
586    /// Trading pair symbol.
587    pub symbol: String,
588    /// Client order ID for the entire list.
589    #[serde(skip_serializing_if = "Option::is_none")]
590    pub list_client_order_id: Option<String>,
591    /// Order side.
592    pub side: BinanceSide,
593    /// Quantity for both legs.
594    pub quantity: String,
595    /// Above leg order type.
596    pub above_type: BinanceSpotOrderType,
597    /// Client order ID for the above leg.
598    #[serde(skip_serializing_if = "Option::is_none")]
599    pub above_client_order_id: Option<String>,
600    /// Iceberg quantity for the above leg.
601    #[serde(skip_serializing_if = "Option::is_none")]
602    pub above_iceberg_qty: Option<String>,
603    /// Limit price for the above leg.
604    #[serde(skip_serializing_if = "Option::is_none")]
605    pub above_price: Option<String>,
606    /// Stop price for the above leg.
607    #[serde(skip_serializing_if = "Option::is_none")]
608    pub above_stop_price: Option<String>,
609    /// Time in force for the above leg.
610    #[serde(skip_serializing_if = "Option::is_none")]
611    pub above_time_in_force: Option<BinanceTimeInForce>,
612    /// Below leg order type.
613    pub below_type: BinanceSpotOrderType,
614    /// Client order ID for the below leg.
615    #[serde(skip_serializing_if = "Option::is_none")]
616    pub below_client_order_id: Option<String>,
617    /// Iceberg quantity for the below leg.
618    #[serde(skip_serializing_if = "Option::is_none")]
619    pub below_iceberg_qty: Option<String>,
620    /// Limit price for the below leg.
621    #[serde(skip_serializing_if = "Option::is_none")]
622    pub below_price: Option<String>,
623    /// Stop price for the below leg.
624    #[serde(skip_serializing_if = "Option::is_none")]
625    pub below_stop_price: Option<String>,
626    /// Time in force for the below leg.
627    #[serde(skip_serializing_if = "Option::is_none")]
628    pub below_time_in_force: Option<BinanceTimeInForce>,
629    /// Response type.
630    #[serde(skip_serializing_if = "Option::is_none")]
631    pub new_order_resp_type: Option<BinanceOrderResponseType>,
632    /// Self-trade prevention mode.
633    #[serde(skip_serializing_if = "Option::is_none")]
634    pub self_trade_prevention_mode: Option<BinanceSelfTradePreventionMode>,
635}
636
637/// Query parameters for canceling an order list (OCO).
638#[derive(Debug, Clone, Serialize)]
639pub struct CancelOrderListParams {
640    /// Trading pair symbol.
641    pub symbol: String,
642    /// Order list ID.
643    #[serde(skip_serializing_if = "Option::is_none", rename = "orderListId")]
644    pub order_list_id: Option<i64>,
645    /// List client order ID.
646    #[serde(skip_serializing_if = "Option::is_none", rename = "listClientOrderId")]
647    pub list_client_order_id: Option<String>,
648    /// New client order ID for the cancel request.
649    #[serde(skip_serializing_if = "Option::is_none", rename = "newClientOrderId")]
650    pub new_client_order_id: Option<String>,
651}
652
653impl CancelOrderListParams {
654    /// Create cancel params by order list ID.
655    #[must_use]
656    pub fn by_order_list_id(symbol: impl Into<String>, order_list_id: i64) -> Self {
657        Self {
658            symbol: symbol.into(),
659            order_list_id: Some(order_list_id),
660            list_client_order_id: None,
661            new_client_order_id: None,
662        }
663    }
664
665    /// Create cancel params by list client order ID.
666    #[must_use]
667    pub fn by_list_client_order_id(
668        symbol: impl Into<String>,
669        list_client_order_id: impl Into<String>,
670    ) -> Self {
671        Self {
672            symbol: symbol.into(),
673            order_list_id: None,
674            list_client_order_id: Some(list_client_order_id.into()),
675            new_client_order_id: None,
676        }
677    }
678}
679
680/// Query parameters for querying an order list (OCO).
681#[derive(Debug, Clone, Serialize)]
682pub struct QueryOrderListParams {
683    /// Order list ID.
684    #[serde(skip_serializing_if = "Option::is_none", rename = "orderListId")]
685    pub order_list_id: Option<i64>,
686    /// List client order ID.
687    #[serde(skip_serializing_if = "Option::is_none", rename = "origClientOrderId")]
688    pub orig_client_order_id: Option<String>,
689}
690
691impl QueryOrderListParams {
692    /// Create query params by order list ID.
693    #[must_use]
694    pub fn by_order_list_id(order_list_id: i64) -> Self {
695        Self {
696            order_list_id: Some(order_list_id),
697            orig_client_order_id: None,
698        }
699    }
700
701    /// Create query params by list client order ID.
702    #[must_use]
703    pub fn by_client_order_id(client_order_id: impl Into<String>) -> Self {
704        Self {
705            order_list_id: None,
706            orig_client_order_id: Some(client_order_id.into()),
707        }
708    }
709}
710
711/// Query parameters for querying all order lists (OCOs).
712#[derive(Debug, Clone, Default, Serialize)]
713pub struct AllOrderListsParams {
714    /// Filter by start time.
715    #[serde(skip_serializing_if = "Option::is_none", rename = "startTime")]
716    pub start_time: Option<i64>,
717    /// Filter by end time.
718    #[serde(skip_serializing_if = "Option::is_none", rename = "endTime")]
719    pub end_time: Option<i64>,
720    /// Maximum number of results (default 500, max 1000).
721    #[serde(skip_serializing_if = "Option::is_none")]
722    pub limit: Option<u32>,
723}
724
725/// Query parameters for querying open order lists (OCOs).
726#[derive(Debug, Clone, Default, Serialize)]
727pub struct OpenOrderListsParams {}
728
729/// Query parameters for account information.
730#[derive(Debug, Clone, Default, Serialize)]
731pub struct AccountInfoParams {
732    /// Omit zero balances from response.
733    #[serde(skip_serializing_if = "Option::is_none", rename = "omitZeroBalances")]
734    pub omit_zero_balances: Option<bool>,
735}
736
737/// Query parameters for account-specific symbol commission rates.
738#[derive(Debug, Clone, Serialize)]
739pub struct AccountCommissionParams {
740    /// Venue symbol.
741    pub symbol: String,
742}
743
744impl AccountCommissionParams {
745    /// Creates commission query parameters for `symbol`.
746    #[must_use]
747    pub fn new(symbol: impl Into<String>) -> Self {
748        Self {
749            symbol: symbol.into(),
750        }
751    }
752}
753
754impl AccountInfoParams {
755    /// Create new account info params.
756    #[must_use]
757    pub fn new() -> Self {
758        Self::default()
759    }
760
761    /// Omit zero balances from response.
762    #[must_use]
763    pub fn omit_zero_balances(mut self) -> Self {
764        self.omit_zero_balances = Some(true);
765        self
766    }
767}
768
769/// Query parameters for account trades.
770#[derive(Debug, Clone, Serialize)]
771pub struct AccountTradesParams {
772    /// Trading pair symbol.
773    pub symbol: String,
774    /// Filter by order ID.
775    #[serde(skip_serializing_if = "Option::is_none", rename = "orderId")]
776    pub order_id: Option<i64>,
777    /// Filter by start time.
778    #[serde(skip_serializing_if = "Option::is_none", rename = "startTime")]
779    pub start_time: Option<i64>,
780    /// Filter by end time.
781    #[serde(skip_serializing_if = "Option::is_none", rename = "endTime")]
782    pub end_time: Option<i64>,
783    /// Filter by trade ID (returns trades >= this ID).
784    #[serde(skip_serializing_if = "Option::is_none", rename = "fromId")]
785    pub from_id: Option<i64>,
786    /// Maximum number of trades to return (default 500, max 1000).
787    #[serde(skip_serializing_if = "Option::is_none")]
788    pub limit: Option<u32>,
789}
790
791impl AccountTradesParams {
792    /// Create new account trades params.
793    #[must_use]
794    pub fn new(symbol: impl Into<String>) -> Self {
795        Self {
796            symbol: symbol.into(),
797            order_id: None,
798            start_time: None,
799            end_time: None,
800            from_id: None,
801            limit: None,
802        }
803    }
804
805    /// Filter by order ID.
806    #[must_use]
807    pub fn for_order(mut self, order_id: i64) -> Self {
808        self.order_id = Some(order_id);
809        self
810    }
811
812    /// Set the limit.
813    #[must_use]
814    pub fn with_limit(mut self, limit: u32) -> Self {
815        self.limit = Some(limit);
816        self
817    }
818
819    /// Set the time range.
820    #[must_use]
821    pub fn with_time_range(mut self, start: i64, end: i64) -> Self {
822        self.start_time = Some(start);
823        self.end_time = Some(end);
824        self
825    }
826}
827
828/// Query parameters for klines (candlestick) data.
829#[derive(Debug, Clone, Serialize)]
830pub struct KlinesParams {
831    /// Trading pair symbol (e.g., "BTCUSDT").
832    pub symbol: String,
833    /// Kline interval (e.g., "1m", "1h", "1d").
834    pub interval: String,
835    /// Filter by start time (milliseconds).
836    #[serde(skip_serializing_if = "Option::is_none", rename = "startTime")]
837    pub start_time: Option<i64>,
838    /// Filter by end time (milliseconds).
839    #[serde(skip_serializing_if = "Option::is_none", rename = "endTime")]
840    pub end_time: Option<i64>,
841    /// Kline time zone offset (+/- hours, default 0 UTC).
842    #[serde(skip_serializing_if = "Option::is_none", rename = "timeZone")]
843    pub time_zone: Option<String>,
844    /// Maximum number of klines to return (default 500, max 1000).
845    #[serde(skip_serializing_if = "Option::is_none")]
846    pub limit: Option<u32>,
847}
848
849/// Query parameters for listen key operations (extend/close).
850#[derive(Debug, Clone, Serialize, Zeroize)]
851pub struct ListenKeyParams {
852    /// The listen key to extend or close.
853    #[serde(rename = "listenKey")]
854    pub listen_key: SecretString,
855}
856
857impl ListenKeyParams {
858    /// Creates new listen key params.
859    #[must_use]
860    pub fn new(listen_key: impl Into<SecretString>) -> Self {
861        Self {
862            listen_key: listen_key.into(),
863        }
864    }
865}
866
867/// Query parameters for ticker endpoints.
868#[derive(Debug, Clone, Default, Serialize)]
869pub struct TickerParams {
870    /// Trading pair symbol (optional, omit for all symbols).
871    #[serde(skip_serializing_if = "Option::is_none")]
872    pub symbol: Option<String>,
873}
874
875impl TickerParams {
876    /// Creates ticker params for all symbols.
877    #[must_use]
878    pub fn all() -> Self {
879        Self { symbol: None }
880    }
881
882    /// Creates ticker params for a specific symbol.
883    #[must_use]
884    pub fn for_symbol(symbol: impl Into<String>) -> Self {
885        Self {
886            symbol: Some(symbol.into()),
887        }
888    }
889}
890
891/// Query parameters for average price endpoint.
892#[derive(Debug, Clone, Serialize)]
893pub struct AvgPriceParams {
894    /// Trading pair symbol (required).
895    pub symbol: String,
896}
897
898impl AvgPriceParams {
899    /// Creates average price params.
900    #[must_use]
901    pub fn new(symbol: impl Into<String>) -> Self {
902        Self {
903            symbol: symbol.into(),
904        }
905    }
906}
907
908/// Query parameters for trade fee endpoint.
909#[derive(Debug, Clone, Default, Serialize)]
910pub struct TradeFeeParams {
911    /// Trading pair symbol (optional, omit for all symbols).
912    #[serde(skip_serializing_if = "Option::is_none")]
913    pub symbol: Option<String>,
914}
915
916impl TradeFeeParams {
917    /// Creates trade fee params for all symbols.
918    #[must_use]
919    pub fn all() -> Self {
920        Self { symbol: None }
921    }
922
923    /// Creates trade fee params for a specific symbol.
924    #[must_use]
925    pub fn for_symbol(symbol: impl Into<String>) -> Self {
926        Self {
927            symbol: Some(symbol.into()),
928        }
929    }
930}
931
932/// Single order in a batch order request (JSON format for batchOrders param).
933#[derive(Debug, Clone, Serialize)]
934#[serde(rename_all = "camelCase")]
935pub struct BatchOrderItem {
936    /// Trading pair symbol.
937    pub symbol: String,
938    /// Order side (BUY or SELL).
939    pub side: String,
940    /// Order type.
941    #[serde(rename = "type")]
942    pub order_type: String,
943    /// Time in force.
944    #[serde(skip_serializing_if = "Option::is_none")]
945    pub time_in_force: Option<String>,
946    /// Order quantity.
947    #[serde(skip_serializing_if = "Option::is_none")]
948    pub quantity: Option<String>,
949    /// Limit price.
950    #[serde(skip_serializing_if = "Option::is_none")]
951    pub price: Option<String>,
952    /// Client order ID.
953    #[serde(skip_serializing_if = "Option::is_none")]
954    pub new_client_order_id: Option<String>,
955    /// Stop price for stop orders.
956    #[serde(skip_serializing_if = "Option::is_none")]
957    pub stop_price: Option<String>,
958}
959
960impl BatchOrderItem {
961    /// Creates a batch order item from NewOrderParams.
962    #[must_use]
963    pub fn from_params(params: &NewOrderParams) -> Self {
964        Self {
965            symbol: params.symbol.clone(),
966            side: format!("{:?}", params.side).to_uppercase(),
967            order_type: format!("{:?}", params.order_type).to_uppercase(),
968            time_in_force: params
969                .time_in_force
970                .map(|t| format!("{t:?}").to_uppercase()),
971            quantity: params.quantity.clone(),
972            price: params.price.clone(),
973            new_client_order_id: params.new_client_order_id.clone(),
974            stop_price: params.stop_price.clone(),
975        }
976    }
977}
978
979/// Single cancel in a batch cancel request.
980#[derive(Debug, Clone, Serialize)]
981#[serde(rename_all = "camelCase")]
982pub struct BatchCancelItem {
983    /// Trading pair symbol.
984    pub symbol: String,
985    /// Order ID to cancel.
986    #[serde(skip_serializing_if = "Option::is_none")]
987    pub order_id: Option<i64>,
988    /// Original client order ID.
989    #[serde(skip_serializing_if = "Option::is_none")]
990    pub orig_client_order_id: Option<String>,
991}
992
993impl BatchCancelItem {
994    /// Creates a batch cancel item by order ID.
995    #[must_use]
996    pub fn by_order_id(symbol: impl Into<String>, order_id: i64) -> Self {
997        Self {
998            symbol: symbol.into(),
999            order_id: Some(order_id),
1000            orig_client_order_id: None,
1001        }
1002    }
1003
1004    /// Creates a batch cancel item by client order ID.
1005    #[must_use]
1006    pub fn by_client_order_id(
1007        symbol: impl Into<String>,
1008        client_order_id: impl Into<String>,
1009    ) -> Self {
1010        Self {
1011            symbol: symbol.into(),
1012            order_id: None,
1013            orig_client_order_id: Some(client_order_id.into()),
1014        }
1015    }
1016}
1017
1018#[cfg(test)]
1019mod tests {
1020    use rstest::rstest;
1021    use zeroize::Zeroize;
1022
1023    use super::*;
1024
1025    #[rstest]
1026    fn test_listen_key_params_preserve_wire_value_and_redact_debug() {
1027        let mut params = ListenKeyParams::new("listen-key-secret");
1028
1029        let serialized = serde_urlencoded::to_string(&params).unwrap();
1030        let debug = format!("{params:?}");
1031
1032        assert_eq!(serialized, "listenKey=listen-key-secret");
1033        assert!(debug.contains(REDACTED));
1034        assert!(!debug.contains(params.listen_key.expose_secret()));
1035
1036        params.zeroize();
1037        assert!(params.listen_key.expose_secret().is_empty());
1038    }
1039}