nautilus-bitmex 0.55.0

BitMEX exchange integration adapter for the Nautilus trading engine
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
// -------------------------------------------------------------------------------------------------
//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
//  https://nautechsystems.io
//
//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
//  You may not use this file except in compliance with the License.
//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
//
//  Unless required by applicable law or agreed to in writing, software
//  distributed under the License is distributed on an "AS IS" BASIS,
//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//  See the License for the specific language governing permissions and
//  limitations under the License.
// -------------------------------------------------------------------------------------------------

//! Builder types for BitMEX REST query parameters and filters.

use chrono::{DateTime, Utc};
use derive_builder::Builder;
use serde::{self, Deserialize, Serialize, Serializer};
use serde_json::Value;

/// Serialize a JSON Value as a string for URL encoding.
fn serialize_json_as_string<S>(value: &Option<Value>, serializer: S) -> Result<S::Ok, S::Error>
where
    S: Serializer,
{
    match value {
        Some(v) => serializer.serialize_str(&v.to_string()),
        None => serializer.serialize_none(),
    }
}

use crate::common::enums::{
    BitmexContingencyType, BitmexExecInstruction, BitmexOrderType, BitmexPegPriceType, BitmexSide,
    BitmexTimeInForce,
};

fn serialize_string_vec_as_json<S>(
    values: &Option<Vec<String>>,
    serializer: S,
) -> Result<S::Ok, S::Error>
where
    S: serde::Serializer,
{
    match values {
        Some(vec) => {
            let json_array = serde_json::to_string(vec).map_err(serde::ser::Error::custom)?;
            serializer.serialize_str(&json_array)
        }
        None => serializer.serialize_none(),
    }
}

/// Parameters for the GET /trade endpoint.
#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
#[builder(default)]
#[builder(setter(into, strip_option))]
#[serde(rename_all = "camelCase")]
pub struct GetTradeParams {
    /// 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`.
    pub symbol: Option<String>,
    /// 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.
    #[serde(
        skip_serializing_if = "Option::is_none",
        serialize_with = "serialize_json_as_string"
    )]
    pub filter: Option<Value>,
    /// 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.
    #[serde(
        skip_serializing_if = "Option::is_none",
        serialize_with = "serialize_json_as_string"
    )]
    pub columns: Option<Value>,
    /// Number of results to fetch.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub count: Option<i32>,
    /// Starting point for results.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub start: Option<i32>,
    /// If true, will sort results newest first.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub reverse: Option<bool>,
    /// Starting date filter for results.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub start_time: Option<DateTime<Utc>>,
    /// Ending date filter for results.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub end_time: Option<DateTime<Utc>>,
}

/// Parameters for the GET /trade/bucketed endpoint.
#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
#[builder(default)]
#[builder(setter(into, strip_option))]
#[serde(rename_all = "camelCase")]
pub struct GetTradeBucketedParams {
    /// Instrument symbol.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub symbol: Option<String>,
    /// Time interval for the bucketed data (e.g. "1m", "5m", "1h", "1d").
    #[serde(skip_serializing_if = "Option::is_none")]
    pub bin_size: Option<String>,
    /// If true, will return partial bins even if the bin spans less than the full interval.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub partial: Option<bool>,
    /// Generic table filter. Send JSON key/value pairs, such as `{"key": "value"}`.
    #[serde(
        skip_serializing_if = "Option::is_none",
        serialize_with = "serialize_json_as_string"
    )]
    pub filter: Option<Value>,
    /// Array of column names to fetch. If omitted, will return all columns.
    #[serde(
        skip_serializing_if = "Option::is_none",
        serialize_with = "serialize_json_as_string"
    )]
    pub columns: Option<Value>,
    /// Number of results to fetch.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub count: Option<i32>,
    /// Starting point for results.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub start: Option<i32>,
    /// If true, will sort results newest first.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub reverse: Option<bool>,
    /// Starting date filter for results.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub start_time: Option<DateTime<Utc>>,
    /// Ending date filter for results.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub end_time: Option<DateTime<Utc>>,
}

/// Parameters for the GET /order endpoint.
#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
#[builder(default)]
#[builder(setter(into, strip_option))]
#[serde(rename_all = "camelCase")]
pub struct GetOrderParams {
    /// 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`.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub symbol: Option<String>,
    /// 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.
    #[serde(
        skip_serializing_if = "Option::is_none",
        serialize_with = "serialize_json_as_string"
    )]
    pub filter: Option<Value>,
    /// 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.
    #[serde(
        skip_serializing_if = "Option::is_none",
        serialize_with = "serialize_json_as_string"
    )]
    pub columns: Option<Value>,
    /// Number of results to fetch.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub count: Option<i32>,
    /// Starting point for results.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub start: Option<i32>,
    /// If true, will sort results newest first.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub reverse: Option<bool>,
    /// Starting date filter for results.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub start_time: Option<DateTime<Utc>>,
    /// Ending date filter for results.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub end_time: Option<DateTime<Utc>>,
}

/// Parameters for the POST /order endpoint.
#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
#[builder(default)]
#[builder(setter(into, strip_option))]
#[serde(rename_all = "camelCase")]
pub struct PostOrderParams {
    /// Instrument symbol. e.g. 'XBTUSD'.
    pub symbol: String,
    /// Order side. Valid options: Buy, Sell. Defaults to 'Buy' unless `orderQty` is negative.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub side: Option<BitmexSide>,
    /// Order quantity in units of the instrument (i.e. contracts).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub order_qty: Option<u32>,
    /// Optional limit price for `Limit`, `StopLimit`, and `LimitIfTouched` orders.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub price: Option<f64>,
    /// Optional quantity to display in the book. Use 0 for a fully hidden order.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub display_qty: Option<u32>,
    /// 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.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub stop_px: Option<f64>,
    /// Optional Client Order ID. This clOrdID will come back on the order and any related executions.
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(rename = "clOrdID")]
    pub cl_ord_id: Option<String>,
    /// Optional Client Order Link ID for contingent orders.
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(rename = "clOrdLinkID")]
    pub cl_ord_link_id: Option<String>,
    /// 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.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub peg_offset_value: Option<f64>,
    /// Optional peg price type. Valid options: `LastPeg`, `MidPricePeg`, `MarketPeg`, `PrimaryPeg`, `TrailingStopPeg`.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub peg_price_type: Option<BitmexPegPriceType>,
    /// 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.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub ord_type: Option<BitmexOrderType>,
    /// Time in force. Valid options: `Day`, `GoodTillCancel`, `ImmediateOrCancel`, `FillOrKill`. Defaults to `GoodTillCancel` for `Limit`, `StopLimit`, and `LimitIfTouched` orders.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub time_in_force: Option<BitmexTimeInForce>,
    /// 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.
    #[serde(
        serialize_with = "serialize_exec_instructions_optional",
        skip_serializing_if = "is_exec_inst_empty"
    )]
    pub exec_inst: Option<Vec<BitmexExecInstruction>>,
    /// Deprecated: linked orders are not supported after 2018/11/10.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub contingency_type: Option<BitmexContingencyType>,
    /// Optional order annotation. e.g. 'Take profit'.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub text: Option<String>,
}

fn is_exec_inst_empty(exec_inst: &Option<Vec<BitmexExecInstruction>>) -> bool {
    exec_inst.as_ref().is_none_or(Vec::is_empty)
}

fn serialize_exec_instructions_optional<S>(
    instructions: &Option<Vec<BitmexExecInstruction>>,
    serializer: S,
) -> Result<S::Ok, S::Error>
where
    S: serde::Serializer,
{
    match instructions {
        Some(inst) if !inst.is_empty() => {
            let joined = inst
                .iter()
                .map(std::string::ToString::to_string)
                .collect::<Vec<_>>()
                .join(",");
            serializer.serialize_some(&joined)
        }
        _ => serializer.serialize_none(),
    }
}

/// Parameters for the DELETE /order endpoint.
#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
#[builder(default)]
#[builder(setter(into, strip_option))]
#[serde(rename_all = "camelCase")]
pub struct DeleteOrderParams {
    /// Order ID(s) (venue-assigned).
    #[serde(
        skip_serializing_if = "Option::is_none",
        serialize_with = "serialize_string_vec_as_json",
        rename = "orderID"
    )]
    pub order_id: Option<Vec<String>>,
    /// Client Order ID(s). See POST /order.
    #[serde(
        skip_serializing_if = "Option::is_none",
        serialize_with = "serialize_string_vec_as_json",
        rename = "clOrdID"
    )]
    pub cl_ord_id: Option<Vec<String>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    /// Optional cancellation annotation. e.g. 'Spread Exceeded'.
    pub text: Option<String>,
}

impl DeleteOrderParamsBuilder {
    /// Build the parameters with validation.
    ///
    /// # Errors
    ///
    /// Returns an error if both order_id and cl_ord_id are provided.
    pub fn build_validated(self) -> Result<DeleteOrderParams, String> {
        let params = self.build().map_err(|e| format!("Failed to build: {e}"))?;

        // Validate that only one of order_id or cl_ord_id is provided
        if params.order_id.is_some() && params.cl_ord_id.is_some() {
            return Err("Cannot provide both order_id and cl_ord_id - use only one".to_string());
        }

        // Validate that at least one is provided
        if params.order_id.is_none() && params.cl_ord_id.is_none() {
            return Err("Must provide either order_id or cl_ord_id".to_string());
        }

        Ok(params)
    }
}

/// Parameters for the DELETE /order/all endpoint.
///
/// # References
///
/// <https://www.bitmex.com/api/explorer/#!/Order/Order_cancelAll>
#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
#[builder(default)]
#[builder(setter(into, strip_option))]
#[serde(rename_all = "camelCase")]
pub struct DeleteAllOrdersParams {
    /// Optional symbol. If provided, only cancels orders for that symbol.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub symbol: Option<String>,
    /// Optional filter for cancellation. Send JSON key/value pairs, such as `{"side": "Buy"}`.
    #[serde(
        skip_serializing_if = "Option::is_none",
        serialize_with = "serialize_json_as_string"
    )]
    pub filter: Option<Value>,
    /// Optional cancellation annotation. e.g. 'Spread Exceeded'.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub text: Option<String>,
}

/// Parameters for the PUT /order endpoint.
#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
#[builder(default)]
#[builder(setter(into, strip_option))]
#[serde(rename_all = "camelCase")]
pub struct PutOrderParams {
    /// Order ID
    #[serde(rename = "orderID")]
    pub order_id: Option<String>,
    /// Client Order ID. See POST /order.
    #[serde(rename = "origClOrdID")]
    pub orig_cl_ord_id: Option<String>,
    /// Optional new Client Order ID, requires `origClOrdID`.
    #[serde(rename = "clOrdID")]
    pub cl_ord_id: Option<String>,
    /// Optional order quantity in units of the instrument (i.e. contracts).
    pub order_qty: Option<u32>,
    /// Optional leaves quantity in units of the instrument (i.e. contracts). Useful for amending partially filled orders.
    pub leaves_qty: Option<u32>,
    /// Optional limit price for `Limit`, `StopLimit`, and `LimitIfTouched` orders.
    pub price: Option<f64>,
    /// 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.
    pub stop_px: Option<f64>,
    /// 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.
    pub peg_offset_value: Option<f64>,
    /// Optional amend annotation. e.g. 'Adjust skew'.
    pub text: Option<String>,
}

/// Parameters for the GET /execution/tradeHistory endpoint.
#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
#[builder(default)]
#[builder(setter(into, strip_option))]
#[serde(rename_all = "camelCase")]
pub struct GetExecutionParams {
    /// 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`.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub symbol: Option<String>,
    /// 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.
    #[serde(
        skip_serializing_if = "Option::is_none",
        serialize_with = "serialize_json_as_string"
    )]
    pub filter: Option<Value>,
    /// 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.
    #[serde(
        skip_serializing_if = "Option::is_none",
        serialize_with = "serialize_json_as_string"
    )]
    pub columns: Option<Value>,
    /// Number of results to fetch.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub count: Option<i32>,
    /// Starting point for results.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub start: Option<i32>,
    /// If true, will sort results newest first.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub reverse: Option<bool>,
    /// Starting date filter for results.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub start_time: Option<DateTime<Utc>>,
    /// Ending date filter for results.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub end_time: Option<DateTime<Utc>>,
}

/// Parameters for the POST /position/leverage endpoint.
#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
#[builder(default)]
#[builder(setter(into, strip_option))]
#[serde(rename_all = "camelCase")]
pub struct PostPositionLeverageParams {
    /// Symbol to set leverage for.
    pub symbol: String,
    /// Leverage value (0.01 to 100).
    pub leverage: f64,
    /// Optional leverage for long position (isolated margin only).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub target_account_id: Option<i64>,
}

/// Parameters for the POST /order/cancelAllAfter endpoint (dead man's switch).
///
/// # References
///
/// <https://www.bitmex.com/api/explorer/#!/Order/Order_cancelAllAfter>
#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
#[builder(default)]
#[builder(setter(into, strip_option))]
pub struct PostCancelAllAfterParams {
    /// Timeout in milliseconds. Setting to 0 disarms the dead man's switch.
    pub timeout: u64,
}

/// Parameters for the GET /position endpoint.
#[derive(Clone, Debug, Deserialize, Serialize, Default, Builder)]
#[builder(default)]
#[builder(setter(into, strip_option))]
#[serde(rename_all = "camelCase")]
pub struct GetPositionParams {
    /// 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.
    #[serde(
        skip_serializing_if = "Option::is_none",
        serialize_with = "serialize_json_as_string"
    )]
    pub filter: Option<Value>,
    /// 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.
    #[serde(
        skip_serializing_if = "Option::is_none",
        serialize_with = "serialize_json_as_string"
    )]
    pub columns: Option<Value>,
    /// Number of results to fetch.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub count: Option<i32>,
}

#[cfg(test)]
mod tests {
    use rstest::rstest;

    use super::*;

    #[rstest]
    fn test_cancel_all_after_params_serializes() {
        let params = PostCancelAllAfterParams { timeout: 60_000 };
        let encoded = serde_urlencoded::to_string(&params).unwrap();
        assert_eq!(encoded, "timeout=60000");
    }

    #[rstest]
    fn test_cancel_all_after_params_disarm_serializes() {
        let params = PostCancelAllAfterParams { timeout: 0 };
        let encoded = serde_urlencoded::to_string(&params).unwrap();
        assert_eq!(encoded, "timeout=0");
    }
}