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