nautilus_dydx/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 dYdX HTTP client.
17
18use std::str::FromStr;
19
20use nautilus_core::python::{IntoPyObjectNautilusExt, to_pyvalue_err};
21use nautilus_model::{
22    identifiers::{AccountId, InstrumentId},
23    python::instruments::instrument_any_to_pyobject,
24};
25use pyo3::{prelude::*, types::PyList};
26use rust_decimal::Decimal;
27use ustr::Ustr;
28
29use crate::http::client::DydxHttpClient;
30
31#[pymethods]
32impl DydxHttpClient {
33    /// Creates a new [`DydxHttpClient`] instance.
34    #[new]
35    #[pyo3(signature = (base_url=None, is_testnet=false))]
36    fn py_new(base_url: Option<String>, is_testnet: bool) -> PyResult<Self> {
37        // Mirror the Rust client's constructor signature with sensible defaults
38        Self::new(
39            base_url, None, // timeout_secs
40            None, // proxy_url
41            is_testnet, None, // retry_config
42        )
43        .map_err(to_pyvalue_err)
44    }
45
46    /// Returns `true` if the client is configured for testnet.
47    #[pyo3(name = "is_testnet")]
48    fn py_is_testnet(&self) -> bool {
49        self.is_testnet()
50    }
51
52    /// Returns the base URL for the HTTP client.
53    #[pyo3(name = "base_url")]
54    fn py_base_url(&self) -> String {
55        self.base_url().to_string()
56    }
57
58    /// Requests all available instruments from the dYdX Indexer API.
59    #[pyo3(name = "request_instruments")]
60    fn py_request_instruments<'py>(
61        &self,
62        py: Python<'py>,
63        maker_fee: Option<String>,
64        taker_fee: Option<String>,
65    ) -> PyResult<Bound<'py, PyAny>> {
66        let maker = maker_fee
67            .as_ref()
68            .map(|s| Decimal::from_str(s))
69            .transpose()
70            .map_err(to_pyvalue_err)?;
71
72        let taker = taker_fee
73            .as_ref()
74            .map(|s| Decimal::from_str(s))
75            .transpose()
76            .map_err(to_pyvalue_err)?;
77
78        let client = self.clone();
79
80        pyo3_async_runtimes::tokio::future_into_py(py, async move {
81            let instruments = client
82                .request_instruments(None, maker, taker)
83                .await
84                .map_err(to_pyvalue_err)?;
85
86            #[allow(deprecated)]
87            Python::with_gil(|py| {
88                let py_instruments: PyResult<Vec<Py<PyAny>>> = instruments
89                    .into_iter()
90                    .map(|inst| instrument_any_to_pyobject(py, inst))
91                    .collect();
92                py_instruments
93            })
94        })
95    }
96
97    /// Fetches all instruments from the API and caches them along with market params.
98    ///
99    /// This is the preferred method for initializing the HTTP client cache before
100    /// submitting orders, as it caches both instruments and their associated market
101    /// parameters needed for order quantization.
102    #[pyo3(name = "fetch_and_cache_instruments")]
103    fn py_fetch_and_cache_instruments<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
104        let client = self.clone();
105        pyo3_async_runtimes::tokio::future_into_py(py, async move {
106            client
107                .fetch_and_cache_instruments()
108                .await
109                .map_err(to_pyvalue_err)?;
110            Ok(())
111        })
112    }
113
114    /// Gets a cached instrument by symbol.
115    #[pyo3(name = "get_instrument")]
116    fn py_get_instrument(&self, py: Python<'_>, symbol: &str) -> PyResult<Option<Py<PyAny>>> {
117        let symbol_ustr = Ustr::from(symbol);
118        let instrument = self.get_instrument(&symbol_ustr);
119        match instrument {
120            Some(inst) => Ok(Some(instrument_any_to_pyobject(py, inst)?)),
121            None => Ok(None),
122        }
123    }
124
125    /// Returns the number of cached instruments.
126    #[pyo3(name = "instrument_count")]
127    fn py_instrument_count(&self) -> usize {
128        self.instruments().len()
129    }
130
131    /// Returns all cached instrument symbols.
132    #[pyo3(name = "instrument_symbols")]
133    fn py_instrument_symbols(&self) -> Vec<String> {
134        self.instruments()
135            .iter()
136            .map(|entry| entry.key().to_string())
137            .collect()
138    }
139
140    /// Cache instruments in the HTTP client for use by order submitter.
141    ///
142    /// This method accepts a list of instrument Python objects returned from `request_instruments()`
143    /// and caches them internally for use by the order submitter.
144    #[pyo3(name = "cache_instruments")]
145    fn py_cache_instruments(
146        &self,
147        py: Python<'_>,
148        py_instruments: Vec<Bound<'_, PyAny>>,
149    ) -> PyResult<()> {
150        use nautilus_model::{
151            instruments::InstrumentAny, python::instruments::pyobject_to_instrument_any,
152        };
153
154        let instruments: Vec<InstrumentAny> = py_instruments
155            .into_iter()
156            .map(|py_inst| {
157                // Convert Bound<PyAny> to Py<PyAny> using unbind()
158                pyobject_to_instrument_any(py, py_inst.unbind())
159            })
160            .collect::<Result<Vec<_>, _>>()
161            .map_err(to_pyvalue_err)?;
162
163        self.cache_instruments(instruments);
164        Ok(())
165    }
166
167    /// Fetches orders for a subaccount.
168    ///
169    /// Returns a JSON string containing the orders response.
170    #[pyo3(name = "get_orders")]
171    #[pyo3(signature = (address, subaccount_number, market=None, limit=None))]
172    fn py_get_orders<'py>(
173        &self,
174        py: Python<'py>,
175        address: String,
176        subaccount_number: u32,
177        market: Option<String>,
178        limit: Option<u32>,
179    ) -> PyResult<Bound<'py, PyAny>> {
180        let client = self.clone();
181        pyo3_async_runtimes::tokio::future_into_py(py, async move {
182            let response = client
183                .inner
184                .get_orders(&address, subaccount_number, market.as_deref(), limit)
185                .await
186                .map_err(to_pyvalue_err)?;
187            serde_json::to_string(&response).map_err(to_pyvalue_err)
188        })
189    }
190
191    /// Fetches fills for a subaccount.
192    ///
193    /// Returns a JSON string containing the fills response.
194    #[pyo3(name = "get_fills")]
195    #[pyo3(signature = (address, subaccount_number, market=None, limit=None))]
196    fn py_get_fills<'py>(
197        &self,
198        py: Python<'py>,
199        address: String,
200        subaccount_number: u32,
201        market: Option<String>,
202        limit: Option<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_fills(&address, subaccount_number, market.as_deref(), limit)
209                .await
210                .map_err(to_pyvalue_err)?;
211            serde_json::to_string(&response).map_err(to_pyvalue_err)
212        })
213    }
214
215    /// Fetches subaccount info including positions.
216    ///
217    /// Returns a JSON string containing the subaccount response.
218    #[pyo3(name = "get_subaccount")]
219    fn py_get_subaccount<'py>(
220        &self,
221        py: Python<'py>,
222        address: String,
223        subaccount_number: u32,
224    ) -> PyResult<Bound<'py, PyAny>> {
225        let client = self.clone();
226        pyo3_async_runtimes::tokio::future_into_py(py, async move {
227            let response = client
228                .inner
229                .get_subaccount(&address, subaccount_number)
230                .await
231                .map_err(to_pyvalue_err)?;
232            serde_json::to_string(&response).map_err(to_pyvalue_err)
233        })
234    }
235
236    /// Requests order status reports for a subaccount.
237    ///
238    /// Returns Nautilus `OrderStatusReport` objects.
239    #[pyo3(name = "request_order_status_reports")]
240    #[pyo3(signature = (address, subaccount_number, account_id, instrument_id=None))]
241    fn py_request_order_status_reports<'py>(
242        &self,
243        py: Python<'py>,
244        address: String,
245        subaccount_number: u32,
246        account_id: AccountId,
247        instrument_id: Option<InstrumentId>,
248    ) -> PyResult<Bound<'py, PyAny>> {
249        let client = self.clone();
250        pyo3_async_runtimes::tokio::future_into_py(py, async move {
251            let reports = client
252                .request_order_status_reports(
253                    &address,
254                    subaccount_number,
255                    account_id,
256                    instrument_id,
257                )
258                .await
259                .map_err(to_pyvalue_err)?;
260
261            Python::attach(|py| {
262                let pylist =
263                    PyList::new(py, reports.into_iter().map(|r| r.into_py_any_unwrap(py)))?;
264                Ok(pylist.into_py_any_unwrap(py))
265            })
266        })
267    }
268
269    /// Requests fill reports for a subaccount.
270    ///
271    /// Returns Nautilus `FillReport` objects.
272    #[pyo3(name = "request_fill_reports")]
273    #[pyo3(signature = (address, subaccount_number, account_id, instrument_id=None))]
274    fn py_request_fill_reports<'py>(
275        &self,
276        py: Python<'py>,
277        address: String,
278        subaccount_number: u32,
279        account_id: AccountId,
280        instrument_id: Option<InstrumentId>,
281    ) -> PyResult<Bound<'py, PyAny>> {
282        let client = self.clone();
283        pyo3_async_runtimes::tokio::future_into_py(py, async move {
284            let reports = client
285                .request_fill_reports(&address, subaccount_number, account_id, instrument_id)
286                .await
287                .map_err(to_pyvalue_err)?;
288
289            Python::attach(|py| {
290                let pylist =
291                    PyList::new(py, reports.into_iter().map(|r| r.into_py_any_unwrap(py)))?;
292                Ok(pylist.into_py_any_unwrap(py))
293            })
294        })
295    }
296
297    /// Requests position status reports for a subaccount.
298    ///
299    /// Returns Nautilus `PositionStatusReport` objects.
300    #[pyo3(name = "request_position_status_reports")]
301    #[pyo3(signature = (address, subaccount_number, account_id, instrument_id=None))]
302    fn py_request_position_status_reports<'py>(
303        &self,
304        py: Python<'py>,
305        address: String,
306        subaccount_number: u32,
307        account_id: AccountId,
308        instrument_id: Option<InstrumentId>,
309    ) -> PyResult<Bound<'py, PyAny>> {
310        let client = self.clone();
311        pyo3_async_runtimes::tokio::future_into_py(py, async move {
312            let reports = client
313                .request_position_status_reports(
314                    &address,
315                    subaccount_number,
316                    account_id,
317                    instrument_id,
318                )
319                .await
320                .map_err(to_pyvalue_err)?;
321
322            Python::attach(|py| {
323                let pylist =
324                    PyList::new(py, reports.into_iter().map(|r| r.into_py_any_unwrap(py)))?;
325                Ok(pylist.into_py_any_unwrap(py))
326            })
327        })
328    }
329
330    /// Requests historical bars for a symbol.
331    ///
332    /// Fetches candle data and converts to Nautilus `Bar` objects.
333    /// Results are ordered by timestamp ascending (oldest first).
334    ///
335    /// Parameters
336    /// ----------
337    /// bar_type : str
338    ///     The bar type string (e.g., "ETH-USD-PERP.DYDX-1-MINUTE-LAST-EXTERNAL").
339    /// resolution : str
340    ///     The dYdX candle resolution (e.g., "1MIN", "5MINS", "1HOUR", "1DAY").
341    /// limit : int, optional
342    ///     Maximum number of bars to fetch.
343    /// start : str, optional
344    ///     Start time in ISO 8601 format.
345    /// end : str, optional
346    ///     End time in ISO 8601 format.
347    ///
348    /// Returns
349    /// -------
350    /// list[Bar]
351    ///     List of Nautilus Bar objects.
352    #[pyo3(name = "request_bars")]
353    #[pyo3(signature = (bar_type, resolution, limit=None, start=None, end=None))]
354    fn py_request_bars<'py>(
355        &self,
356        py: Python<'py>,
357        bar_type: String,
358        resolution: String,
359        limit: Option<u32>,
360        start: Option<String>,
361        end: Option<String>,
362    ) -> PyResult<Bound<'py, PyAny>> {
363        use std::str::FromStr;
364
365        use chrono::DateTime;
366        use nautilus_model::data::BarType;
367
368        use crate::common::enums::DydxCandleResolution;
369
370        let bar_type = BarType::from_str(&bar_type).map_err(to_pyvalue_err)?;
371        let resolution = DydxCandleResolution::from_str(&resolution).map_err(to_pyvalue_err)?;
372
373        let from_iso = start
374            .map(|s| DateTime::parse_from_rfc3339(&s).map(|dt| dt.with_timezone(&chrono::Utc)))
375            .transpose()
376            .map_err(to_pyvalue_err)?;
377
378        let to_iso = end
379            .map(|s| DateTime::parse_from_rfc3339(&s).map(|dt| dt.with_timezone(&chrono::Utc)))
380            .transpose()
381            .map_err(to_pyvalue_err)?;
382
383        let client = self.clone();
384
385        pyo3_async_runtimes::tokio::future_into_py(py, async move {
386            let bars = client
387                .request_bars(bar_type, resolution, limit, from_iso, to_iso)
388                .await
389                .map_err(to_pyvalue_err)?;
390
391            Python::attach(|py| {
392                let pylist = PyList::new(py, bars.into_iter().map(|b| b.into_py_any_unwrap(py)))?;
393                Ok(pylist.into_py_any_unwrap(py))
394            })
395        })
396    }
397
398    /// Requests historical trade ticks for a symbol.
399    ///
400    /// Fetches trade data and converts to Nautilus `TradeTick` objects.
401    /// Results are ordered by timestamp descending (newest first).
402    ///
403    /// Parameters
404    /// ----------
405    /// instrument_id : InstrumentId
406    ///     The instrument ID to fetch trades for.
407    /// limit : int, optional
408    ///     Maximum number of trades to fetch.
409    ///
410    /// Returns
411    /// -------
412    /// list[TradeTick]
413    ///     List of Nautilus TradeTick objects.
414    #[pyo3(name = "request_trade_ticks")]
415    #[pyo3(signature = (instrument_id, limit=None))]
416    fn py_request_trade_ticks<'py>(
417        &self,
418        py: Python<'py>,
419        instrument_id: InstrumentId,
420        limit: Option<u32>,
421    ) -> PyResult<Bound<'py, PyAny>> {
422        let client = self.clone();
423
424        pyo3_async_runtimes::tokio::future_into_py(py, async move {
425            let trades = client
426                .request_trade_ticks(instrument_id, limit)
427                .await
428                .map_err(to_pyvalue_err)?;
429
430            Python::attach(|py| {
431                let pylist = PyList::new(py, trades.into_iter().map(|t| t.into_py_any_unwrap(py)))?;
432                Ok(pylist.into_py_any_unwrap(py))
433            })
434        })
435    }
436
437    /// Requests an order book snapshot for a symbol.
438    ///
439    /// Fetches order book data and converts to Nautilus `OrderBookDeltas`.
440    /// The snapshot is represented as deltas starting with CLEAR followed by ADD actions.
441    ///
442    /// Parameters
443    /// ----------
444    /// instrument_id : InstrumentId
445    ///     The instrument ID to fetch the order book for.
446    ///
447    /// Returns
448    /// -------
449    /// OrderBookDeltas
450    ///     The order book snapshot as deltas.
451    #[pyo3(name = "request_orderbook_snapshot")]
452    fn py_request_orderbook_snapshot<'py>(
453        &self,
454        py: Python<'py>,
455        instrument_id: InstrumentId,
456    ) -> PyResult<Bound<'py, PyAny>> {
457        let client = self.clone();
458
459        pyo3_async_runtimes::tokio::future_into_py(py, async move {
460            let deltas = client
461                .request_orderbook_snapshot(instrument_id)
462                .await
463                .map_err(to_pyvalue_err)?;
464
465            Python::attach(|py| Ok(deltas.into_py_any_unwrap(py)))
466        })
467    }
468
469    /// Get current server time from the dYdX Indexer.
470    ///
471    /// Returns
472    /// -------
473    /// dict
474    ///     Dictionary containing 'iso' (ISO 8601 string) and 'epoch' (Unix timestamp float).
475    #[pyo3(name = "get_time")]
476    fn py_get_time<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
477        let client = self.clone();
478        pyo3_async_runtimes::tokio::future_into_py(py, async move {
479            let response = client.inner.get_time().await.map_err(to_pyvalue_err)?;
480            Python::attach(|py| {
481                use pyo3::types::PyDict;
482                let dict = PyDict::new(py);
483                dict.set_item("iso", response.iso.to_string())?;
484                dict.set_item("epoch", response.epoch_ms)?;
485                Ok(dict.into_py_any_unwrap(py))
486            })
487        })
488    }
489
490    /// Get current blockchain height from the dYdX Indexer.
491    ///
492    /// Returns
493    /// -------
494    /// dict
495    ///     Dictionary containing 'height' (block number) and 'time' (ISO 8601 string).
496    #[pyo3(name = "get_height")]
497    fn py_get_height<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
498        let client = self.clone();
499        pyo3_async_runtimes::tokio::future_into_py(py, async move {
500            let response = client.inner.get_height().await.map_err(to_pyvalue_err)?;
501            Python::attach(|py| {
502                use pyo3::types::PyDict;
503                let dict = PyDict::new(py);
504                dict.set_item("height", response.height)?;
505                dict.set_item("time", response.time)?;
506                Ok(dict.into_py_any_unwrap(py))
507            })
508        })
509    }
510
511    /// Fetches transfer history for a subaccount.
512    ///
513    /// Returns a JSON string containing the transfers response.
514    #[pyo3(name = "get_transfers")]
515    #[pyo3(signature = (address, subaccount_number, limit=None))]
516    fn py_get_transfers<'py>(
517        &self,
518        py: Python<'py>,
519        address: String,
520        subaccount_number: u32,
521        limit: Option<u32>,
522    ) -> PyResult<Bound<'py, PyAny>> {
523        let client = self.clone();
524        pyo3_async_runtimes::tokio::future_into_py(py, async move {
525            let response = client
526                .inner
527                .get_transfers(&address, subaccount_number, limit)
528                .await
529                .map_err(to_pyvalue_err)?;
530            serde_json::to_string(&response).map_err(to_pyvalue_err)
531        })
532    }
533
534    fn __repr__(&self) -> String {
535        format!(
536            "DydxHttpClient(base_url='{}', is_testnet={}, cached_instruments={})",
537            self.base_url(),
538            self.is_testnet(),
539            self.instruments().len()
540        )
541    }
542}