Skip to main content

nautilus_bitmex/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//! Builder types for BitMEX REST query parameters and filters.
17
18use chrono::{DateTime, Utc};
19use derive_builder::Builder;
20use serde::{self, Deserialize, Serialize, Serializer};
21use serde_json::Value;
22
23/// Serialize a JSON Value as a string for URL encoding.
24fn serialize_json_as_string<S>(value: &Option<Value>, serializer: S) -> Result<S::Ok, S::Error>
25where
26    S: Serializer,
27{
28    match value {
29        Some(v) => serializer.serialize_str(&v.to_string()),
30        None => serializer.serialize_none(),
31    }
32}
33
34use crate::common::enums::{
35    BitmexContingencyType, BitmexExecInstruction, BitmexOrderType, BitmexPegPriceType, BitmexSide,
36    BitmexTimeInForce,
37};
38
39fn serialize_string_vec_as_json<S>(
40    values: &Option<Vec<String>>,
41    serializer: S,
42) -> Result<S::Ok, S::Error>
43where
44    S: serde::Serializer,
45{
46    match values {
47        Some(vec) => {
48            let json_array = serde_json::to_string(vec).map_err(serde::ser::Error::custom)?;
49            serializer.serialize_str(&json_array)
50        }
51        None => serializer.serialize_none(),
52    }
53}
54
55/// Parameters for the GET /trade endpoint.
56#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
57#[builder(default)]
58#[builder(setter(into, strip_option))]
59#[serde(rename_all = "camelCase")]
60pub struct GetTradeParams {
61    /// Instrument symbol. Send a bare series (e.g., XBT) to get data for the nearest expiring contract in that series.  You can also send a timeframe, e.g. `XBT:quarterly`. Timeframes are `nearest`, `daily`, `weekly`, `monthly`, `quarterly`, `biquarterly`, and `perpetual`.
62    pub symbol: Option<String>,
63    /// Generic table filter. Send JSON key/value pairs, such as `{"key": "value"}`. You can key on individual fields, and do more advanced querying on timestamps. See the [Timestamp Docs](https://www.bitmex.com/app/restAPI#Timestamp-Filters) for more details.
64    #[serde(
65        skip_serializing_if = "Option::is_none",
66        serialize_with = "serialize_json_as_string"
67    )]
68    pub filter: Option<Value>,
69    /// Array of column names to fetch. If omitted, will return all columns.  Note that this method will always return item keys, even when not specified, so you may receive more columns that you expect.
70    #[serde(
71        skip_serializing_if = "Option::is_none",
72        serialize_with = "serialize_json_as_string"
73    )]
74    pub columns: Option<Value>,
75    /// Number of results to fetch.
76    #[serde(skip_serializing_if = "Option::is_none")]
77    pub count: Option<i32>,
78    /// Starting point for results.
79    #[serde(skip_serializing_if = "Option::is_none")]
80    pub start: Option<i32>,
81    /// If true, will sort results newest first.
82    #[serde(skip_serializing_if = "Option::is_none")]
83    pub reverse: Option<bool>,
84    /// Starting date filter for results.
85    #[serde(skip_serializing_if = "Option::is_none")]
86    pub start_time: Option<DateTime<Utc>>,
87    /// Ending date filter for results.
88    #[serde(skip_serializing_if = "Option::is_none")]
89    pub end_time: Option<DateTime<Utc>>,
90}
91
92/// Parameters for the GET /trade/bucketed endpoint.
93#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
94#[builder(default)]
95#[builder(setter(into, strip_option))]
96#[serde(rename_all = "camelCase")]
97pub struct GetTradeBucketedParams {
98    /// Instrument symbol.
99    #[serde(skip_serializing_if = "Option::is_none")]
100    pub symbol: Option<String>,
101    /// Time interval for the bucketed data (e.g. "1m", "5m", "1h", "1d").
102    #[serde(skip_serializing_if = "Option::is_none")]
103    pub bin_size: Option<String>,
104    /// If true, will return partial bins even if the bin spans less than the full interval.
105    #[serde(skip_serializing_if = "Option::is_none")]
106    pub partial: Option<bool>,
107    /// Generic table filter. Send JSON key/value pairs, such as `{"key": "value"}`.
108    #[serde(
109        skip_serializing_if = "Option::is_none",
110        serialize_with = "serialize_json_as_string"
111    )]
112    pub filter: Option<Value>,
113    /// Array of column names to fetch. If omitted, will return all columns.
114    #[serde(
115        skip_serializing_if = "Option::is_none",
116        serialize_with = "serialize_json_as_string"
117    )]
118    pub columns: Option<Value>,
119    /// Number of results to fetch.
120    #[serde(skip_serializing_if = "Option::is_none")]
121    pub count: Option<i32>,
122    /// Starting point for results.
123    #[serde(skip_serializing_if = "Option::is_none")]
124    pub start: Option<i32>,
125    /// If true, will sort results newest first.
126    #[serde(skip_serializing_if = "Option::is_none")]
127    pub reverse: Option<bool>,
128    /// Starting date filter for results.
129    #[serde(skip_serializing_if = "Option::is_none")]
130    pub start_time: Option<DateTime<Utc>>,
131    /// Ending date filter for results.
132    #[serde(skip_serializing_if = "Option::is_none")]
133    pub end_time: Option<DateTime<Utc>>,
134}
135
136/// Parameters for the GET /orderBook/L2 endpoint.
137#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
138#[builder(default)]
139#[builder(setter(into, strip_option))]
140#[serde(rename_all = "camelCase")]
141pub struct GetOrderBookL2Params {
142    /// Instrument symbol.
143    pub symbol: String,
144    /// Book depth.
145    #[serde(skip_serializing_if = "Option::is_none")]
146    pub depth: Option<u32>,
147}
148
149/// Parameters for the GET /funding endpoint.
150#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
151#[builder(default)]
152#[builder(setter(into, strip_option))]
153#[serde(rename_all = "camelCase")]
154pub struct GetFundingParams {
155    /// Instrument symbol.
156    #[serde(skip_serializing_if = "Option::is_none")]
157    pub symbol: Option<String>,
158    /// Generic table filter. Send JSON key/value pairs, such as `{"key": "value"}`.
159    #[serde(
160        skip_serializing_if = "Option::is_none",
161        serialize_with = "serialize_json_as_string"
162    )]
163    pub filter: Option<Value>,
164    /// Array of column names to fetch. If omitted, all columns are returned.
165    #[serde(
166        skip_serializing_if = "Option::is_none",
167        serialize_with = "serialize_json_as_string"
168    )]
169    pub columns: Option<Value>,
170    /// Number of results to fetch.
171    #[serde(skip_serializing_if = "Option::is_none")]
172    pub count: Option<i32>,
173    /// Starting point for results.
174    #[serde(skip_serializing_if = "Option::is_none")]
175    pub start: Option<i32>,
176    /// If true, sorts results newest first.
177    #[serde(skip_serializing_if = "Option::is_none")]
178    pub reverse: Option<bool>,
179    /// Starting date filter for results.
180    #[serde(skip_serializing_if = "Option::is_none")]
181    pub start_time: Option<DateTime<Utc>>,
182    /// Ending date filter for results.
183    #[serde(skip_serializing_if = "Option::is_none")]
184    pub end_time: Option<DateTime<Utc>>,
185}
186
187/// Parameters for the GET /order endpoint.
188#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
189#[builder(default)]
190#[builder(setter(into, strip_option))]
191#[serde(rename_all = "camelCase")]
192pub struct GetOrderParams {
193    /// Instrument symbol. Send a bare series (e.g., XBT) to get data for the nearest expiring contract in that series.  You can also send a timeframe, e.g. `XBT:quarterly`. Timeframes are `nearest`, `daily`, `weekly`, `monthly`, `quarterly`, `biquarterly`, and `perpetual`.
194    #[serde(skip_serializing_if = "Option::is_none")]
195    pub symbol: Option<String>,
196    /// Generic table filter. Send JSON key/value pairs, such as `{"key": "value"}`. You can key on individual fields, and do more advanced querying on timestamps. See the [Timestamp Docs](https://www.bitmex.com/app/restAPI#Timestamp-Filters) for more details.
197    #[serde(
198        skip_serializing_if = "Option::is_none",
199        serialize_with = "serialize_json_as_string"
200    )]
201    pub filter: Option<Value>,
202    /// Array of column names to fetch. If omitted, will return all columns.  Note that this method will always return item keys, even when not specified, so you may receive more columns that you expect.
203    #[serde(
204        skip_serializing_if = "Option::is_none",
205        serialize_with = "serialize_json_as_string"
206    )]
207    pub columns: Option<Value>,
208    /// Number of results to fetch.
209    #[serde(skip_serializing_if = "Option::is_none")]
210    pub count: Option<i32>,
211    /// Starting point for results.
212    #[serde(skip_serializing_if = "Option::is_none")]
213    pub start: Option<i32>,
214    /// If true, will sort results newest first.
215    #[serde(skip_serializing_if = "Option::is_none")]
216    pub reverse: Option<bool>,
217    /// Starting date filter for results.
218    #[serde(skip_serializing_if = "Option::is_none")]
219    pub start_time: Option<DateTime<Utc>>,
220    /// Ending date filter for results.
221    #[serde(skip_serializing_if = "Option::is_none")]
222    pub end_time: Option<DateTime<Utc>>,
223}
224
225/// Parameters for the POST /order endpoint.
226#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
227#[builder(default)]
228#[builder(setter(into, strip_option))]
229#[serde(rename_all = "camelCase")]
230pub struct PostOrderParams {
231    /// Instrument symbol. e.g. 'XBTUSD'.
232    pub symbol: String,
233    /// Order side. Valid options: Buy, Sell. Defaults to 'Buy' unless `orderQty` is negative.
234    #[serde(skip_serializing_if = "Option::is_none")]
235    pub side: Option<BitmexSide>,
236    /// Order quantity in units of the instrument (i.e. contracts).
237    #[serde(skip_serializing_if = "Option::is_none")]
238    pub order_qty: Option<u32>,
239    /// Optional limit price for `Limit`, `StopLimit`, and `LimitIfTouched` orders.
240    #[serde(skip_serializing_if = "Option::is_none")]
241    pub price: Option<f64>,
242    /// Optional quantity to display in the book. Use 0 for a fully hidden order.
243    #[serde(skip_serializing_if = "Option::is_none")]
244    pub display_qty: Option<u32>,
245    /// Optional trigger price for `Stop`, `StopLimit`, `MarketIfTouched`, and `LimitIfTouched` orders. Use a price below the current price for stop-sell orders and buy-if-touched orders. Use `execInst` of `MarkPrice` or `LastPrice` to define the current price used for triggering.
246    #[serde(skip_serializing_if = "Option::is_none")]
247    pub stop_px: Option<f64>,
248    /// Optional Client Order ID. This clOrdID will come back on the order and any related executions.
249    #[serde(skip_serializing_if = "Option::is_none")]
250    #[serde(rename = "clOrdID")]
251    pub cl_ord_id: Option<String>,
252    /// Optional Client Order Link ID for contingent orders.
253    #[serde(skip_serializing_if = "Option::is_none")]
254    #[serde(rename = "clOrdLinkID")]
255    pub cl_ord_link_id: Option<String>,
256    /// Optional trailing offset from the current price for `Stop`, `StopLimit`, `MarketIfTouched`, and `LimitIfTouched` orders; use a negative offset for stop-sell orders and buy-if-touched orders. Optional offset from the peg price for 'Pegged' orders.
257    #[serde(skip_serializing_if = "Option::is_none")]
258    pub peg_offset_value: Option<f64>,
259    /// Optional peg price type. Valid options: `LastPeg`, `MidPricePeg`, `MarketPeg`, `PrimaryPeg`, `TrailingStopPeg`.
260    #[serde(skip_serializing_if = "Option::is_none")]
261    pub peg_price_type: Option<BitmexPegPriceType>,
262    /// Order type. Valid options: Market, Limit, Stop, `StopLimit`, `MarketIfTouched`, `LimitIfTouched`, Pegged. Defaults to `Limit` when `price` is specified. Defaults to `Stop` when `stopPx` is specified. Defaults to `StopLimit` when `price` and `stopPx` are specified.
263    #[serde(skip_serializing_if = "Option::is_none")]
264    pub ord_type: Option<BitmexOrderType>,
265    /// Time in force. Valid options: `Day`, `GoodTillCancel`, `ImmediateOrCancel`, `FillOrKill`. Defaults to `GoodTillCancel` for `Limit`, `StopLimit`, and `LimitIfTouched` orders.
266    #[serde(skip_serializing_if = "Option::is_none")]
267    pub time_in_force: Option<BitmexTimeInForce>,
268    /// Optional execution instructions. Valid options: `ParticipateDoNotInitiate`, `AllOrNone`, `MarkPrice`, `IndexPrice`, `LastPrice`, `Close`, `ReduceOnly`, Fixed. `AllOrNone` instruction requires `displayQty` to be 0. `MarkPrice`, `IndexPrice` or `LastPrice` instruction valid for `Stop`, `StopLimit`, `MarketIfTouched`, and `LimitIfTouched` orders.
269    #[serde(
270        serialize_with = "serialize_exec_instructions_optional",
271        skip_serializing_if = "is_exec_inst_empty"
272    )]
273    pub exec_inst: Option<Vec<BitmexExecInstruction>>,
274    /// Deprecated: linked orders are not supported after 2018/11/10.
275    #[serde(skip_serializing_if = "Option::is_none")]
276    pub contingency_type: Option<BitmexContingencyType>,
277    /// Optional order annotation. e.g. 'Take profit'.
278    #[serde(skip_serializing_if = "Option::is_none")]
279    pub text: Option<String>,
280}
281
282fn is_exec_inst_empty(exec_inst: &Option<Vec<BitmexExecInstruction>>) -> bool {
283    exec_inst.as_ref().is_none_or(Vec::is_empty)
284}
285
286fn serialize_exec_instructions_optional<S>(
287    instructions: &Option<Vec<BitmexExecInstruction>>,
288    serializer: S,
289) -> Result<S::Ok, S::Error>
290where
291    S: serde::Serializer,
292{
293    match instructions {
294        Some(inst) if !inst.is_empty() => {
295            let joined = inst
296                .iter()
297                .map(std::string::ToString::to_string)
298                .collect::<Vec<_>>()
299                .join(",");
300            serializer.serialize_some(&joined)
301        }
302        _ => serializer.serialize_none(),
303    }
304}
305
306/// Parameters for the DELETE /order endpoint.
307#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
308#[builder(default)]
309#[builder(setter(into, strip_option))]
310#[serde(rename_all = "camelCase")]
311pub struct DeleteOrderParams {
312    /// Order ID(s) (venue-assigned).
313    #[serde(
314        skip_serializing_if = "Option::is_none",
315        serialize_with = "serialize_string_vec_as_json",
316        rename = "orderID"
317    )]
318    pub order_id: Option<Vec<String>>,
319    /// Client Order ID(s). See POST /order.
320    #[serde(
321        skip_serializing_if = "Option::is_none",
322        serialize_with = "serialize_string_vec_as_json",
323        rename = "clOrdID"
324    )]
325    pub cl_ord_id: Option<Vec<String>>,
326    #[serde(skip_serializing_if = "Option::is_none")]
327    /// Optional cancellation annotation. e.g. 'Spread Exceeded'.
328    pub text: Option<String>,
329}
330
331impl DeleteOrderParamsBuilder {
332    /// Build the parameters with validation.
333    ///
334    /// # Errors
335    ///
336    /// Returns an error if both order_id and cl_ord_id are provided.
337    pub fn build_validated(self) -> Result<DeleteOrderParams, String> {
338        let params = self.build().map_err(|e| format!("Failed to build: {e}"))?;
339
340        // Validate that only one of order_id or cl_ord_id is provided
341        if params.order_id.is_some() && params.cl_ord_id.is_some() {
342            return Err("Cannot provide both order_id and cl_ord_id - use only one".to_string());
343        }
344
345        // Validate that at least one is provided
346        if params.order_id.is_none() && params.cl_ord_id.is_none() {
347            return Err("Must provide either order_id or cl_ord_id".to_string());
348        }
349
350        Ok(params)
351    }
352}
353
354/// Parameters for the DELETE /order/all endpoint.
355///
356/// # References
357///
358/// <https://www.bitmex.com/api/explorer/#!/Order/Order_cancelAll>
359#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
360#[builder(default)]
361#[builder(setter(into, strip_option))]
362#[serde(rename_all = "camelCase")]
363pub struct DeleteAllOrdersParams {
364    /// Optional symbol. If provided, only cancels orders for that symbol.
365    #[serde(skip_serializing_if = "Option::is_none")]
366    pub symbol: Option<String>,
367    /// Optional filter for cancellation. Send JSON key/value pairs, such as `{"side": "Buy"}`.
368    #[serde(
369        skip_serializing_if = "Option::is_none",
370        serialize_with = "serialize_json_as_string"
371    )]
372    pub filter: Option<Value>,
373    /// Optional cancellation annotation. e.g. 'Spread Exceeded'.
374    #[serde(skip_serializing_if = "Option::is_none")]
375    pub text: Option<String>,
376}
377
378/// Parameters for the PUT /order endpoint.
379#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
380#[builder(default)]
381#[builder(setter(into, strip_option))]
382#[serde(rename_all = "camelCase")]
383pub struct PutOrderParams {
384    /// Order ID
385    #[serde(rename = "orderID")]
386    pub order_id: Option<String>,
387    /// Client Order ID. See POST /order.
388    #[serde(rename = "origClOrdID")]
389    pub orig_cl_ord_id: Option<String>,
390    /// Optional new Client Order ID, requires `origClOrdID`.
391    #[serde(rename = "clOrdID")]
392    pub cl_ord_id: Option<String>,
393    /// Optional order quantity in units of the instrument (i.e. contracts).
394    pub order_qty: Option<u32>,
395    /// Optional leaves quantity in units of the instrument (i.e. contracts). Useful for amending partially filled orders.
396    pub leaves_qty: Option<u32>,
397    /// Optional limit price for `Limit`, `StopLimit`, and `LimitIfTouched` orders.
398    pub price: Option<f64>,
399    /// Optional trigger price for `Stop`, `StopLimit`, `MarketIfTouched`, and `LimitIfTouched` orders. Use a price below the current price for stop-sell orders and buy-if-touched orders.
400    pub stop_px: Option<f64>,
401    /// Optional trailing offset from the current price for `Stop`, `StopLimit`, `MarketIfTouched`, and `LimitIfTouched` orders; use a negative offset for stop-sell orders and buy-if-touched orders. Optional offset from the peg price for 'Pegged' orders.
402    pub peg_offset_value: Option<f64>,
403    /// Optional amend annotation. e.g. 'Adjust skew'.
404    pub text: Option<String>,
405}
406
407/// Parameters for the GET /execution/tradeHistory endpoint.
408#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
409#[builder(default)]
410#[builder(setter(into, strip_option))]
411#[serde(rename_all = "camelCase")]
412pub struct GetExecutionParams {
413    /// Instrument symbol. Send a bare series (e.g. XBT) to get data for the nearest expiring contract in that series.  You can also send a timeframe, e.g. `XBT:quarterly`. Timeframes are `nearest`, `daily`, `weekly`, `monthly`, `quarterly`, `biquarterly`, and `perpetual`.
414    #[serde(skip_serializing_if = "Option::is_none")]
415    pub symbol: Option<String>,
416    /// Generic table filter. Send JSON key/value pairs, such as `{"key": "value"}`. You can key on individual fields, and do more advanced querying on timestamps. See the [Timestamp Docs](https://www.bitmex.com/app/restAPI#Timestamp-Filters) for more details.
417    #[serde(
418        skip_serializing_if = "Option::is_none",
419        serialize_with = "serialize_json_as_string"
420    )]
421    pub filter: Option<Value>,
422    /// Array of column names to fetch. If omitted, will return all columns.  Note that this method will always return item keys, even when not specified, so you may receive more columns that you expect.
423    #[serde(
424        skip_serializing_if = "Option::is_none",
425        serialize_with = "serialize_json_as_string"
426    )]
427    pub columns: Option<Value>,
428    /// Number of results to fetch.
429    #[serde(skip_serializing_if = "Option::is_none")]
430    pub count: Option<i32>,
431    /// Starting point for results.
432    #[serde(skip_serializing_if = "Option::is_none")]
433    pub start: Option<i32>,
434    /// If true, will sort results newest first.
435    #[serde(skip_serializing_if = "Option::is_none")]
436    pub reverse: Option<bool>,
437    /// Starting date filter for results.
438    #[serde(skip_serializing_if = "Option::is_none")]
439    pub start_time: Option<DateTime<Utc>>,
440    /// Ending date filter for results.
441    #[serde(skip_serializing_if = "Option::is_none")]
442    pub end_time: Option<DateTime<Utc>>,
443}
444
445/// Parameters for the POST /position/leverage endpoint.
446#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
447#[builder(default)]
448#[builder(setter(into, strip_option))]
449#[serde(rename_all = "camelCase")]
450pub struct PostPositionLeverageParams {
451    /// Symbol to set leverage for.
452    pub symbol: String,
453    /// Leverage value (0.01 to 100).
454    pub leverage: f64,
455    /// Optional leverage for long position (isolated margin only).
456    #[serde(skip_serializing_if = "Option::is_none")]
457    pub target_account_id: Option<i64>,
458}
459
460/// Parameters for the POST /order/cancelAllAfter endpoint (dead man's switch).
461///
462/// # References
463///
464/// <https://www.bitmex.com/api/explorer/#!/Order/Order_cancelAllAfter>
465#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
466#[builder(default)]
467#[builder(setter(into, strip_option))]
468pub struct PostCancelAllAfterParams {
469    /// Timeout in milliseconds. Setting to 0 disarms the dead man's switch.
470    pub timeout: u64,
471}
472
473/// Parameters for the GET /position endpoint.
474#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
475#[builder(default)]
476#[builder(setter(into, strip_option))]
477#[serde(rename_all = "camelCase")]
478pub struct GetPositionParams {
479    /// Generic table filter. Send JSON key/value pairs, such as `{"key": "value"}`. You can key on individual fields, and do more advanced querying on timestamps. See the [Timestamp Docs](https://www.bitmex.com/app/restAPI#Timestamp-Filters) for more details.
480    #[serde(
481        skip_serializing_if = "Option::is_none",
482        serialize_with = "serialize_json_as_string"
483    )]
484    pub filter: Option<Value>,
485    /// Array of column names to fetch. If omitted, will return all columns.  Note that this method will always return item keys, even when not specified, so you may receive more columns that you expect.
486    #[serde(
487        skip_serializing_if = "Option::is_none",
488        serialize_with = "serialize_json_as_string"
489    )]
490    pub columns: Option<Value>,
491    /// Number of results to fetch.
492    #[serde(skip_serializing_if = "Option::is_none")]
493    pub count: Option<i32>,
494}
495
496#[cfg(test)]
497mod tests {
498    use rstest::rstest;
499
500    use super::*;
501
502    #[rstest]
503    fn test_cancel_all_after_params_serializes() {
504        let params = PostCancelAllAfterParams { timeout: 60_000 };
505        let encoded = serde_urlencoded::to_string(&params).unwrap();
506        assert_eq!(encoded, "timeout=60000");
507    }
508
509    #[rstest]
510    fn test_cancel_all_after_params_disarm_serializes() {
511        let params = PostCancelAllAfterParams { timeout: 0 };
512        let encoded = serde_urlencoded::to_string(&params).unwrap();
513        assert_eq!(encoded, "timeout=0");
514    }
515}