nautilus_bitmex/python/
http.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//! Python bindings for the BitMEX HTTP client.
17
18use chrono::{DateTime, Utc};
19use nautilus_core::python::to_pyvalue_err;
20use nautilus_model::{
21    data::BarType,
22    enums::{ContingencyType, OrderSide, OrderType, TimeInForce, TriggerType},
23    identifiers::{AccountId, ClientOrderId, InstrumentId, OrderListId, VenueOrderId},
24    python::instruments::{instrument_any_to_pyobject, pyobject_to_instrument_any},
25    types::{Price, Quantity},
26};
27use pyo3::{conversion::IntoPyObjectExt, prelude::*, types::PyList};
28
29use crate::http::client::BitmexHttpClient;
30
31#[pymethods]
32impl BitmexHttpClient {
33    #[new]
34    #[pyo3(signature = (api_key=None, api_secret=None, base_url=None, testnet=false, timeout_secs=None, max_retries=None, retry_delay_ms=None, retry_delay_max_ms=None, recv_window_ms=None, max_requests_per_second=None, max_requests_per_minute=None))]
35    #[allow(clippy::too_many_arguments)]
36    fn py_new(
37        api_key: Option<&str>,
38        api_secret: Option<&str>,
39        base_url: Option<&str>,
40        testnet: bool,
41        timeout_secs: Option<u64>,
42        max_retries: Option<u32>,
43        retry_delay_ms: Option<u64>,
44        retry_delay_max_ms: Option<u64>,
45        recv_window_ms: Option<u64>,
46        max_requests_per_second: Option<u32>,
47        max_requests_per_minute: Option<u32>,
48    ) -> PyResult<Self> {
49        let timeout = timeout_secs.or(Some(60));
50
51        // If credentials not provided, try to load from environment
52        let (final_api_key, final_api_secret) = if api_key.is_none() && api_secret.is_none() {
53            // Choose environment variables based on testnet flag
54            let (key_var, secret_var) = if testnet {
55                ("BITMEX_TESTNET_API_KEY", "BITMEX_TESTNET_API_SECRET")
56            } else {
57                ("BITMEX_API_KEY", "BITMEX_API_SECRET")
58            };
59
60            let env_key = std::env::var(key_var).ok();
61            let env_secret = std::env::var(secret_var).ok();
62            (env_key, env_secret)
63        } else {
64            (api_key.map(String::from), api_secret.map(String::from))
65        };
66
67        Self::new(
68            base_url.map(String::from),
69            final_api_key,
70            final_api_secret,
71            testnet,
72            timeout,
73            max_retries,
74            retry_delay_ms,
75            retry_delay_max_ms,
76            recv_window_ms,
77            max_requests_per_second,
78            max_requests_per_minute,
79        )
80        .map_err(to_pyvalue_err)
81    }
82
83    #[staticmethod]
84    #[pyo3(name = "from_env")]
85    fn py_from_env() -> PyResult<Self> {
86        Self::from_env().map_err(to_pyvalue_err)
87    }
88
89    #[getter]
90    #[pyo3(name = "base_url")]
91    #[must_use]
92    pub fn py_base_url(&self) -> &str {
93        self.base_url()
94    }
95
96    #[getter]
97    #[pyo3(name = "api_key")]
98    #[must_use]
99    pub fn py_api_key(&self) -> Option<&str> {
100        self.api_key()
101    }
102
103    #[pyo3(name = "update_position_leverage")]
104    fn py_update_position_leverage<'py>(
105        &self,
106        py: Python<'py>,
107        _symbol: String,
108        _leverage: f64,
109    ) -> PyResult<Bound<'py, PyAny>> {
110        let _client = self.clone();
111
112        pyo3_async_runtimes::tokio::future_into_py(py, async move {
113            // Call the leverage update method once it's implemented
114            // let report = client.update_position_leverage(&symbol, leverage)
115            //     .await
116            //     .map_err(to_pyvalue_err)?;
117
118            Python::attach(|py| -> PyResult<Py<PyAny>> {
119                // report.into_py_any(py).map_err(to_pyvalue_err)
120                Ok(py.None())
121            })
122        })
123    }
124
125    #[pyo3(name = "request_instrument")]
126    fn py_request_instrument<'py>(
127        &self,
128        py: Python<'py>,
129        instrument_id: InstrumentId,
130    ) -> PyResult<Bound<'py, PyAny>> {
131        let client = self.clone();
132
133        pyo3_async_runtimes::tokio::future_into_py(py, async move {
134            let instrument = client
135                .request_instrument(instrument_id)
136                .await
137                .map_err(to_pyvalue_err)?;
138
139            Python::attach(|py| match instrument {
140                Some(inst) => instrument_any_to_pyobject(py, inst),
141                None => Ok(py.None()),
142            })
143        })
144    }
145
146    #[pyo3(name = "request_instruments")]
147    fn py_request_instruments<'py>(
148        &self,
149        py: Python<'py>,
150        active_only: bool,
151    ) -> PyResult<Bound<'py, PyAny>> {
152        let client = self.clone();
153
154        pyo3_async_runtimes::tokio::future_into_py(py, async move {
155            let instruments = client
156                .request_instruments(active_only)
157                .await
158                .map_err(to_pyvalue_err)?;
159
160            Python::attach(|py| {
161                let py_instruments: PyResult<Vec<_>> = instruments
162                    .into_iter()
163                    .map(|inst| instrument_any_to_pyobject(py, inst))
164                    .collect();
165                let pylist = PyList::new(py, py_instruments?)
166                    .unwrap()
167                    .into_any()
168                    .unbind();
169                Ok(pylist)
170            })
171        })
172    }
173
174    #[pyo3(name = "request_trades")]
175    #[pyo3(signature = (instrument_id, start=None, end=None, limit=None))]
176    fn py_request_trades<'py>(
177        &self,
178        py: Python<'py>,
179        instrument_id: InstrumentId,
180        start: Option<DateTime<Utc>>,
181        end: Option<DateTime<Utc>>,
182        limit: Option<u32>,
183    ) -> PyResult<Bound<'py, PyAny>> {
184        let client = self.clone();
185
186        pyo3_async_runtimes::tokio::future_into_py(py, async move {
187            let trades = client
188                .request_trades(instrument_id, start, end, limit)
189                .await
190                .map_err(to_pyvalue_err)?;
191
192            Python::attach(|py| {
193                let py_trades: PyResult<Vec<_>> = trades
194                    .into_iter()
195                    .map(|trade| trade.into_py_any(py))
196                    .collect();
197                let pylist = PyList::new(py, py_trades?).unwrap().into_any().unbind();
198                Ok(pylist)
199            })
200        })
201    }
202
203    #[pyo3(name = "request_bars")]
204    #[pyo3(signature = (bar_type, start=None, end=None, limit=None, partial=false))]
205    fn py_request_bars<'py>(
206        &self,
207        py: Python<'py>,
208        bar_type: BarType,
209        start: Option<DateTime<Utc>>,
210        end: Option<DateTime<Utc>>,
211        limit: Option<u32>,
212        partial: bool,
213    ) -> PyResult<Bound<'py, PyAny>> {
214        let client = self.clone();
215
216        pyo3_async_runtimes::tokio::future_into_py(py, async move {
217            let bars = client
218                .request_bars(bar_type, start, end, limit, partial)
219                .await
220                .map_err(to_pyvalue_err)?;
221
222            Python::attach(|py| {
223                let py_bars: PyResult<Vec<_>> =
224                    bars.into_iter().map(|bar| bar.into_py_any(py)).collect();
225                let pylist = PyList::new(py, py_bars?).unwrap().into_any().unbind();
226                Ok(pylist)
227            })
228        })
229    }
230
231    #[pyo3(name = "query_order")]
232    #[pyo3(signature = (instrument_id, client_order_id=None, venue_order_id=None))]
233    fn py_query_order<'py>(
234        &self,
235        py: Python<'py>,
236        instrument_id: InstrumentId,
237        client_order_id: Option<ClientOrderId>,
238        venue_order_id: Option<VenueOrderId>,
239    ) -> PyResult<Bound<'py, PyAny>> {
240        let client = self.clone();
241
242        pyo3_async_runtimes::tokio::future_into_py(py, async move {
243            match client
244                .query_order(instrument_id, client_order_id, venue_order_id)
245                .await
246            {
247                Ok(Some(report)) => Python::attach(|py| report.into_py_any(py)),
248                Ok(None) => Ok(Python::attach(|py| py.None())),
249                Err(e) => Err(to_pyvalue_err(e)),
250            }
251        })
252    }
253
254    #[pyo3(name = "request_order_status_reports")]
255    #[pyo3(signature = (instrument_id=None, open_only=false, limit=None))]
256    fn py_request_order_status_reports<'py>(
257        &self,
258        py: Python<'py>,
259        instrument_id: Option<InstrumentId>,
260        open_only: bool,
261        limit: Option<u32>,
262    ) -> PyResult<Bound<'py, PyAny>> {
263        let client = self.clone();
264
265        pyo3_async_runtimes::tokio::future_into_py(py, async move {
266            let reports = client
267                .request_order_status_reports(instrument_id, open_only, limit)
268                .await
269                .map_err(to_pyvalue_err)?;
270
271            Python::attach(|py| {
272                let py_reports: PyResult<Vec<_>> = reports
273                    .into_iter()
274                    .map(|report| report.into_py_any(py))
275                    .collect();
276                let pylist = PyList::new(py, py_reports?).unwrap().into_any().unbind();
277                Ok(pylist)
278            })
279        })
280    }
281
282    #[pyo3(name = "request_fill_reports")]
283    #[pyo3(signature = (instrument_id=None, limit=None))]
284    fn py_request_fill_reports<'py>(
285        &self,
286        py: Python<'py>,
287        instrument_id: Option<InstrumentId>,
288        limit: Option<u32>,
289    ) -> PyResult<Bound<'py, PyAny>> {
290        let client = self.clone();
291
292        pyo3_async_runtimes::tokio::future_into_py(py, async move {
293            let reports = client
294                .request_fill_reports(instrument_id, limit)
295                .await
296                .map_err(to_pyvalue_err)?;
297
298            Python::attach(|py| {
299                let py_reports: PyResult<Vec<_>> = reports
300                    .into_iter()
301                    .map(|report| report.into_py_any(py))
302                    .collect();
303                let pylist = PyList::new(py, py_reports?).unwrap().into_any().unbind();
304                Ok(pylist)
305            })
306        })
307    }
308
309    #[pyo3(name = "request_position_status_reports")]
310    fn py_request_position_status_reports<'py>(
311        &self,
312        py: Python<'py>,
313    ) -> PyResult<Bound<'py, PyAny>> {
314        let client = self.clone();
315
316        pyo3_async_runtimes::tokio::future_into_py(py, async move {
317            let reports = client
318                .request_position_status_reports()
319                .await
320                .map_err(to_pyvalue_err)?;
321
322            Python::attach(|py| {
323                let py_reports: PyResult<Vec<_>> = reports
324                    .into_iter()
325                    .map(|report| report.into_py_any(py))
326                    .collect();
327                let pylist = PyList::new(py, py_reports?).unwrap().into_any().unbind();
328                Ok(pylist)
329            })
330        })
331    }
332
333    #[pyo3(name = "submit_order")]
334    #[pyo3(signature = (
335        instrument_id,
336        client_order_id,
337        order_side,
338        order_type,
339        quantity,
340        time_in_force,
341        price = None,
342        trigger_price = None,
343        trigger_type = None,
344        display_qty = None,
345        post_only = false,
346        reduce_only = false,
347        order_list_id = None,
348        contingency_type = None
349    ))]
350    #[allow(clippy::too_many_arguments)]
351    fn py_submit_order<'py>(
352        &self,
353        py: Python<'py>,
354        instrument_id: InstrumentId,
355        client_order_id: ClientOrderId,
356        order_side: OrderSide,
357        order_type: OrderType,
358        quantity: Quantity,
359        time_in_force: TimeInForce,
360        price: Option<Price>,
361        trigger_price: Option<Price>,
362        trigger_type: Option<TriggerType>,
363        display_qty: Option<Quantity>,
364        post_only: bool,
365        reduce_only: bool,
366        order_list_id: Option<OrderListId>,
367        contingency_type: Option<ContingencyType>,
368    ) -> PyResult<Bound<'py, PyAny>> {
369        let client = self.clone();
370
371        pyo3_async_runtimes::tokio::future_into_py(py, async move {
372            let report = client
373                .submit_order(
374                    instrument_id,
375                    client_order_id,
376                    order_side,
377                    order_type,
378                    quantity,
379                    time_in_force,
380                    price,
381                    trigger_price,
382                    trigger_type,
383                    display_qty,
384                    post_only,
385                    reduce_only,
386                    order_list_id,
387                    contingency_type,
388                )
389                .await
390                .map_err(to_pyvalue_err)?;
391
392            Python::attach(|py| report.into_py_any(py))
393        })
394    }
395
396    #[pyo3(name = "cancel_order")]
397    #[pyo3(signature = (instrument_id, client_order_id=None, venue_order_id=None))]
398    fn py_cancel_order<'py>(
399        &self,
400        py: Python<'py>,
401        instrument_id: InstrumentId,
402        client_order_id: Option<ClientOrderId>,
403        venue_order_id: Option<VenueOrderId>,
404    ) -> PyResult<Bound<'py, PyAny>> {
405        let client = self.clone();
406
407        pyo3_async_runtimes::tokio::future_into_py(py, async move {
408            let report = client
409                .cancel_order(instrument_id, client_order_id, venue_order_id)
410                .await
411                .map_err(to_pyvalue_err)?;
412
413            Python::attach(|py| report.into_py_any(py))
414        })
415    }
416
417    #[pyo3(name = "cancel_orders")]
418    #[pyo3(signature = (instrument_id, client_order_ids=None, venue_order_ids=None))]
419    fn py_cancel_orders<'py>(
420        &self,
421        py: Python<'py>,
422        instrument_id: InstrumentId,
423        client_order_ids: Option<Vec<ClientOrderId>>,
424        venue_order_ids: Option<Vec<VenueOrderId>>,
425    ) -> PyResult<Bound<'py, PyAny>> {
426        let client = self.clone();
427
428        pyo3_async_runtimes::tokio::future_into_py(py, async move {
429            let reports = client
430                .cancel_orders(instrument_id, client_order_ids, venue_order_ids)
431                .await
432                .map_err(to_pyvalue_err)?;
433
434            Python::attach(|py| {
435                let py_reports: PyResult<Vec<_>> = reports
436                    .into_iter()
437                    .map(|report| report.into_py_any(py))
438                    .collect();
439                let pylist = PyList::new(py, py_reports?).unwrap().into_any().unbind();
440                Ok(pylist)
441            })
442        })
443    }
444
445    #[pyo3(name = "cancel_all_orders")]
446    #[pyo3(signature = (instrument_id, order_side))]
447    fn py_cancel_all_orders<'py>(
448        &self,
449        py: Python<'py>,
450        instrument_id: InstrumentId,
451        order_side: Option<OrderSide>,
452    ) -> PyResult<Bound<'py, PyAny>> {
453        let client = self.clone();
454
455        pyo3_async_runtimes::tokio::future_into_py(py, async move {
456            let reports = client
457                .cancel_all_orders(instrument_id, order_side)
458                .await
459                .map_err(to_pyvalue_err)?;
460
461            Python::attach(|py| {
462                let py_reports: PyResult<Vec<_>> = reports
463                    .into_iter()
464                    .map(|report| report.into_py_any(py))
465                    .collect();
466                let pylist = PyList::new(py, py_reports?).unwrap().into_any().unbind();
467                Ok(pylist)
468            })
469        })
470    }
471
472    #[pyo3(name = "modify_order")]
473    #[pyo3(signature = (
474        instrument_id,
475        client_order_id=None,
476        venue_order_id=None,
477        quantity=None,
478        price=None,
479        trigger_price=None
480    ))]
481    #[allow(clippy::too_many_arguments)]
482    fn py_modify_order<'py>(
483        &self,
484        py: Python<'py>,
485        instrument_id: InstrumentId,
486        client_order_id: Option<ClientOrderId>,
487        venue_order_id: Option<VenueOrderId>,
488        quantity: Option<Quantity>,
489        price: Option<Price>,
490        trigger_price: Option<Price>,
491    ) -> PyResult<Bound<'py, PyAny>> {
492        let client = self.clone();
493
494        pyo3_async_runtimes::tokio::future_into_py(py, async move {
495            let report = client
496                .modify_order(
497                    instrument_id,
498                    client_order_id,
499                    venue_order_id,
500                    quantity,
501                    price,
502                    trigger_price,
503                )
504                .await
505                .map_err(to_pyvalue_err)?;
506
507            Python::attach(|py| report.into_py_any(py))
508        })
509    }
510
511    #[pyo3(name = "add_instrument")]
512    fn py_add_instrument(&mut self, py: Python, instrument: Py<PyAny>) -> PyResult<()> {
513        let inst_any = pyobject_to_instrument_any(py, instrument)?;
514        self.add_instrument(inst_any);
515        Ok(())
516    }
517
518    #[pyo3(name = "cancel_all_requests")]
519    fn py_cancel_all_requests(&self) {
520        self.cancel_all_requests();
521    }
522
523    #[pyo3(name = "http_get_margin")]
524    fn py_http_get_margin<'py>(
525        &self,
526        py: Python<'py>,
527        currency: String,
528    ) -> PyResult<Bound<'py, PyAny>> {
529        let client = self.clone();
530
531        pyo3_async_runtimes::tokio::future_into_py(py, async move {
532            let margin = client
533                .http_get_margin(&currency)
534                .await
535                .map_err(to_pyvalue_err)?;
536
537            Python::attach(|py| {
538                // Create a simple Python object with just the account field we need
539                // We can expand this if more fields are needed
540                let account = margin.account;
541                account.into_py_any(py)
542            })
543        })
544    }
545
546    #[pyo3(name = "request_account_state")]
547    fn py_request_account_state<'py>(
548        &self,
549        py: Python<'py>,
550        account_id: AccountId,
551    ) -> PyResult<Bound<'py, PyAny>> {
552        let client = self.clone();
553
554        pyo3_async_runtimes::tokio::future_into_py(py, async move {
555            let account_state = client
556                .request_account_state(account_id)
557                .await
558                .map_err(to_pyvalue_err)?;
559
560            Python::attach(|py| account_state.into_py_any(py).map_err(to_pyvalue_err))
561        })
562    }
563
564    #[pyo3(name = "submit_orders_bulk")]
565    fn py_submit_orders_bulk<'py>(
566        &self,
567        py: Python<'py>,
568        orders: Vec<Py<PyAny>>,
569    ) -> PyResult<Bound<'py, PyAny>> {
570        let _client = self.clone();
571
572        // Convert Python objects to PostOrderParams
573        let _params = Python::attach(|_py| {
574            orders
575                .into_iter()
576                .map(|obj| {
577                    // Extract order parameters from Python dict
578                    // This is a placeholder - actual implementation would need proper conversion
579                    Ok(obj)
580                })
581                .collect::<PyResult<Vec<_>>>()
582        })?;
583
584        pyo3_async_runtimes::tokio::future_into_py(py, async move {
585            // Call the bulk order method once it's implemented
586            // let reports = client.submit_orders_bulk(params).await.map_err(to_pyvalue_err)?;
587
588            Python::attach(|py| -> PyResult<Py<PyAny>> {
589                let py_list = PyList::new(py, Vec::<Py<PyAny>>::new())?;
590                // for report in reports {
591                //     py_list.append(report.into_py_any(py)?)?;
592                // }
593                Ok(py_list.into())
594            })
595        })
596    }
597
598    #[pyo3(name = "modify_orders_bulk")]
599    fn py_modify_orders_bulk<'py>(
600        &self,
601        py: Python<'py>,
602        orders: Vec<Py<PyAny>>,
603    ) -> PyResult<Bound<'py, PyAny>> {
604        let _client = self.clone();
605
606        // Convert Python objects to PutOrderParams
607        let _params = Python::attach(|_py| {
608            orders
609                .into_iter()
610                .map(|obj| {
611                    // Extract order parameters from Python dict
612                    // This is a placeholder - actual implementation would need proper conversion
613                    Ok(obj)
614                })
615                .collect::<PyResult<Vec<_>>>()
616        })?;
617
618        pyo3_async_runtimes::tokio::future_into_py(py, async move {
619            // Call the bulk amend method once it's implemented
620            // let reports = client.modify_orders_bulk(params).await.map_err(to_pyvalue_err)?;
621
622            Python::attach(|py| -> PyResult<Py<PyAny>> {
623                let py_list = PyList::new(py, Vec::<Py<PyAny>>::new())?;
624                // for report in reports {
625                //     py_list.append(report.into_py_any(py)?)?;
626                // }
627                Ok(py_list.into())
628            })
629        })
630    }
631
632    #[pyo3(name = "http_get_server_time")]
633    fn py_http_get_server_time<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
634        let client = self.clone();
635
636        pyo3_async_runtimes::tokio::future_into_py(py, async move {
637            let timestamp = client
638                .http_get_server_time()
639                .await
640                .map_err(to_pyvalue_err)?;
641
642            Python::attach(|py| timestamp.into_py_any(py))
643        })
644    }
645}