Skip to main content

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