nautilus_hyperliquid/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
16use nautilus_core::python::{IntoPyObjectNautilusExt, to_pyvalue_err};
17use nautilus_model::{
18    instruments::{Instrument, InstrumentAny},
19    python::{
20        instruments::{instrument_any_to_pyobject, pyobject_to_instrument_any},
21        orders::pyobject_to_order_any,
22    },
23};
24use pyo3::{prelude::*, types::PyList};
25use serde_json::to_string;
26
27use crate::http::client::HyperliquidHttpClient;
28
29#[pymethods]
30impl HyperliquidHttpClient {
31    #[new]
32    #[pyo3(signature = (is_testnet=false, timeout_secs=None))]
33    fn py_new(is_testnet: bool, timeout_secs: Option<u64>) -> Self {
34        Self::new(is_testnet, timeout_secs)
35    }
36
37    /// Create an authenticated HTTP client from environment variables.
38    ///
39    /// Reads credentials from:
40    /// - `HYPERLIQUID_PK` or `HYPERLIQUID_TESTNET_PK` (private key)
41    /// - `HYPERLIQUID_VAULT` or `HYPERLIQUID_TESTNET_VAULT` (optional vault address)
42    ///
43    /// Returns an authenticated HyperliquidHttpClient or raises an error if credentials are missing.
44    #[staticmethod]
45    #[pyo3(name = "from_env")]
46    fn py_from_env() -> PyResult<Self> {
47        Self::from_env().map_err(to_pyvalue_err)
48    }
49
50    /// Get perpetuals metadata as a JSON string.
51    #[pyo3(name = "get_perp_meta")]
52    fn py_get_perp_meta<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
53        let client = self.clone();
54        pyo3_async_runtimes::tokio::future_into_py(py, async move {
55            let meta = client.load_perp_meta().await.map_err(to_pyvalue_err)?;
56            to_string(&meta).map_err(to_pyvalue_err)
57        })
58    }
59
60    /// Get spot metadata as a JSON string.
61    #[pyo3(name = "get_spot_meta")]
62    fn py_get_spot_meta<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
63        let client = self.clone();
64        pyo3_async_runtimes::tokio::future_into_py(py, async move {
65            let meta = client.get_spot_meta().await.map_err(to_pyvalue_err)?;
66            to_string(&meta).map_err(to_pyvalue_err)
67        })
68    }
69
70    /// Load all available instruments (perps and/or spot) as Nautilus instrument objects.
71    #[pyo3(name = "load_instrument_definitions", signature = (include_perp=true, include_spot=true))]
72    fn py_load_instrument_definitions<'py>(
73        &self,
74        py: Python<'py>,
75        include_perp: bool,
76        include_spot: bool,
77    ) -> PyResult<Bound<'py, PyAny>> {
78        let client = self.clone();
79
80        pyo3_async_runtimes::tokio::future_into_py(py, async move {
81            let mut instruments = client.request_instruments().await.map_err(to_pyvalue_err)?;
82
83            if !include_perp || !include_spot {
84                instruments.retain(|instrument| match instrument {
85                    InstrumentAny::CryptoPerpetual(_) => include_perp,
86                    InstrumentAny::CurrencyPair(_) => include_spot,
87                    _ => true,
88                });
89            }
90
91            instruments.sort_by_key(|instrument| instrument.id());
92
93            Python::attach(|py| {
94                let mut py_instruments = Vec::with_capacity(instruments.len());
95                for instrument in instruments {
96                    py_instruments.push(instrument_any_to_pyobject(py, instrument)?);
97                }
98
99                let py_list = PyList::new(py, &py_instruments)?;
100                Ok(py_list.into_any().unbind())
101            })
102        })
103    }
104
105    /// Submit a single order to the Hyperliquid exchange.
106    ///
107    /// Takes a Nautilus Order object and handles all conversion and serialization internally in Rust.
108    /// This pushes complexity down to the Rust layer for pure Rust execution support.
109    ///
110    /// Returns an OrderStatusReport object.
111    #[pyo3(name = "submit_order")]
112    fn py_submit_order<'py>(
113        &self,
114        py: Python<'py>,
115        order: Py<PyAny>,
116    ) -> PyResult<Bound<'py, PyAny>> {
117        let client = self.clone();
118
119        pyo3_async_runtimes::tokio::future_into_py(py, async move {
120            // Convert Python Order object to Rust OrderAny
121            let order_any =
122                Python::attach(|py| pyobject_to_order_any(py, order).map_err(to_pyvalue_err))?;
123
124            let report = client
125                .submit_order(&order_any)
126                .await
127                .map_err(to_pyvalue_err)?;
128
129            Python::attach(|py| Ok(report.into_py_any_unwrap(py)))
130        })
131    }
132
133    /// Submit multiple orders to the Hyperliquid exchange in a single request.
134    ///
135    /// Takes a list of Nautilus Order objects and handles all conversion and serialization internally in Rust.
136    /// This pushes complexity down to the Rust layer for pure Rust execution support.
137    ///
138    /// Returns a list of OrderStatusReport objects.
139    #[pyo3(name = "submit_orders")]
140    fn py_submit_orders<'py>(
141        &self,
142        py: Python<'py>,
143        orders: Vec<Py<PyAny>>,
144    ) -> PyResult<Bound<'py, PyAny>> {
145        let client = self.clone();
146
147        pyo3_async_runtimes::tokio::future_into_py(py, async move {
148            // Convert Python Order objects to Rust OrderAny objects
149            let order_anys: Vec<nautilus_model::orders::any::OrderAny> = Python::attach(|py| {
150                orders
151                    .into_iter()
152                    .map(|order| pyobject_to_order_any(py, order))
153                    .collect::<PyResult<Vec<_>>>()
154                    .map_err(to_pyvalue_err)
155            })?;
156
157            // Create references for the submit_orders call
158            let order_refs: Vec<&nautilus_model::orders::any::OrderAny> =
159                order_anys.iter().collect();
160
161            let reports = client
162                .submit_orders(&order_refs)
163                .await
164                .map_err(to_pyvalue_err)?;
165
166            Python::attach(|py| {
167                let pylist =
168                    PyList::new(py, reports.into_iter().map(|r| r.into_py_any_unwrap(py)))?;
169                Ok(pylist.into_py_any_unwrap(py))
170            })
171        })
172    }
173
174    /// Get open orders for the authenticated user.
175    ///
176    /// Returns the response from the exchange as a JSON string.
177    #[pyo3(name = "get_open_orders")]
178    fn py_get_open_orders<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
179        let client = self.clone();
180
181        pyo3_async_runtimes::tokio::future_into_py(py, async move {
182            let user_address = client.get_user_address().map_err(to_pyvalue_err)?;
183            let response = client
184                .info_open_orders(&user_address)
185                .await
186                .map_err(to_pyvalue_err)?;
187            to_string(&response).map_err(to_pyvalue_err)
188        })
189    }
190
191    /// Get clearinghouse state (balances, positions, margin) for the authenticated user.
192    ///
193    /// Returns the response from the exchange as a JSON string.
194    #[pyo3(name = "get_clearinghouse_state")]
195    fn py_get_clearinghouse_state<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
196        let client = self.clone();
197
198        pyo3_async_runtimes::tokio::future_into_py(py, async move {
199            let user_address = client.get_user_address().map_err(to_pyvalue_err)?;
200            let response = client
201                .info_clearinghouse_state(&user_address)
202                .await
203                .map_err(to_pyvalue_err)?;
204            to_string(&response).map_err(to_pyvalue_err)
205        })
206    }
207
208    /// Add an instrument to the internal cache.
209    ///
210    /// This is required before calling report generation methods.
211    #[pyo3(name = "add_instrument")]
212    fn py_add_instrument(&self, py: Python<'_>, instrument: Py<PyAny>) -> PyResult<()> {
213        self.add_instrument(pyobject_to_instrument_any(py, instrument)?);
214        Ok(())
215    }
216
217    /// Set the account ID for report generation.
218    ///
219    /// This is required before calling report generation methods.
220    #[pyo3(name = "set_account_id")]
221    fn py_set_account_id(&mut self, account_id: &str) -> PyResult<()> {
222        let account_id = nautilus_model::identifiers::AccountId::from(account_id);
223        self.set_account_id(account_id);
224        Ok(())
225    }
226
227    /// Request order status reports for the authenticated user.
228    ///
229    /// Returns a list of OrderStatusReport objects.
230    #[pyo3(name = "request_order_status_reports")]
231    fn py_request_order_status_reports<'py>(
232        &self,
233        py: Python<'py>,
234        instrument_id: Option<&str>,
235    ) -> PyResult<Bound<'py, PyAny>> {
236        let client = self.clone();
237        let instrument_id = instrument_id.map(nautilus_model::identifiers::InstrumentId::from);
238
239        pyo3_async_runtimes::tokio::future_into_py(py, async move {
240            let user_address = client.get_user_address().map_err(to_pyvalue_err)?;
241            let reports = client
242                .request_order_status_reports(&user_address, instrument_id)
243                .await
244                .map_err(to_pyvalue_err)?;
245
246            Python::attach(|py| {
247                let pylist =
248                    PyList::new(py, reports.into_iter().map(|r| r.into_py_any_unwrap(py)))?;
249                Ok(pylist.into_py_any_unwrap(py))
250            })
251        })
252    }
253
254    /// Request fill reports for the authenticated user.
255    ///
256    /// Returns a list of FillReport objects.
257    #[pyo3(name = "request_fill_reports")]
258    fn py_request_fill_reports<'py>(
259        &self,
260        py: Python<'py>,
261        instrument_id: Option<&str>,
262    ) -> PyResult<Bound<'py, PyAny>> {
263        let client = self.clone();
264        let instrument_id = instrument_id.map(nautilus_model::identifiers::InstrumentId::from);
265
266        pyo3_async_runtimes::tokio::future_into_py(py, async move {
267            let user_address = client.get_user_address().map_err(to_pyvalue_err)?;
268            let reports = client
269                .request_fill_reports(&user_address, instrument_id)
270                .await
271                .map_err(to_pyvalue_err)?;
272
273            Python::attach(|py| {
274                let pylist =
275                    PyList::new(py, reports.into_iter().map(|r| r.into_py_any_unwrap(py)))?;
276                Ok(pylist.into_py_any_unwrap(py))
277            })
278        })
279    }
280
281    /// Request position status reports for the authenticated user.
282    ///
283    /// Returns a list of PositionStatusReport objects.
284    #[pyo3(name = "request_position_status_reports")]
285    fn py_request_position_status_reports<'py>(
286        &self,
287        py: Python<'py>,
288        instrument_id: Option<&str>,
289    ) -> PyResult<Bound<'py, PyAny>> {
290        let client = self.clone();
291        let instrument_id = instrument_id.map(nautilus_model::identifiers::InstrumentId::from);
292
293        pyo3_async_runtimes::tokio::future_into_py(py, async move {
294            let user_address = client.get_user_address().map_err(to_pyvalue_err)?;
295            let reports = client
296                .request_position_status_reports(&user_address, instrument_id)
297                .await
298                .map_err(to_pyvalue_err)?;
299
300            Python::attach(|py| {
301                let pylist =
302                    PyList::new(py, reports.into_iter().map(|r| r.into_py_any_unwrap(py)))?;
303                Ok(pylist.into_py_any_unwrap(py))
304            })
305        })
306    }
307}