nautilus_bitmex/http/
query.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 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 /order 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 GetOrderParams {
142    /// 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`.
143    #[serde(skip_serializing_if = "Option::is_none")]
144    pub symbol: Option<String>,
145    /// 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.
146    #[serde(
147        skip_serializing_if = "Option::is_none",
148        serialize_with = "serialize_json_as_string"
149    )]
150    pub filter: Option<Value>,
151    /// 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.
152    #[serde(
153        skip_serializing_if = "Option::is_none",
154        serialize_with = "serialize_json_as_string"
155    )]
156    pub columns: Option<Value>,
157    /// Number of results to fetch.
158    #[serde(skip_serializing_if = "Option::is_none")]
159    pub count: Option<i32>,
160    /// Starting point for results.
161    #[serde(skip_serializing_if = "Option::is_none")]
162    pub start: Option<i32>,
163    /// If true, will sort results newest first.
164    #[serde(skip_serializing_if = "Option::is_none")]
165    pub reverse: Option<bool>,
166    /// Starting date filter for results.
167    #[serde(skip_serializing_if = "Option::is_none")]
168    pub start_time: Option<DateTime<Utc>>,
169    /// Ending date filter for results.
170    #[serde(skip_serializing_if = "Option::is_none")]
171    pub end_time: Option<DateTime<Utc>>,
172}
173
174/// Parameters for the POST /order endpoint.
175#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
176#[builder(default)]
177#[builder(setter(into, strip_option))]
178#[serde(rename_all = "camelCase")]
179pub struct PostOrderParams {
180    /// Instrument symbol. e.g. 'XBTUSD'.
181    pub symbol: String,
182    /// Order side. Valid options: Buy, Sell. Defaults to 'Buy' unless `orderQty` is negative.
183    #[serde(skip_serializing_if = "Option::is_none")]
184    pub side: Option<BitmexSide>,
185    /// Order quantity in units of the instrument (i.e. contracts).
186    #[serde(skip_serializing_if = "Option::is_none")]
187    pub order_qty: Option<u32>,
188    /// Optional limit price for `Limit`, `StopLimit`, and `LimitIfTouched` orders.
189    #[serde(skip_serializing_if = "Option::is_none")]
190    pub price: Option<f64>,
191    /// Optional quantity to display in the book. Use 0 for a fully hidden order.
192    #[serde(skip_serializing_if = "Option::is_none")]
193    pub display_qty: Option<u32>,
194    /// 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.
195    #[serde(skip_serializing_if = "Option::is_none")]
196    pub stop_px: Option<f64>,
197    /// Optional Client Order ID. This clOrdID will come back on the order and any related executions.
198    #[serde(skip_serializing_if = "Option::is_none")]
199    #[serde(rename = "clOrdID")]
200    pub cl_ord_id: Option<String>,
201    /// Optional Client Order Link ID for contingent orders.
202    #[serde(skip_serializing_if = "Option::is_none")]
203    #[serde(rename = "clOrdLinkID")]
204    pub cl_ord_link_id: Option<String>,
205    /// 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.
206    #[serde(skip_serializing_if = "Option::is_none")]
207    pub peg_offset_value: Option<f64>,
208    /// Optional peg price type. Valid options: `LastPeg`, `MidPricePeg`, `MarketPeg`, `PrimaryPeg`, `TrailingStopPeg`.
209    #[serde(skip_serializing_if = "Option::is_none")]
210    pub peg_price_type: Option<BitmexPegPriceType>,
211    /// 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.
212    #[serde(skip_serializing_if = "Option::is_none")]
213    pub ord_type: Option<BitmexOrderType>,
214    /// Time in force. Valid options: `Day`, `GoodTillCancel`, `ImmediateOrCancel`, `FillOrKill`. Defaults to `GoodTillCancel` for `Limit`, `StopLimit`, and `LimitIfTouched` orders.
215    #[serde(skip_serializing_if = "Option::is_none")]
216    pub time_in_force: Option<BitmexTimeInForce>,
217    /// 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.
218    #[serde(
219        serialize_with = "serialize_exec_instructions_optional",
220        skip_serializing_if = "is_exec_inst_empty"
221    )]
222    pub exec_inst: Option<Vec<BitmexExecInstruction>>,
223    /// Deprecated: linked orders are not supported after 2018/11/10.
224    #[serde(skip_serializing_if = "Option::is_none")]
225    pub contingency_type: Option<BitmexContingencyType>,
226    /// Optional order annotation. e.g. 'Take profit'.
227    #[serde(skip_serializing_if = "Option::is_none")]
228    pub text: Option<String>,
229}
230
231fn is_exec_inst_empty(exec_inst: &Option<Vec<BitmexExecInstruction>>) -> bool {
232    exec_inst.as_ref().is_none_or(Vec::is_empty)
233}
234
235fn serialize_exec_instructions_optional<S>(
236    instructions: &Option<Vec<BitmexExecInstruction>>,
237    serializer: S,
238) -> Result<S::Ok, S::Error>
239where
240    S: serde::Serializer,
241{
242    match instructions {
243        Some(inst) if !inst.is_empty() => {
244            let joined = inst
245                .iter()
246                .map(std::string::ToString::to_string)
247                .collect::<Vec<_>>()
248                .join(",");
249            serializer.serialize_some(&joined)
250        }
251        _ => serializer.serialize_none(),
252    }
253}
254
255/// Parameters for the DELETE /order endpoint.
256#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
257#[builder(default)]
258#[builder(setter(into, strip_option))]
259#[serde(rename_all = "camelCase")]
260pub struct DeleteOrderParams {
261    /// Order ID(s) (venue-assigned).
262    #[serde(
263        skip_serializing_if = "Option::is_none",
264        serialize_with = "serialize_string_vec_as_json",
265        rename = "orderID"
266    )]
267    pub order_id: Option<Vec<String>>,
268    /// Client Order ID(s). See POST /order.
269    #[serde(
270        skip_serializing_if = "Option::is_none",
271        serialize_with = "serialize_string_vec_as_json",
272        rename = "clOrdID"
273    )]
274    pub cl_ord_id: Option<Vec<String>>,
275    #[serde(skip_serializing_if = "Option::is_none")]
276    /// Optional cancellation annotation. e.g. 'Spread Exceeded'.
277    pub text: Option<String>,
278}
279
280impl DeleteOrderParamsBuilder {
281    /// Build the parameters with validation.
282    ///
283    /// # Errors
284    ///
285    /// Returns an error if both order_id and cl_ord_id are provided.
286    pub fn build_validated(self) -> Result<DeleteOrderParams, String> {
287        let params = self.build().map_err(|e| format!("Failed to build: {e}"))?;
288
289        // Validate that only one of order_id or cl_ord_id is provided
290        if params.order_id.is_some() && params.cl_ord_id.is_some() {
291            return Err("Cannot provide both order_id and cl_ord_id - use only one".to_string());
292        }
293
294        // Validate that at least one is provided
295        if params.order_id.is_none() && params.cl_ord_id.is_none() {
296            return Err("Must provide either order_id or cl_ord_id".to_string());
297        }
298
299        Ok(params)
300    }
301}
302
303/// Parameters for the DELETE /order/all endpoint.
304///
305/// # References
306///
307/// <https://www.bitmex.com/api/explorer/#!/Order/Order_cancelAll>
308#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
309#[builder(default)]
310#[builder(setter(into, strip_option))]
311#[serde(rename_all = "camelCase")]
312pub struct DeleteAllOrdersParams {
313    /// Optional symbol. If provided, only cancels orders for that symbol.
314    #[serde(skip_serializing_if = "Option::is_none")]
315    pub symbol: Option<String>,
316    /// Optional filter for cancellation. Send JSON key/value pairs, such as `{"side": "Buy"}`.
317    #[serde(
318        skip_serializing_if = "Option::is_none",
319        serialize_with = "serialize_json_as_string"
320    )]
321    pub filter: Option<Value>,
322    /// Optional cancellation annotation. e.g. 'Spread Exceeded'.
323    #[serde(skip_serializing_if = "Option::is_none")]
324    pub text: Option<String>,
325}
326
327/// Parameters for the PUT /order endpoint.
328#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
329#[builder(default)]
330#[builder(setter(into, strip_option))]
331#[serde(rename_all = "camelCase")]
332pub struct PutOrderParams {
333    /// Order ID
334    #[serde(rename = "orderID")]
335    pub order_id: Option<String>,
336    /// Client Order ID. See POST /order.
337    #[serde(rename = "origClOrdID")]
338    pub orig_cl_ord_id: Option<String>,
339    /// Optional new Client Order ID, requires `origClOrdID`.
340    #[serde(rename = "clOrdID")]
341    pub cl_ord_id: Option<String>,
342    /// Optional order quantity in units of the instrument (i.e. contracts).
343    pub order_qty: Option<u32>,
344    /// Optional leaves quantity in units of the instrument (i.e. contracts). Useful for amending partially filled orders.
345    pub leaves_qty: Option<u32>,
346    /// Optional limit price for `Limit`, `StopLimit`, and `LimitIfTouched` orders.
347    pub price: Option<f64>,
348    /// 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.
349    pub stop_px: Option<f64>,
350    /// 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.
351    pub peg_offset_value: Option<f64>,
352    /// Optional amend annotation. e.g. 'Adjust skew'.
353    pub text: Option<String>,
354}
355
356/// Parameters for the GET /execution/tradeHistory endpoint.
357#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
358#[builder(default)]
359#[builder(setter(into, strip_option))]
360#[serde(rename_all = "camelCase")]
361pub struct GetExecutionParams {
362    /// 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`.
363    #[serde(skip_serializing_if = "Option::is_none")]
364    pub symbol: Option<String>,
365    /// 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.
366    #[serde(
367        skip_serializing_if = "Option::is_none",
368        serialize_with = "serialize_json_as_string"
369    )]
370    pub filter: Option<Value>,
371    /// 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.
372    #[serde(
373        skip_serializing_if = "Option::is_none",
374        serialize_with = "serialize_json_as_string"
375    )]
376    pub columns: Option<Value>,
377    /// Number of results to fetch.
378    #[serde(skip_serializing_if = "Option::is_none")]
379    pub count: Option<i32>,
380    /// Starting point for results.
381    #[serde(skip_serializing_if = "Option::is_none")]
382    pub start: Option<i32>,
383    /// If true, will sort results newest first.
384    #[serde(skip_serializing_if = "Option::is_none")]
385    pub reverse: Option<bool>,
386    /// Starting date filter for results.
387    #[serde(skip_serializing_if = "Option::is_none")]
388    pub start_time: Option<DateTime<Utc>>,
389    /// Ending date filter for results.
390    #[serde(skip_serializing_if = "Option::is_none")]
391    pub end_time: Option<DateTime<Utc>>,
392}
393
394/// Parameters for the POST /position/leverage endpoint.
395#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
396#[builder(default)]
397#[builder(setter(into, strip_option))]
398#[serde(rename_all = "camelCase")]
399pub struct PostPositionLeverageParams {
400    /// Symbol to set leverage for.
401    pub symbol: String,
402    /// Leverage value (0.01 to 100).
403    pub leverage: f64,
404    /// Optional leverage for long position (isolated margin only).
405    #[serde(skip_serializing_if = "Option::is_none")]
406    pub target_account_id: Option<i64>,
407}
408
409/// Parameters for the GET /position endpoint.
410#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
411#[builder(default)]
412#[builder(setter(into, strip_option))]
413#[serde(rename_all = "camelCase")]
414pub struct GetPositionParams {
415    /// 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.
416    #[serde(
417        skip_serializing_if = "Option::is_none",
418        serialize_with = "serialize_json_as_string"
419    )]
420    pub filter: Option<Value>,
421    /// 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.
422    #[serde(
423        skip_serializing_if = "Option::is_none",
424        serialize_with = "serialize_json_as_string"
425    )]
426    pub columns: Option<Value>,
427    /// Number of results to fetch.
428    #[serde(skip_serializing_if = "Option::is_none")]
429    pub count: Option<i32>,
430}