nautilus_dydx/python/
http.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//! Python bindings for dYdX HTTP client.
17
18#![allow(clippy::missing_errors_doc)]
19
20use std::str::FromStr;
21
22use chrono::DateTime;
23use nautilus_core::python::{IntoPyObjectNautilusExt, to_pyvalue_err};
24use nautilus_model::{
25    data::BarType,
26    identifiers::{AccountId, InstrumentId},
27    instruments::InstrumentAny,
28    python::instruments::{instrument_any_to_pyobject, pyobject_to_instrument_any},
29};
30use pyo3::{
31    prelude::*,
32    types::{PyDict, PyList},
33};
34use rust_decimal::Decimal;
35use ustr::Ustr;
36
37use crate::{common::enums::DydxCandleResolution, http::client::DydxHttpClient};
38
39#[pymethods]
40impl DydxHttpClient {
41    #[new]
42    #[pyo3(signature = (base_url=None, is_testnet=false))]
43    fn py_new(base_url: Option<String>, is_testnet: bool) -> PyResult<Self> {
44        // Mirror the Rust client's constructor signature with sensible defaults
45        Self::new(
46            base_url, None, // timeout_secs
47            None, // proxy_url
48            is_testnet, None, // retry_config
49        )
50        .map_err(to_pyvalue_err)
51    }
52
53    #[pyo3(name = "is_testnet")]
54    fn py_is_testnet(&self) -> bool {
55        self.is_testnet()
56    }
57
58    #[pyo3(name = "base_url")]
59    fn py_base_url(&self) -> String {
60        self.base_url().to_string()
61    }
62
63    #[pyo3(name = "request_instruments")]
64    fn py_request_instruments<'py>(
65        &self,
66        py: Python<'py>,
67        maker_fee: Option<String>,
68        taker_fee: Option<String>,
69    ) -> PyResult<Bound<'py, PyAny>> {
70        let maker = maker_fee
71            .as_ref()
72            .map(|s| Decimal::from_str(s))
73            .transpose()
74            .map_err(to_pyvalue_err)?;
75
76        let taker = taker_fee
77            .as_ref()
78            .map(|s| Decimal::from_str(s))
79            .transpose()
80            .map_err(to_pyvalue_err)?;
81
82        let client = self.clone();
83
84        pyo3_async_runtimes::tokio::future_into_py(py, async move {
85            let instruments = client
86                .request_instruments(None, maker, taker)
87                .await
88                .map_err(to_pyvalue_err)?;
89
90            #[allow(deprecated)]
91            Python::with_gil(|py| {
92                let py_instruments: PyResult<Vec<Py<PyAny>>> = instruments
93                    .into_iter()
94                    .map(|inst| instrument_any_to_pyobject(py, inst))
95                    .collect();
96                py_instruments
97            })
98        })
99    }
100
101    #[pyo3(name = "fetch_and_cache_instruments")]
102    fn py_fetch_and_cache_instruments<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
103        let client = self.clone();
104        pyo3_async_runtimes::tokio::future_into_py(py, async move {
105            client
106                .fetch_and_cache_instruments()
107                .await
108                .map_err(to_pyvalue_err)?;
109            Ok(())
110        })
111    }
112
113    #[pyo3(name = "get_instrument")]
114    fn py_get_instrument(&self, py: Python<'_>, symbol: &str) -> PyResult<Option<Py<PyAny>>> {
115        let symbol_ustr = Ustr::from(symbol);
116        let instrument = self.get_instrument(&symbol_ustr);
117        match instrument {
118            Some(inst) => Ok(Some(instrument_any_to_pyobject(py, inst)?)),
119            None => Ok(None),
120        }
121    }
122
123    #[pyo3(name = "instrument_count")]
124    fn py_instrument_count(&self) -> usize {
125        self.instruments().len()
126    }
127
128    #[pyo3(name = "instrument_symbols")]
129    fn py_instrument_symbols(&self) -> Vec<String> {
130        self.instruments()
131            .iter()
132            .map(|entry| entry.key().to_string())
133            .collect()
134    }
135
136    #[pyo3(name = "cache_instruments")]
137    fn py_cache_instruments(
138        &self,
139        py: Python<'_>,
140        py_instruments: Vec<Bound<'_, PyAny>>,
141    ) -> PyResult<()> {
142        let instruments: Vec<InstrumentAny> = py_instruments
143            .into_iter()
144            .map(|py_inst| {
145                // Convert Bound<PyAny> to Py<PyAny> using unbind()
146                pyobject_to_instrument_any(py, py_inst.unbind())
147            })
148            .collect::<Result<Vec<_>, _>>()
149            .map_err(to_pyvalue_err)?;
150
151        self.cache_instruments(instruments);
152        Ok(())
153    }
154
155    #[pyo3(name = "get_orders")]
156    #[pyo3(signature = (address, subaccount_number, market=None, limit=None))]
157    fn py_get_orders<'py>(
158        &self,
159        py: Python<'py>,
160        address: String,
161        subaccount_number: u32,
162        market: Option<String>,
163        limit: Option<u32>,
164    ) -> PyResult<Bound<'py, PyAny>> {
165        let client = self.clone();
166        pyo3_async_runtimes::tokio::future_into_py(py, async move {
167            let response = client
168                .inner
169                .get_orders(&address, subaccount_number, market.as_deref(), limit)
170                .await
171                .map_err(to_pyvalue_err)?;
172            serde_json::to_string(&response).map_err(to_pyvalue_err)
173        })
174    }
175
176    #[pyo3(name = "get_fills")]
177    #[pyo3(signature = (address, subaccount_number, market=None, limit=None))]
178    fn py_get_fills<'py>(
179        &self,
180        py: Python<'py>,
181        address: String,
182        subaccount_number: u32,
183        market: Option<String>,
184        limit: Option<u32>,
185    ) -> PyResult<Bound<'py, PyAny>> {
186        let client = self.clone();
187        pyo3_async_runtimes::tokio::future_into_py(py, async move {
188            let response = client
189                .inner
190                .get_fills(&address, subaccount_number, market.as_deref(), limit)
191                .await
192                .map_err(to_pyvalue_err)?;
193            serde_json::to_string(&response).map_err(to_pyvalue_err)
194        })
195    }
196
197    #[pyo3(name = "get_subaccount")]
198    fn py_get_subaccount<'py>(
199        &self,
200        py: Python<'py>,
201        address: String,
202        subaccount_number: u32,
203    ) -> PyResult<Bound<'py, PyAny>> {
204        let client = self.clone();
205        pyo3_async_runtimes::tokio::future_into_py(py, async move {
206            let response = client
207                .inner
208                .get_subaccount(&address, subaccount_number)
209                .await
210                .map_err(to_pyvalue_err)?;
211            serde_json::to_string(&response).map_err(to_pyvalue_err)
212        })
213    }
214
215    #[pyo3(name = "request_order_status_reports")]
216    #[pyo3(signature = (address, subaccount_number, account_id, instrument_id=None))]
217    fn py_request_order_status_reports<'py>(
218        &self,
219        py: Python<'py>,
220        address: String,
221        subaccount_number: u32,
222        account_id: AccountId,
223        instrument_id: Option<InstrumentId>,
224    ) -> PyResult<Bound<'py, PyAny>> {
225        let client = self.clone();
226        pyo3_async_runtimes::tokio::future_into_py(py, async move {
227            let reports = client
228                .request_order_status_reports(
229                    &address,
230                    subaccount_number,
231                    account_id,
232                    instrument_id,
233                )
234                .await
235                .map_err(to_pyvalue_err)?;
236
237            Python::attach(|py| {
238                let pylist =
239                    PyList::new(py, reports.into_iter().map(|r| r.into_py_any_unwrap(py)))?;
240                Ok(pylist.into_py_any_unwrap(py))
241            })
242        })
243    }
244
245    #[pyo3(name = "request_fill_reports")]
246    #[pyo3(signature = (address, subaccount_number, account_id, instrument_id=None))]
247    fn py_request_fill_reports<'py>(
248        &self,
249        py: Python<'py>,
250        address: String,
251        subaccount_number: u32,
252        account_id: AccountId,
253        instrument_id: Option<InstrumentId>,
254    ) -> PyResult<Bound<'py, PyAny>> {
255        let client = self.clone();
256        pyo3_async_runtimes::tokio::future_into_py(py, async move {
257            let reports = client
258                .request_fill_reports(&address, subaccount_number, account_id, instrument_id)
259                .await
260                .map_err(to_pyvalue_err)?;
261
262            Python::attach(|py| {
263                let pylist =
264                    PyList::new(py, reports.into_iter().map(|r| r.into_py_any_unwrap(py)))?;
265                Ok(pylist.into_py_any_unwrap(py))
266            })
267        })
268    }
269
270    #[pyo3(name = "request_position_status_reports")]
271    #[pyo3(signature = (address, subaccount_number, account_id, instrument_id=None))]
272    fn py_request_position_status_reports<'py>(
273        &self,
274        py: Python<'py>,
275        address: String,
276        subaccount_number: u32,
277        account_id: AccountId,
278        instrument_id: Option<InstrumentId>,
279    ) -> PyResult<Bound<'py, PyAny>> {
280        let client = self.clone();
281        pyo3_async_runtimes::tokio::future_into_py(py, async move {
282            let reports = client
283                .request_position_status_reports(
284                    &address,
285                    subaccount_number,
286                    account_id,
287                    instrument_id,
288                )
289                .await
290                .map_err(to_pyvalue_err)?;
291
292            Python::attach(|py| {
293                let pylist =
294                    PyList::new(py, reports.into_iter().map(|r| r.into_py_any_unwrap(py)))?;
295                Ok(pylist.into_py_any_unwrap(py))
296            })
297        })
298    }
299
300    #[pyo3(name = "request_bars")]
301    #[pyo3(signature = (bar_type, resolution, limit=None, start=None, end=None))]
302    fn py_request_bars<'py>(
303        &self,
304        py: Python<'py>,
305        bar_type: String,
306        resolution: String,
307        limit: Option<u32>,
308        start: Option<String>,
309        end: Option<String>,
310    ) -> PyResult<Bound<'py, PyAny>> {
311        let bar_type = BarType::from_str(&bar_type).map_err(to_pyvalue_err)?;
312        let resolution = DydxCandleResolution::from_str(&resolution).map_err(to_pyvalue_err)?;
313
314        let from_iso = start
315            .map(|s| DateTime::parse_from_rfc3339(&s).map(|dt| dt.with_timezone(&chrono::Utc)))
316            .transpose()
317            .map_err(to_pyvalue_err)?;
318
319        let to_iso = end
320            .map(|s| DateTime::parse_from_rfc3339(&s).map(|dt| dt.with_timezone(&chrono::Utc)))
321            .transpose()
322            .map_err(to_pyvalue_err)?;
323
324        let client = self.clone();
325
326        pyo3_async_runtimes::tokio::future_into_py(py, async move {
327            let bars = client
328                .request_bars(bar_type, resolution, limit, from_iso, to_iso)
329                .await
330                .map_err(to_pyvalue_err)?;
331
332            Python::attach(|py| {
333                let pylist = PyList::new(py, bars.into_iter().map(|b| b.into_py_any_unwrap(py)))?;
334                Ok(pylist.into_py_any_unwrap(py))
335            })
336        })
337    }
338
339    #[pyo3(name = "request_trade_ticks")]
340    #[pyo3(signature = (instrument_id, limit=None))]
341    fn py_request_trade_ticks<'py>(
342        &self,
343        py: Python<'py>,
344        instrument_id: InstrumentId,
345        limit: Option<u32>,
346    ) -> PyResult<Bound<'py, PyAny>> {
347        let client = self.clone();
348
349        pyo3_async_runtimes::tokio::future_into_py(py, async move {
350            let trades = client
351                .request_trade_ticks(instrument_id, limit)
352                .await
353                .map_err(to_pyvalue_err)?;
354
355            Python::attach(|py| {
356                let pylist = PyList::new(py, trades.into_iter().map(|t| t.into_py_any_unwrap(py)))?;
357                Ok(pylist.into_py_any_unwrap(py))
358            })
359        })
360    }
361
362    #[pyo3(name = "request_orderbook_snapshot")]
363    fn py_request_orderbook_snapshot<'py>(
364        &self,
365        py: Python<'py>,
366        instrument_id: InstrumentId,
367    ) -> PyResult<Bound<'py, PyAny>> {
368        let client = self.clone();
369
370        pyo3_async_runtimes::tokio::future_into_py(py, async move {
371            let deltas = client
372                .request_orderbook_snapshot(instrument_id)
373                .await
374                .map_err(to_pyvalue_err)?;
375
376            Python::attach(|py| Ok(deltas.into_py_any_unwrap(py)))
377        })
378    }
379
380    #[pyo3(name = "get_time")]
381    fn py_get_time<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
382        let client = self.clone();
383        pyo3_async_runtimes::tokio::future_into_py(py, async move {
384            let response = client.inner.get_time().await.map_err(to_pyvalue_err)?;
385            Python::attach(|py| {
386                let dict = PyDict::new(py);
387                dict.set_item("iso", response.iso.to_string())?;
388                dict.set_item("epoch", response.epoch_ms)?;
389                Ok(dict.into_py_any_unwrap(py))
390            })
391        })
392    }
393
394    #[pyo3(name = "get_height")]
395    fn py_get_height<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
396        let client = self.clone();
397        pyo3_async_runtimes::tokio::future_into_py(py, async move {
398            let response = client.inner.get_height().await.map_err(to_pyvalue_err)?;
399            Python::attach(|py| {
400                let dict = PyDict::new(py);
401                dict.set_item("height", response.height)?;
402                dict.set_item("time", response.time)?;
403                Ok(dict.into_py_any_unwrap(py))
404            })
405        })
406    }
407
408    #[pyo3(name = "get_transfers")]
409    #[pyo3(signature = (address, subaccount_number, limit=None))]
410    fn py_get_transfers<'py>(
411        &self,
412        py: Python<'py>,
413        address: String,
414        subaccount_number: u32,
415        limit: Option<u32>,
416    ) -> PyResult<Bound<'py, PyAny>> {
417        let client = self.clone();
418        pyo3_async_runtimes::tokio::future_into_py(py, async move {
419            let response = client
420                .inner
421                .get_transfers(&address, subaccount_number, limit)
422                .await
423                .map_err(to_pyvalue_err)?;
424            serde_json::to_string(&response).map_err(to_pyvalue_err)
425        })
426    }
427
428    fn __repr__(&self) -> String {
429        format!(
430            "DydxHttpClient(base_url='{}', is_testnet={}, cached_instruments={})",
431            self.base_url(),
432            self.is_testnet(),
433            self.instruments().len()
434        )
435    }
436}