Skip to main content

nautilus_hyperliquid/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
16use std::collections::HashMap;
17
18use nautilus_core::python::{IntoPyObjectNautilusExt, to_pyvalue_err};
19use nautilus_model::{
20    data::BarType,
21    enums::{OrderSide, OrderType, TimeInForce},
22    identifiers::{AccountId, ClientOrderId, InstrumentId, VenueOrderId},
23    instruments::{Instrument, InstrumentAny},
24    orders::OrderAny,
25    python::{
26        instruments::{instrument_any_to_pyobject, pyobject_to_instrument_any},
27        orders::pyobject_to_order_any,
28    },
29    types::{Price, Quantity},
30};
31use pyo3::{prelude::*, types::PyList};
32use serde_json::to_string;
33
34use crate::http::client::HyperliquidHttpClient;
35
36#[pymethods]
37impl HyperliquidHttpClient {
38    /// Creates a new [`HyperliquidHttpClient`].
39    ///
40    /// If credentials are not provided, falls back to environment variables:
41    /// - Testnet: `HYPERLIQUID_TESTNET_PK`, `HYPERLIQUID_TESTNET_VAULT`
42    /// - Mainnet: `HYPERLIQUID_PK`, `HYPERLIQUID_VAULT`
43    ///
44    /// If no credentials are provided and no environment variables are set,
45    /// creates an unauthenticated client for public endpoints only.
46    #[new]
47    #[pyo3(signature = (private_key=None, vault_address=None, is_testnet=false, timeout_secs=None, proxy_url=None))]
48    fn py_new(
49        private_key: Option<String>,
50        vault_address: Option<String>,
51        is_testnet: bool,
52        timeout_secs: Option<u64>,
53        proxy_url: Option<String>,
54    ) -> PyResult<Self> {
55        Self::with_credentials(
56            private_key,
57            vault_address,
58            is_testnet,
59            timeout_secs,
60            proxy_url,
61        )
62        .map_err(to_pyvalue_err)
63    }
64
65    #[staticmethod]
66    #[pyo3(name = "from_env", signature = (is_testnet=false))]
67    fn py_from_env(is_testnet: bool) -> PyResult<Self> {
68        Self::from_env(is_testnet).map_err(to_pyvalue_err)
69    }
70
71    #[staticmethod]
72    #[pyo3(name = "from_credentials", signature = (private_key, vault_address=None, is_testnet=false, timeout_secs=None, proxy_url=None))]
73    fn py_from_credentials(
74        private_key: &str,
75        vault_address: Option<&str>,
76        is_testnet: bool,
77        timeout_secs: Option<u64>,
78        proxy_url: Option<String>,
79    ) -> PyResult<Self> {
80        Self::from_credentials(
81            private_key,
82            vault_address,
83            is_testnet,
84            timeout_secs,
85            proxy_url,
86        )
87        .map_err(to_pyvalue_err)
88    }
89
90    #[pyo3(name = "cache_instrument")]
91    fn py_cache_instrument(&self, py: Python<'_>, instrument: Py<PyAny>) -> PyResult<()> {
92        self.cache_instrument(pyobject_to_instrument_any(py, instrument)?);
93        Ok(())
94    }
95
96    #[pyo3(name = "set_account_id")]
97    fn py_set_account_id(&mut self, account_id: &str) -> PyResult<()> {
98        let account_id = AccountId::from(account_id);
99        self.set_account_id(account_id);
100        Ok(())
101    }
102
103    #[pyo3(name = "get_user_address")]
104    fn py_get_user_address(&self) -> PyResult<String> {
105        self.get_user_address().map_err(to_pyvalue_err)
106    }
107
108    #[pyo3(name = "get_spot_fill_coin_mapping")]
109    fn py_get_spot_fill_coin_mapping(&self) -> HashMap<String, String> {
110        self.get_spot_fill_coin_mapping()
111            .into_iter()
112            .map(|(k, v)| (k.to_string(), v.to_string()))
113            .collect()
114    }
115
116    #[pyo3(name = "get_spot_meta")]
117    fn py_get_spot_meta<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
118        let client = self.clone();
119        pyo3_async_runtimes::tokio::future_into_py(py, async move {
120            let meta = client.get_spot_meta().await.map_err(to_pyvalue_err)?;
121            to_string(&meta).map_err(to_pyvalue_err)
122        })
123    }
124
125    #[pyo3(name = "get_perp_meta")]
126    fn py_get_perp_meta<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
127        let client = self.clone();
128        pyo3_async_runtimes::tokio::future_into_py(py, async move {
129            let meta = client.load_perp_meta().await.map_err(to_pyvalue_err)?;
130            to_string(&meta).map_err(to_pyvalue_err)
131        })
132    }
133
134    #[pyo3(name = "load_instrument_definitions", signature = (include_perp=true, include_spot=true))]
135    fn py_load_instrument_definitions<'py>(
136        &self,
137        py: Python<'py>,
138        include_perp: bool,
139        include_spot: bool,
140    ) -> PyResult<Bound<'py, PyAny>> {
141        let client = self.clone();
142
143        pyo3_async_runtimes::tokio::future_into_py(py, async move {
144            let mut instruments = client.request_instruments().await.map_err(to_pyvalue_err)?;
145
146            if !include_perp || !include_spot {
147                instruments.retain(|instrument| match instrument {
148                    InstrumentAny::CryptoPerpetual(_) => include_perp,
149                    InstrumentAny::CurrencyPair(_) => include_spot,
150                    _ => true,
151                });
152            }
153
154            instruments.sort_by_key(|instrument| instrument.id());
155
156            Python::attach(|py| {
157                let mut py_instruments = Vec::with_capacity(instruments.len());
158                for instrument in instruments {
159                    py_instruments.push(instrument_any_to_pyobject(py, instrument)?);
160                }
161
162                let py_list = PyList::new(py, &py_instruments)?;
163                Ok(py_list.into_any().unbind())
164            })
165        })
166    }
167
168    #[pyo3(name = "request_quote_ticks", signature = (instrument_id, start=None, end=None, limit=None))]
169    fn py_request_quote_ticks<'py>(
170        &self,
171        py: Python<'py>,
172        instrument_id: InstrumentId,
173        start: Option<chrono::DateTime<chrono::Utc>>,
174        end: Option<chrono::DateTime<chrono::Utc>>,
175        limit: Option<u32>,
176    ) -> PyResult<Bound<'py, PyAny>> {
177        let _ = (instrument_id, start, end, limit);
178        pyo3_async_runtimes::tokio::future_into_py(py, async move {
179            Err::<Vec<u8>, _>(to_pyvalue_err(anyhow::anyhow!(
180                "Hyperliquid does not provide historical quotes via HTTP API"
181            )))
182        })
183    }
184
185    #[pyo3(name = "request_trade_ticks", signature = (instrument_id, start=None, end=None, limit=None))]
186    fn py_request_trade_ticks<'py>(
187        &self,
188        py: Python<'py>,
189        instrument_id: InstrumentId,
190        start: Option<chrono::DateTime<chrono::Utc>>,
191        end: Option<chrono::DateTime<chrono::Utc>>,
192        limit: Option<u32>,
193    ) -> PyResult<Bound<'py, PyAny>> {
194        let _ = (instrument_id, start, end, limit);
195        pyo3_async_runtimes::tokio::future_into_py(py, async move {
196            Err::<Vec<u8>, _>(to_pyvalue_err(anyhow::anyhow!(
197                "Hyperliquid does not provide historical market trades via HTTP API"
198            )))
199        })
200    }
201
202    #[pyo3(name = "request_bars", signature = (bar_type, start=None, end=None, limit=None))]
203    fn py_request_bars<'py>(
204        &self,
205        py: Python<'py>,
206        bar_type: BarType,
207        start: Option<chrono::DateTime<chrono::Utc>>,
208        end: Option<chrono::DateTime<chrono::Utc>>,
209        limit: Option<u32>,
210    ) -> PyResult<Bound<'py, PyAny>> {
211        let client = self.clone();
212
213        pyo3_async_runtimes::tokio::future_into_py(py, async move {
214            let bars = client
215                .request_bars(bar_type, start, end, limit)
216                .await
217                .map_err(to_pyvalue_err)?;
218
219            Python::attach(|py| {
220                let pylist = PyList::new(py, bars.into_iter().map(|b| b.into_py_any_unwrap(py)))?;
221                Ok(pylist.into_py_any_unwrap(py))
222            })
223        })
224    }
225
226    #[pyo3(name = "submit_order", signature = (
227        instrument_id,
228        client_order_id,
229        order_side,
230        order_type,
231        quantity,
232        time_in_force,
233        price=None,
234        trigger_price=None,
235        post_only=false,
236        reduce_only=false,
237    ))]
238    #[allow(clippy::too_many_arguments)]
239    fn py_submit_order<'py>(
240        &self,
241        py: Python<'py>,
242        instrument_id: InstrumentId,
243        client_order_id: ClientOrderId,
244        order_side: OrderSide,
245        order_type: OrderType,
246        quantity: Quantity,
247        time_in_force: TimeInForce,
248        price: Option<Price>,
249        trigger_price: Option<Price>,
250        post_only: bool,
251        reduce_only: bool,
252    ) -> PyResult<Bound<'py, PyAny>> {
253        let client = self.clone();
254
255        pyo3_async_runtimes::tokio::future_into_py(py, async move {
256            let report = client
257                .submit_order(
258                    instrument_id,
259                    client_order_id,
260                    order_side,
261                    order_type,
262                    quantity,
263                    time_in_force,
264                    price,
265                    trigger_price,
266                    post_only,
267                    reduce_only,
268                )
269                .await
270                .map_err(to_pyvalue_err)?;
271
272            Python::attach(|py| Ok(report.into_py_any_unwrap(py)))
273        })
274    }
275
276    #[pyo3(name = "cancel_order", signature = (
277        instrument_id,
278        client_order_id=None,
279        venue_order_id=None,
280    ))]
281    fn py_cancel_order<'py>(
282        &self,
283        py: Python<'py>,
284        instrument_id: InstrumentId,
285        client_order_id: Option<ClientOrderId>,
286        venue_order_id: Option<VenueOrderId>,
287    ) -> PyResult<Bound<'py, PyAny>> {
288        let client = self.clone();
289
290        pyo3_async_runtimes::tokio::future_into_py(py, async move {
291            client
292                .cancel_order(instrument_id, client_order_id, venue_order_id)
293                .await
294                .map_err(to_pyvalue_err)?;
295            Ok(())
296        })
297    }
298
299    #[pyo3(name = "submit_orders")]
300    fn py_submit_orders<'py>(
301        &self,
302        py: Python<'py>,
303        orders: Vec<Py<PyAny>>,
304    ) -> PyResult<Bound<'py, PyAny>> {
305        let client = self.clone();
306
307        pyo3_async_runtimes::tokio::future_into_py(py, async move {
308            let order_anys: Vec<OrderAny> = Python::attach(|py| {
309                orders
310                    .into_iter()
311                    .map(|order| pyobject_to_order_any(py, order))
312                    .collect::<PyResult<Vec<_>>>()
313                    .map_err(to_pyvalue_err)
314            })?;
315
316            let order_refs: Vec<&OrderAny> = order_anys.iter().collect();
317
318            let reports = client
319                .submit_orders(&order_refs)
320                .await
321                .map_err(to_pyvalue_err)?;
322
323            Python::attach(|py| {
324                let pylist =
325                    PyList::new(py, reports.into_iter().map(|r| r.into_py_any_unwrap(py)))?;
326                Ok(pylist.into_py_any_unwrap(py))
327            })
328        })
329    }
330
331    #[pyo3(name = "request_order_status_reports")]
332    fn py_request_order_status_reports<'py>(
333        &self,
334        py: Python<'py>,
335        instrument_id: Option<&str>,
336    ) -> PyResult<Bound<'py, PyAny>> {
337        let client = self.clone();
338        let instrument_id = instrument_id.map(InstrumentId::from);
339
340        pyo3_async_runtimes::tokio::future_into_py(py, async move {
341            let account_address = client.get_account_address().map_err(to_pyvalue_err)?;
342            let reports = client
343                .request_order_status_reports(&account_address, instrument_id)
344                .await
345                .map_err(to_pyvalue_err)?;
346
347            Python::attach(|py| {
348                let pylist =
349                    PyList::new(py, reports.into_iter().map(|r| r.into_py_any_unwrap(py)))?;
350                Ok(pylist.into_py_any_unwrap(py))
351            })
352        })
353    }
354
355    #[pyo3(name = "request_fill_reports")]
356    fn py_request_fill_reports<'py>(
357        &self,
358        py: Python<'py>,
359        instrument_id: Option<&str>,
360    ) -> PyResult<Bound<'py, PyAny>> {
361        let client = self.clone();
362        let instrument_id = instrument_id.map(InstrumentId::from);
363
364        pyo3_async_runtimes::tokio::future_into_py(py, async move {
365            let account_address = client.get_account_address().map_err(to_pyvalue_err)?;
366            let reports = client
367                .request_fill_reports(&account_address, instrument_id)
368                .await
369                .map_err(to_pyvalue_err)?;
370
371            Python::attach(|py| {
372                let pylist =
373                    PyList::new(py, reports.into_iter().map(|r| r.into_py_any_unwrap(py)))?;
374                Ok(pylist.into_py_any_unwrap(py))
375            })
376        })
377    }
378
379    #[pyo3(name = "request_position_status_reports")]
380    fn py_request_position_status_reports<'py>(
381        &self,
382        py: Python<'py>,
383        instrument_id: Option<&str>,
384    ) -> PyResult<Bound<'py, PyAny>> {
385        let client = self.clone();
386        let instrument_id = instrument_id.map(InstrumentId::from);
387
388        pyo3_async_runtimes::tokio::future_into_py(py, async move {
389            let account_address = client.get_account_address().map_err(to_pyvalue_err)?;
390            let reports = client
391                .request_position_status_reports(&account_address, instrument_id)
392                .await
393                .map_err(to_pyvalue_err)?;
394
395            Python::attach(|py| {
396                let pylist =
397                    PyList::new(py, reports.into_iter().map(|r| r.into_py_any_unwrap(py)))?;
398                Ok(pylist.into_py_any_unwrap(py))
399            })
400        })
401    }
402
403    #[pyo3(name = "request_account_state")]
404    fn py_request_account_state<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
405        let client = self.clone();
406
407        pyo3_async_runtimes::tokio::future_into_py(py, async move {
408            let account_address = client.get_account_address().map_err(to_pyvalue_err)?;
409            let account_state = client
410                .request_account_state(&account_address)
411                .await
412                .map_err(to_pyvalue_err)?;
413
414            Python::attach(|py| Ok(account_state.into_py_any_unwrap(py)))
415        })
416    }
417}