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 = (private_key=None, vault_address=None, is_testnet=false, timeout_secs=None))]
33    fn py_new(
34        private_key: Option<String>,
35        vault_address: Option<String>,
36        is_testnet: bool,
37        timeout_secs: Option<u64>,
38    ) -> PyResult<Self> {
39        // Try to get credentials from parameters or environment variables
40        let pk = private_key.or_else(|| {
41            if is_testnet {
42                std::env::var("HYPERLIQUID_TESTNET_PK").ok()
43            } else {
44                std::env::var("HYPERLIQUID_PK").ok()
45            }
46        });
47
48        let vault = vault_address.or_else(|| {
49            if is_testnet {
50                std::env::var("HYPERLIQUID_TESTNET_VAULT").ok()
51            } else {
52                std::env::var("HYPERLIQUID_VAULT").ok()
53            }
54        });
55
56        if let Some(key) = pk {
57            Self::from_credentials(&key, vault.as_deref(), is_testnet, timeout_secs)
58                .map_err(to_pyvalue_err)
59        } else {
60            Ok(Self::new(is_testnet, timeout_secs))
61        }
62    }
63
64    /// Create an authenticated HTTP client from environment variables.
65    ///
66    /// Reads credentials from:
67    /// - `HYPERLIQUID_PK` or `HYPERLIQUID_TESTNET_PK` (private key)
68    /// - `HYPERLIQUID_VAULT` or `HYPERLIQUID_TESTNET_VAULT` (optional vault address)
69    ///
70    /// Returns an authenticated HyperliquidHttpClient or raises an error if credentials are missing.
71    #[staticmethod]
72    #[pyo3(name = "from_env")]
73    fn py_from_env() -> PyResult<Self> {
74        Self::from_env().map_err(to_pyvalue_err)
75    }
76
77    /// Create an authenticated HTTP client with explicit credentials.
78    ///
79    /// Args:
80    ///     private_key: The private key hex string (with or without 0x prefix)
81    ///     vault_address: Optional vault address for vault trading
82    ///     is_testnet: Whether to use testnet (default: false)
83    ///     timeout_secs: Optional request timeout in seconds
84    ///
85    /// Returns an authenticated HyperliquidHttpClient or raises an error if credentials are invalid.
86    #[staticmethod]
87    #[pyo3(name = "from_credentials", signature = (private_key, vault_address=None, is_testnet=false, timeout_secs=None))]
88    fn py_from_credentials(
89        private_key: &str,
90        vault_address: Option<&str>,
91        is_testnet: bool,
92        timeout_secs: Option<u64>,
93    ) -> PyResult<Self> {
94        Self::from_credentials(private_key, vault_address, is_testnet, timeout_secs)
95            .map_err(to_pyvalue_err)
96    }
97
98    /// Get perpetuals metadata as a JSON string.
99    #[pyo3(name = "get_perp_meta")]
100    fn py_get_perp_meta<'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            let meta = client.load_perp_meta().await.map_err(to_pyvalue_err)?;
104            to_string(&meta).map_err(to_pyvalue_err)
105        })
106    }
107
108    /// Get spot metadata as a JSON string.
109    #[pyo3(name = "get_spot_meta")]
110    fn py_get_spot_meta<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
111        let client = self.clone();
112        pyo3_async_runtimes::tokio::future_into_py(py, async move {
113            let meta = client.get_spot_meta().await.map_err(to_pyvalue_err)?;
114            to_string(&meta).map_err(to_pyvalue_err)
115        })
116    }
117
118    /// Get L2 order book for a specific coin.
119    ///
120    /// Args:
121    ///     coin: The coin symbol (e.g., "BTC", "ETH")
122    ///
123    /// Returns a JSON string with the order book data.
124    #[pyo3(name = "get_l2_book")]
125    fn py_get_l2_book<'py>(&self, py: Python<'py>, coin: &str) -> PyResult<Bound<'py, PyAny>> {
126        let client = self.clone();
127        let coin = coin.to_string();
128        pyo3_async_runtimes::tokio::future_into_py(py, async move {
129            let book = client.info_l2_book(&coin).await.map_err(to_pyvalue_err)?;
130            to_string(&book).map_err(to_pyvalue_err)
131        })
132    }
133
134    /// Load all available instruments (perps and/or spot) as Nautilus instrument objects.
135    #[pyo3(name = "load_instrument_definitions", signature = (include_perp=true, include_spot=true))]
136    fn py_load_instrument_definitions<'py>(
137        &self,
138        py: Python<'py>,
139        include_perp: bool,
140        include_spot: bool,
141    ) -> PyResult<Bound<'py, PyAny>> {
142        let client = self.clone();
143
144        pyo3_async_runtimes::tokio::future_into_py(py, async move {
145            let mut instruments = client.request_instruments().await.map_err(to_pyvalue_err)?;
146
147            if !include_perp || !include_spot {
148                instruments.retain(|instrument| match instrument {
149                    InstrumentAny::CryptoPerpetual(_) => include_perp,
150                    InstrumentAny::CurrencyPair(_) => include_spot,
151                    _ => true,
152                });
153            }
154
155            instruments.sort_by_key(|instrument| instrument.id());
156
157            Python::attach(|py| {
158                let mut py_instruments = Vec::with_capacity(instruments.len());
159                for instrument in instruments {
160                    py_instruments.push(instrument_any_to_pyobject(py, instrument)?);
161                }
162
163                let py_list = PyList::new(py, &py_instruments)?;
164                Ok(py_list.into_any().unbind())
165            })
166        })
167    }
168
169    /// Submit a single order to the Hyperliquid exchange.
170    ///
171    /// Takes a Nautilus Order object and handles all conversion and serialization internally in Rust.
172    /// This pushes complexity down to the Rust layer for pure Rust execution support.
173    ///
174    /// Returns an OrderStatusReport object.
175    #[pyo3(name = "submit_order")]
176    fn py_submit_order<'py>(
177        &self,
178        py: Python<'py>,
179        order: Py<PyAny>,
180    ) -> PyResult<Bound<'py, PyAny>> {
181        let client = self.clone();
182
183        pyo3_async_runtimes::tokio::future_into_py(py, async move {
184            // Convert Python Order object to Rust OrderAny
185            let order_any =
186                Python::attach(|py| pyobject_to_order_any(py, order).map_err(to_pyvalue_err))?;
187
188            let report = client
189                .submit_order(&order_any)
190                .await
191                .map_err(to_pyvalue_err)?;
192
193            Python::attach(|py| Ok(report.into_py_any_unwrap(py)))
194        })
195    }
196
197    /// Submit multiple orders to the Hyperliquid exchange in a single request.
198    ///
199    /// Takes a list of Nautilus Order objects and handles all conversion and serialization internally in Rust.
200    /// This pushes complexity down to the Rust layer for pure Rust execution support.
201    ///
202    /// Returns a list of OrderStatusReport objects.
203    #[pyo3(name = "submit_orders")]
204    fn py_submit_orders<'py>(
205        &self,
206        py: Python<'py>,
207        orders: Vec<Py<PyAny>>,
208    ) -> PyResult<Bound<'py, PyAny>> {
209        let client = self.clone();
210
211        pyo3_async_runtimes::tokio::future_into_py(py, async move {
212            // Convert Python Order objects to Rust OrderAny objects
213            let order_anys: Vec<nautilus_model::orders::any::OrderAny> = Python::attach(|py| {
214                orders
215                    .into_iter()
216                    .map(|order| pyobject_to_order_any(py, order))
217                    .collect::<PyResult<Vec<_>>>()
218                    .map_err(to_pyvalue_err)
219            })?;
220
221            // Create references for the submit_orders call
222            let order_refs: Vec<&nautilus_model::orders::any::OrderAny> =
223                order_anys.iter().collect();
224
225            let reports = client
226                .submit_orders(&order_refs)
227                .await
228                .map_err(to_pyvalue_err)?;
229
230            Python::attach(|py| {
231                let pylist =
232                    PyList::new(py, reports.into_iter().map(|r| r.into_py_any_unwrap(py)))?;
233                Ok(pylist.into_py_any_unwrap(py))
234            })
235        })
236    }
237
238    /// Get open orders for the authenticated user.
239    ///
240    /// Returns the response from the exchange as a JSON string.
241    #[pyo3(name = "get_open_orders")]
242    fn py_get_open_orders<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
243        let client = self.clone();
244
245        pyo3_async_runtimes::tokio::future_into_py(py, async move {
246            let user_address = client.get_user_address().map_err(to_pyvalue_err)?;
247            let response = client
248                .info_open_orders(&user_address)
249                .await
250                .map_err(to_pyvalue_err)?;
251            to_string(&response).map_err(to_pyvalue_err)
252        })
253    }
254
255    /// Get clearinghouse state (balances, positions, margin) for the authenticated user.
256    ///
257    /// Returns the response from the exchange as a JSON string.
258    #[pyo3(name = "get_clearinghouse_state")]
259    fn py_get_clearinghouse_state<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
260        let client = self.clone();
261
262        pyo3_async_runtimes::tokio::future_into_py(py, async move {
263            let user_address = client.get_user_address().map_err(to_pyvalue_err)?;
264            let response = client
265                .info_clearinghouse_state(&user_address)
266                .await
267                .map_err(to_pyvalue_err)?;
268            to_string(&response).map_err(to_pyvalue_err)
269        })
270    }
271
272    /// Add an instrument to the internal cache.
273    ///
274    /// This is required before calling report generation methods.
275    #[pyo3(name = "add_instrument")]
276    fn py_add_instrument(&self, py: Python<'_>, instrument: Py<PyAny>) -> PyResult<()> {
277        self.add_instrument(pyobject_to_instrument_any(py, instrument)?);
278        Ok(())
279    }
280
281    /// Set the account ID for report generation.
282    ///
283    /// This is required before calling report generation methods.
284    #[pyo3(name = "set_account_id")]
285    fn py_set_account_id(&mut self, account_id: &str) -> PyResult<()> {
286        let account_id = nautilus_model::identifiers::AccountId::from(account_id);
287        self.set_account_id(account_id);
288        Ok(())
289    }
290
291    /// Get the user's wallet address derived from the private key.
292    ///
293    /// Returns the Ethereum address as a string (e.g., "0x123...").
294    #[pyo3(name = "get_user_address")]
295    fn py_get_user_address(&self) -> PyResult<String> {
296        self.get_user_address().map_err(to_pyvalue_err)
297    }
298
299    /// Request order status reports for the authenticated user.
300    ///
301    /// Returns a list of OrderStatusReport objects.
302    #[pyo3(name = "request_order_status_reports")]
303    fn py_request_order_status_reports<'py>(
304        &self,
305        py: Python<'py>,
306        instrument_id: Option<&str>,
307    ) -> PyResult<Bound<'py, PyAny>> {
308        let client = self.clone();
309        let instrument_id = instrument_id.map(nautilus_model::identifiers::InstrumentId::from);
310
311        pyo3_async_runtimes::tokio::future_into_py(py, async move {
312            let user_address = client.get_user_address().map_err(to_pyvalue_err)?;
313            let reports = client
314                .request_order_status_reports(&user_address, instrument_id)
315                .await
316                .map_err(to_pyvalue_err)?;
317
318            Python::attach(|py| {
319                let pylist =
320                    PyList::new(py, reports.into_iter().map(|r| r.into_py_any_unwrap(py)))?;
321                Ok(pylist.into_py_any_unwrap(py))
322            })
323        })
324    }
325
326    /// Request fill reports for the authenticated user.
327    ///
328    /// Returns a list of FillReport objects.
329    #[pyo3(name = "request_fill_reports")]
330    fn py_request_fill_reports<'py>(
331        &self,
332        py: Python<'py>,
333        instrument_id: Option<&str>,
334    ) -> PyResult<Bound<'py, PyAny>> {
335        let client = self.clone();
336        let instrument_id = instrument_id.map(nautilus_model::identifiers::InstrumentId::from);
337
338        pyo3_async_runtimes::tokio::future_into_py(py, async move {
339            let user_address = client.get_user_address().map_err(to_pyvalue_err)?;
340            let reports = client
341                .request_fill_reports(&user_address, instrument_id)
342                .await
343                .map_err(to_pyvalue_err)?;
344
345            Python::attach(|py| {
346                let pylist =
347                    PyList::new(py, reports.into_iter().map(|r| r.into_py_any_unwrap(py)))?;
348                Ok(pylist.into_py_any_unwrap(py))
349            })
350        })
351    }
352
353    /// Request position status reports for the authenticated user.
354    ///
355    /// Returns a list of PositionStatusReport objects.
356    #[pyo3(name = "request_position_status_reports")]
357    fn py_request_position_status_reports<'py>(
358        &self,
359        py: Python<'py>,
360        instrument_id: Option<&str>,
361    ) -> PyResult<Bound<'py, PyAny>> {
362        let client = self.clone();
363        let instrument_id = instrument_id.map(nautilus_model::identifiers::InstrumentId::from);
364
365        pyo3_async_runtimes::tokio::future_into_py(py, async move {
366            let user_address = client.get_user_address().map_err(to_pyvalue_err)?;
367            let reports = client
368                .request_position_status_reports(&user_address, instrument_id)
369                .await
370                .map_err(to_pyvalue_err)?;
371
372            Python::attach(|py| {
373                let pylist =
374                    PyList::new(py, reports.into_iter().map(|r| r.into_py_any_unwrap(py)))?;
375                Ok(pylist.into_py_any_unwrap(py))
376            })
377        })
378    }
379}