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_bars")]
324    #[pyo3(signature = (bar_type, start=None, end=None, limit=None, timestamp_on_close=true))]
325    fn py_request_bars<'py>(
326        &self,
327        py: Python<'py>,
328        bar_type: BarType,
329        start: Option<DateTime<Utc>>,
330        end: Option<DateTime<Utc>>,
331        limit: Option<u32>,
332        timestamp_on_close: bool,
333    ) -> PyResult<Bound<'py, PyAny>> {
334        let client = self.clone();
335
336        pyo3_async_runtimes::tokio::future_into_py(py, async move {
337            let bars = client
338                .request_bars(bar_type, start, end, limit, timestamp_on_close)
339                .await
340                .map_err(to_pyvalue_err)?;
341
342            Python::attach(|py| {
343                let pylist = PyList::new(py, bars.into_iter().map(|b| b.into_py_any_unwrap(py)))?;
344                Ok(pylist.into_py_any_unwrap(py))
345            })
346        })
347    }
348
349    #[pyo3(name = "request_trade_ticks")]
350    #[pyo3(signature = (instrument_id, start=None, end=None, limit=None))]
351    fn py_request_trade_ticks<'py>(
352        &self,
353        py: Python<'py>,
354        instrument_id: InstrumentId,
355        start: Option<DateTime<Utc>>,
356        end: Option<DateTime<Utc>>,
357        limit: Option<u32>,
358    ) -> PyResult<Bound<'py, PyAny>> {
359        let client = self.clone();
360
361        pyo3_async_runtimes::tokio::future_into_py(py, async move {
362            let trades = client
363                .request_trade_ticks(instrument_id, start, end, limit)
364                .await
365                .map_err(to_pyvalue_err)?;
366
367            Python::attach(|py| {
368                let pylist = PyList::new(py, trades.into_iter().map(|t| t.into_py_any_unwrap(py)))?;
369                Ok(pylist.into_py_any_unwrap(py))
370            })
371        })
372    }
373
374    #[pyo3(name = "request_orderbook_snapshot")]
375    fn py_request_orderbook_snapshot<'py>(
376        &self,
377        py: Python<'py>,
378        instrument_id: InstrumentId,
379    ) -> PyResult<Bound<'py, PyAny>> {
380        let client = self.clone();
381
382        pyo3_async_runtimes::tokio::future_into_py(py, async move {
383            let deltas = client
384                .request_orderbook_snapshot(instrument_id)
385                .await
386                .map_err(to_pyvalue_err)?;
387
388            Python::attach(|py| Ok(deltas.into_py_any_unwrap(py)))
389        })
390    }
391
392    #[pyo3(name = "get_time")]
393    fn py_get_time<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
394        let client = self.clone();
395        pyo3_async_runtimes::tokio::future_into_py(py, async move {
396            let response = client.inner.get_time().await.map_err(to_pyvalue_err)?;
397            Python::attach(|py| {
398                let dict = PyDict::new(py);
399                dict.set_item("iso", response.iso.to_string())?;
400                dict.set_item("epoch", response.epoch_ms)?;
401                Ok(dict.into_py_any_unwrap(py))
402            })
403        })
404    }
405
406    #[pyo3(name = "get_height")]
407    fn py_get_height<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
408        let client = self.clone();
409        pyo3_async_runtimes::tokio::future_into_py(py, async move {
410            let response = client.inner.get_height().await.map_err(to_pyvalue_err)?;
411            Python::attach(|py| {
412                let dict = PyDict::new(py);
413                dict.set_item("height", response.height)?;
414                dict.set_item("time", response.time)?;
415                Ok(dict.into_py_any_unwrap(py))
416            })
417        })
418    }
419
420    #[pyo3(name = "get_transfers")]
421    #[pyo3(signature = (address, subaccount_number, limit=None))]
422    fn py_get_transfers<'py>(
423        &self,
424        py: Python<'py>,
425        address: String,
426        subaccount_number: u32,
427        limit: Option<u32>,
428    ) -> PyResult<Bound<'py, PyAny>> {
429        let client = self.clone();
430        pyo3_async_runtimes::tokio::future_into_py(py, async move {
431            let response = client
432                .inner
433                .get_transfers(&address, subaccount_number, limit)
434                .await
435                .map_err(to_pyvalue_err)?;
436            serde_json::to_string(&response).map_err(to_pyvalue_err)
437        })
438    }
439
440    fn __repr__(&self) -> String {
441        format!(
442            "DydxHttpClient(base_url='{}', is_testnet={}, cached_instruments={})",
443            self.base_url(),
444            self.is_testnet(),
445            self.cached_instruments_count()
446        )
447    }
448}