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,
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, parse::HyperliquidMarketType};
35
36#[pymethods]
37#[pyo3_stub_gen::derive::gen_stub_pymethods]
38impl HyperliquidHttpClient {
39    /// Provides a high-level HTTP client for the [Hyperliquid](https://hyperliquid.xyz/) REST API.
40    ///
41    /// This domain client wraps `HyperliquidRawHttpClient` and provides methods that work
42    /// with Nautilus domain types. It maintains an instrument cache and handles conversions
43    /// between Hyperliquid API responses and Nautilus domain models.
44    #[new]
45    #[pyo3(signature = (private_key=None, vault_address=None, account_address=None, is_testnet=false, timeout_secs=60, proxy_url=None, normalize_prices=true))]
46    fn py_new(
47        private_key: Option<String>,
48        vault_address: Option<String>,
49        account_address: Option<String>,
50        is_testnet: bool,
51        timeout_secs: u64,
52        proxy_url: Option<String>,
53        normalize_prices: bool,
54    ) -> PyResult<Self> {
55        let mut client = Self::with_credentials(
56            private_key,
57            vault_address,
58            account_address,
59            is_testnet,
60            timeout_secs,
61            proxy_url,
62        )
63        .map_err(to_pyvalue_err)?;
64        client.set_normalize_prices(normalize_prices);
65        Ok(client)
66    }
67
68    /// Creates an authenticated client from environment variables for the specified network.
69    ///
70    /// # Errors
71    ///
72    /// Returns `Error.Auth` if required environment variables are not set.
73    #[staticmethod]
74    #[pyo3(name = "from_env", signature = (is_testnet=false))]
75    fn py_from_env(is_testnet: bool) -> PyResult<Self> {
76        Self::from_env(is_testnet).map_err(to_pyvalue_err)
77    }
78
79    /// Creates a new `HyperliquidHttpClient` configured with explicit credentials.
80    #[staticmethod]
81    #[pyo3(name = "from_credentials", signature = (private_key, vault_address=None, is_testnet=false, timeout_secs=60, proxy_url=None))]
82    fn py_from_credentials(
83        private_key: &str,
84        vault_address: Option<&str>,
85        is_testnet: bool,
86        timeout_secs: u64,
87        proxy_url: Option<String>,
88    ) -> PyResult<Self> {
89        Self::from_credentials(
90            private_key,
91            vault_address,
92            is_testnet,
93            timeout_secs,
94            proxy_url,
95        )
96        .map_err(to_pyvalue_err)
97    }
98
99    /// Caches a single instrument.
100    ///
101    /// This is required for parsing orders, fills, and positions into reports.
102    /// Any existing instrument with the same symbol will be replaced.
103    #[pyo3(name = "cache_instrument")]
104    fn py_cache_instrument(&self, py: Python<'_>, instrument: Py<PyAny>) -> PyResult<()> {
105        self.cache_instrument(&pyobject_to_instrument_any(py, instrument)?);
106        Ok(())
107    }
108
109    /// Set the account ID for this client.
110    ///
111    /// This is required for generating reports with the correct account ID.
112    #[pyo3(name = "set_account_id")]
113    fn py_set_account_id(&mut self, account_id: &str) {
114        let account_id = AccountId::from(account_id);
115        self.set_account_id(account_id);
116    }
117
118    /// Gets the user address derived from the private key (if client has credentials).
119    ///
120    /// # Errors
121    ///
122    /// Returns `Error.Auth` if the client has no signer configured.
123    #[pyo3(name = "get_user_address")]
124    fn py_get_user_address(&self) -> PyResult<String> {
125        self.get_user_address().map_err(to_pyvalue_err)
126    }
127
128    /// Get mapping from spot fill coin identifiers to instrument symbols.
129    ///
130    /// Hyperliquid WebSocket fills for spot use `@{pair_index}` format (e.g., `@107`),
131    /// while instruments are identified by full symbols (e.g., `HYPE-USDC-SPOT`).
132    /// This mapping allows looking up the instrument from a spot fill.
133    ///
134    /// This method also caches the mapping internally for use by fill parsing methods.
135    #[pyo3(name = "get_spot_fill_coin_mapping")]
136    fn py_get_spot_fill_coin_mapping(&self) -> HashMap<String, String> {
137        self.get_spot_fill_coin_mapping()
138            .into_iter()
139            .map(|(k, v)| (k.to_string(), v.to_string()))
140            .collect()
141    }
142
143    /// Get spot metadata (internal helper).
144    #[pyo3(name = "get_spot_meta")]
145    fn py_get_spot_meta<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
146        let client = self.clone();
147        pyo3_async_runtimes::tokio::future_into_py(py, async move {
148            let meta = client.get_spot_meta().await.map_err(to_pyvalue_err)?;
149            to_string(&meta).map_err(to_pyvalue_err)
150        })
151    }
152
153    #[pyo3(name = "get_perp_meta")]
154    fn py_get_perp_meta<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
155        let client = self.clone();
156        pyo3_async_runtimes::tokio::future_into_py(py, async move {
157            let meta = client.load_perp_meta().await.map_err(to_pyvalue_err)?;
158            to_string(&meta).map_err(to_pyvalue_err)
159        })
160    }
161
162    #[pyo3(name = "load_instrument_definitions", signature = (include_spot=true, include_perps=true, include_perps_hip3=false))]
163    fn py_load_instrument_definitions<'py>(
164        &self,
165        py: Python<'py>,
166        include_spot: bool,
167        include_perps: bool,
168        include_perps_hip3: bool,
169    ) -> PyResult<Bound<'py, PyAny>> {
170        let client = self.clone();
171
172        pyo3_async_runtimes::tokio::future_into_py(py, async move {
173            let mut defs = client
174                .request_instrument_defs()
175                .await
176                .map_err(to_pyvalue_err)?;
177
178            defs.retain(|def| match def.market_type {
179                HyperliquidMarketType::Perp => {
180                    if def.is_hip3 {
181                        include_perps_hip3
182                    } else {
183                        include_perps
184                    }
185                }
186                HyperliquidMarketType::Spot => include_spot,
187            });
188
189            let mut instruments = client.convert_defs(defs);
190            instruments.sort_by_key(|instrument| instrument.id());
191
192            Python::attach(|py| {
193                let mut py_instruments = Vec::with_capacity(instruments.len());
194                for instrument in instruments {
195                    py_instruments.push(instrument_any_to_pyobject(py, instrument)?);
196                }
197
198                let py_list = PyList::new(py, &py_instruments)?;
199                Ok(py_list.into_any().unbind())
200            })
201        })
202    }
203
204    #[pyo3(name = "request_quote_ticks", signature = (instrument_id, start=None, end=None, limit=None))]
205    fn py_request_quote_ticks<'py>(
206        &self,
207        py: Python<'py>,
208        instrument_id: InstrumentId,
209        start: Option<chrono::DateTime<chrono::Utc>>,
210        end: Option<chrono::DateTime<chrono::Utc>>,
211        limit: Option<u32>,
212    ) -> PyResult<Bound<'py, PyAny>> {
213        let _ = (instrument_id, start, end, limit);
214        pyo3_async_runtimes::tokio::future_into_py(py, async move {
215            Err::<Vec<u8>, _>(to_pyvalue_err(anyhow::anyhow!(
216                "Hyperliquid does not provide historical quotes via HTTP API"
217            )))
218        })
219    }
220
221    #[pyo3(name = "request_trade_ticks", signature = (instrument_id, start=None, end=None, limit=None))]
222    fn py_request_trade_ticks<'py>(
223        &self,
224        py: Python<'py>,
225        instrument_id: InstrumentId,
226        start: Option<chrono::DateTime<chrono::Utc>>,
227        end: Option<chrono::DateTime<chrono::Utc>>,
228        limit: Option<u32>,
229    ) -> PyResult<Bound<'py, PyAny>> {
230        let _ = (instrument_id, start, end, limit);
231        pyo3_async_runtimes::tokio::future_into_py(py, async move {
232            Err::<Vec<u8>, _>(to_pyvalue_err(anyhow::anyhow!(
233                "Hyperliquid does not provide historical market trades via HTTP API"
234            )))
235        })
236    }
237
238    /// Request historical bars for an instrument.
239    ///
240    /// Fetches candle data from the Hyperliquid API and converts it to Nautilus bars.
241    /// Incomplete bars (where end_timestamp >= current time) are filtered out.
242    ///
243    /// # References
244    ///
245    /// <https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint#candles-snapshot>
246    #[pyo3(name = "request_bars", signature = (bar_type, start=None, end=None, limit=None))]
247    fn py_request_bars<'py>(
248        &self,
249        py: Python<'py>,
250        bar_type: BarType,
251        start: Option<chrono::DateTime<chrono::Utc>>,
252        end: Option<chrono::DateTime<chrono::Utc>>,
253        limit: Option<u32>,
254    ) -> PyResult<Bound<'py, PyAny>> {
255        let client = self.clone();
256
257        pyo3_async_runtimes::tokio::future_into_py(py, async move {
258            let bars = client
259                .request_bars(bar_type, start, end, limit)
260                .await
261                .map_err(to_pyvalue_err)?;
262
263            Python::attach(|py| {
264                let pylist = PyList::new(py, bars.into_iter().map(|b| b.into_py_any_unwrap(py)))?;
265                Ok(pylist.into_py_any_unwrap(py))
266            })
267        })
268    }
269
270    /// Submits an order to the exchange.
271    #[pyo3(name = "submit_order", signature = (
272        instrument_id,
273        client_order_id,
274        order_side,
275        order_type,
276        quantity,
277        time_in_force,
278        price=None,
279        trigger_price=None,
280        post_only=false,
281        reduce_only=false,
282    ))]
283    #[allow(clippy::too_many_arguments)]
284    fn py_submit_order<'py>(
285        &self,
286        py: Python<'py>,
287        instrument_id: InstrumentId,
288        client_order_id: ClientOrderId,
289        order_side: OrderSide,
290        order_type: OrderType,
291        quantity: Quantity,
292        time_in_force: TimeInForce,
293        price: Option<Price>,
294        trigger_price: Option<Price>,
295        post_only: bool,
296        reduce_only: bool,
297    ) -> PyResult<Bound<'py, PyAny>> {
298        let client = self.clone();
299
300        pyo3_async_runtimes::tokio::future_into_py(py, async move {
301            let report = client
302                .submit_order(
303                    instrument_id,
304                    client_order_id,
305                    order_side,
306                    order_type,
307                    quantity,
308                    time_in_force,
309                    price,
310                    trigger_price,
311                    post_only,
312                    reduce_only,
313                )
314                .await
315                .map_err(to_pyvalue_err)?;
316
317            Python::attach(|py| Ok(report.into_py_any_unwrap(py)))
318        })
319    }
320
321    /// Cancel an order on the Hyperliquid exchange.
322    ///
323    /// Can cancel either by venue order ID or client order ID.
324    /// At least one ID must be provided.
325    #[pyo3(name = "cancel_order", signature = (
326        instrument_id,
327        client_order_id=None,
328        venue_order_id=None,
329    ))]
330    fn py_cancel_order<'py>(
331        &self,
332        py: Python<'py>,
333        instrument_id: InstrumentId,
334        client_order_id: Option<ClientOrderId>,
335        venue_order_id: Option<VenueOrderId>,
336    ) -> PyResult<Bound<'py, PyAny>> {
337        let client = self.clone();
338
339        pyo3_async_runtimes::tokio::future_into_py(py, async move {
340            client
341                .cancel_order(instrument_id, client_order_id, venue_order_id)
342                .await
343                .map_err(to_pyvalue_err)?;
344            Ok(())
345        })
346    }
347
348    /// Modify an order on the Hyperliquid exchange.
349    ///
350    /// The HL modify API requires a full replacement order spec plus the
351    /// venue order ID. The caller must provide all order fields.
352    #[pyo3(name = "modify_order")]
353    #[allow(clippy::too_many_arguments)]
354    fn py_modify_order<'py>(
355        &self,
356        py: Python<'py>,
357        instrument_id: InstrumentId,
358        venue_order_id: VenueOrderId,
359        order_side: OrderSide,
360        order_type: OrderType,
361        price: Price,
362        quantity: Quantity,
363        trigger_price: Option<Price>,
364        reduce_only: bool,
365        post_only: bool,
366        time_in_force: TimeInForce,
367        client_order_id: Option<ClientOrderId>,
368    ) -> PyResult<Bound<'py, PyAny>> {
369        let client = self.clone();
370
371        pyo3_async_runtimes::tokio::future_into_py(py, async move {
372            client
373                .modify_order(
374                    instrument_id,
375                    venue_order_id,
376                    order_side,
377                    order_type,
378                    price,
379                    quantity,
380                    trigger_price,
381                    reduce_only,
382                    post_only,
383                    time_in_force,
384                    client_order_id,
385                )
386                .await
387                .map_err(to_pyvalue_err)?;
388            Ok(())
389        })
390    }
391
392    /// Submit multiple orders to the Hyperliquid exchange in a single request.
393    #[pyo3(name = "submit_orders")]
394    fn py_submit_orders<'py>(
395        &self,
396        py: Python<'py>,
397        orders: Vec<Py<PyAny>>,
398    ) -> PyResult<Bound<'py, PyAny>> {
399        let client = self.clone();
400
401        pyo3_async_runtimes::tokio::future_into_py(py, async move {
402            let order_anys: Vec<OrderAny> = Python::attach(|py| {
403                orders
404                    .into_iter()
405                    .map(|order| pyobject_to_order_any(py, order))
406                    .collect::<PyResult<Vec<_>>>()
407                    .map_err(to_pyvalue_err)
408            })?;
409
410            let order_refs: Vec<&OrderAny> = order_anys.iter().collect();
411
412            let reports = client
413                .submit_orders(&order_refs)
414                .await
415                .map_err(to_pyvalue_err)?;
416
417            Python::attach(|py| {
418                let pylist =
419                    PyList::new(py, reports.into_iter().map(|r| r.into_py_any_unwrap(py)))?;
420                Ok(pylist.into_py_any_unwrap(py))
421            })
422        })
423    }
424
425    /// Request order status reports for a user.
426    ///
427    /// Fetches open orders via `info_frontend_open_orders` and parses them into OrderStatusReports.
428    /// This method requires instruments to be added to the client cache via `cache_instrument()`.
429    ///
430    /// For vault tokens (starting with "vntls:") that are not in the cache, synthetic instruments
431    /// will be created automatically.
432    #[pyo3(name = "request_order_status_reports")]
433    fn py_request_order_status_reports<'py>(
434        &self,
435        py: Python<'py>,
436        instrument_id: Option<&str>,
437    ) -> PyResult<Bound<'py, PyAny>> {
438        let client = self.clone();
439        let instrument_id = instrument_id.map(InstrumentId::from);
440
441        pyo3_async_runtimes::tokio::future_into_py(py, async move {
442            let account_address = client.get_account_address().map_err(to_pyvalue_err)?;
443            let reports = client
444                .request_order_status_reports(&account_address, instrument_id)
445                .await
446                .map_err(to_pyvalue_err)?;
447
448            Python::attach(|py| {
449                let pylist =
450                    PyList::new(py, reports.into_iter().map(|r| r.into_py_any_unwrap(py)))?;
451                Ok(pylist.into_py_any_unwrap(py))
452            })
453        })
454    }
455
456    /// Request fill reports for a user.
457    ///
458    /// Fetches user fills via `info_user_fills` and parses them into FillReports.
459    /// This method requires instruments to be added to the client cache via `cache_instrument()`.
460    ///
461    /// For vault tokens (starting with "vntls:") that are not in the cache, synthetic instruments
462    /// will be created automatically.
463    #[pyo3(name = "request_fill_reports")]
464    fn py_request_fill_reports<'py>(
465        &self,
466        py: Python<'py>,
467        instrument_id: Option<&str>,
468    ) -> PyResult<Bound<'py, PyAny>> {
469        let client = self.clone();
470        let instrument_id = instrument_id.map(InstrumentId::from);
471
472        pyo3_async_runtimes::tokio::future_into_py(py, async move {
473            let account_address = client.get_account_address().map_err(to_pyvalue_err)?;
474            let reports = client
475                .request_fill_reports(&account_address, instrument_id)
476                .await
477                .map_err(to_pyvalue_err)?;
478
479            Python::attach(|py| {
480                let pylist =
481                    PyList::new(py, reports.into_iter().map(|r| r.into_py_any_unwrap(py)))?;
482                Ok(pylist.into_py_any_unwrap(py))
483            })
484        })
485    }
486
487    /// Request position status reports for a user.
488    ///
489    /// Fetches clearinghouse state via `info_clearinghouse_state` and parses positions into PositionStatusReports.
490    /// This method requires instruments to be added to the client cache via `cache_instrument()`.
491    ///
492    /// For vault tokens (starting with "vntls:") that are not in the cache, synthetic instruments
493    /// will be created automatically.
494    #[pyo3(name = "request_position_status_reports")]
495    fn py_request_position_status_reports<'py>(
496        &self,
497        py: Python<'py>,
498        instrument_id: Option<&str>,
499    ) -> PyResult<Bound<'py, PyAny>> {
500        let client = self.clone();
501        let instrument_id = instrument_id.map(InstrumentId::from);
502
503        pyo3_async_runtimes::tokio::future_into_py(py, async move {
504            let account_address = client.get_account_address().map_err(to_pyvalue_err)?;
505            let reports = client
506                .request_position_status_reports(&account_address, instrument_id)
507                .await
508                .map_err(to_pyvalue_err)?;
509
510            Python::attach(|py| {
511                let pylist =
512                    PyList::new(py, reports.into_iter().map(|r| r.into_py_any_unwrap(py)))?;
513                Ok(pylist.into_py_any_unwrap(py))
514            })
515        })
516    }
517
518    /// Request account state (balances and margins) for a user.
519    ///
520    /// Fetches clearinghouse state from Hyperliquid API and converts it to `AccountState`.
521    ///
522    /// # Errors
523    ///
524    /// Returns an error if `account_id` is not set or the API request fails.
525    #[pyo3(name = "request_account_state")]
526    fn py_request_account_state<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
527        let client = self.clone();
528
529        pyo3_async_runtimes::tokio::future_into_py(py, async move {
530            let account_address = client.get_account_address().map_err(to_pyvalue_err)?;
531            let account_state = client
532                .request_account_state(&account_address)
533                .await
534                .map_err(to_pyvalue_err)?;
535
536            Python::attach(|py| Ok(account_state.into_py_any_unwrap(py)))
537        })
538    }
539
540    /// Get user fee schedule and effective rates.
541    #[pyo3(name = "info_user_fees")]
542    fn py_info_user_fees<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
543        let client = self.clone();
544
545        pyo3_async_runtimes::tokio::future_into_py(py, async move {
546            let account_address = client.get_account_address().map_err(to_pyvalue_err)?;
547            let json = client
548                .info_user_fees(&account_address)
549                .await
550                .map_err(to_pyvalue_err)?;
551            to_string(&json).map_err(to_pyvalue_err)
552        })
553    }
554}