Skip to main content

nautilus_hyperliquid/python/
mod.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
16//! Python bindings from `pyo3`.
17
18#![expect(
19    clippy::missing_errors_doc,
20    reason = "errors documented on underlying Rust methods"
21)]
22
23pub mod config;
24pub mod enums;
25pub mod factories;
26pub mod http;
27pub mod urls;
28pub mod websocket;
29
30use nautilus_common::factories::{ClientConfig, DataClientFactory, ExecutionClientFactory};
31use nautilus_core::python::{to_pyruntime_err, to_pyvalue_err};
32use nautilus_model::{data::ensure_rust_extractor_registered, identifiers::ClientOrderId};
33use nautilus_system::get_global_pyo3_registry;
34use pyo3::prelude::*;
35
36use crate::{
37    account::resolve_execution_account_address,
38    common::{
39        consts::{HYPERLIQUID, HYPERLIQUID_POST_ONLY_WOULD_MATCH},
40        enums::{
41            HyperliquidConditionalOrderType, HyperliquidEnvironment, HyperliquidProductType,
42            HyperliquidTpSl, HyperliquidTrailingOffsetType,
43        },
44    },
45    config::{HyperliquidDataClientConfig, HyperliquidExecClientConfig},
46    data_types::{
47        HyperliquidAllDexsAssetCtxs, HyperliquidAllMids, HyperliquidOpenInterest,
48        register_hyperliquid_custom_data,
49    },
50    factories::{
51        HyperliquidDataClientFactory, HyperliquidExecFactoryConfig,
52        HyperliquidExecutionClientFactory,
53    },
54    http::{HyperliquidHttpClient, models::Cloid},
55    websocket::HyperliquidWebSocketClient,
56};
57
58/// Compute the deterministic CLOID from a client_order_id.
59///
60/// The CLOID is derived from a keccak256 hash of the client_order_id,
61/// truncated to 16 bytes, represented as a hex string with `0x` prefix.
62#[pyfunction]
63#[pyo3_stub_gen::derive::gen_stub_pyfunction(module = "nautilus_trader.adapters.hyperliquid")]
64#[pyo3(name = "hyperliquid_cloid_from_client_order_id")]
65fn py_hyperliquid_cloid_from_client_order_id(client_order_id: ClientOrderId) -> String {
66    Cloid::from_client_order_id(client_order_id).to_hex()
67}
68
69/// Extract product type from a Hyperliquid symbol.
70///
71/// # Errors
72///
73/// Returns an error if the symbol does not contain a valid Hyperliquid product type suffix.
74#[pyfunction]
75#[pyo3_stub_gen::derive::gen_stub_pyfunction(module = "nautilus_trader.adapters.hyperliquid")]
76#[pyo3(name = "hyperliquid_product_type_from_symbol")]
77fn py_hyperliquid_product_type_from_symbol(symbol: &str) -> PyResult<HyperliquidProductType> {
78    HyperliquidProductType::from_symbol(symbol).map_err(to_pyvalue_err)
79}
80
81/// Resolve the Hyperliquid execution account address for REST queries and WebSocket subscriptions.
82///
83/// # Errors
84///
85/// Returns an error if the selected vault address or private key is invalid.
86#[pyfunction]
87#[pyo3_stub_gen::derive::gen_stub_pyfunction(module = "nautilus_trader.adapters.hyperliquid")]
88#[pyo3(name = "hyperliquid_resolve_execution_account_address", signature = (private_key=None, vault_address=None, account_address=None, environment=HyperliquidEnvironment::Mainnet))]
89fn py_hyperliquid_resolve_execution_account_address(
90    private_key: Option<&str>,
91    vault_address: Option<&str>,
92    account_address: Option<&str>,
93    environment: HyperliquidEnvironment,
94) -> PyResult<Option<String>> {
95    resolve_execution_account_address(private_key, vault_address, account_address, environment)
96        .map_err(to_pyvalue_err)
97}
98
99#[expect(clippy::needless_pass_by_value)]
100fn extract_hyperliquid_data_factory(
101    py: Python<'_>,
102    factory: Py<PyAny>,
103) -> PyResult<Box<dyn DataClientFactory>> {
104    match factory.extract::<HyperliquidDataClientFactory>(py) {
105        Ok(f) => Ok(Box::new(f)),
106        Err(e) => Err(to_pyvalue_err(format!(
107            "Failed to extract HyperliquidDataClientFactory: {e}"
108        ))),
109    }
110}
111
112#[expect(clippy::needless_pass_by_value)]
113fn extract_hyperliquid_exec_factory(
114    py: Python<'_>,
115    factory: Py<PyAny>,
116) -> PyResult<Box<dyn ExecutionClientFactory>> {
117    match factory.extract::<HyperliquidExecutionClientFactory>(py) {
118        Ok(f) => Ok(Box::new(f)),
119        Err(e) => Err(to_pyvalue_err(format!(
120            "Failed to extract HyperliquidExecutionClientFactory: {e}"
121        ))),
122    }
123}
124
125#[expect(clippy::needless_pass_by_value)]
126fn extract_hyperliquid_data_config(
127    py: Python<'_>,
128    config: Py<PyAny>,
129) -> PyResult<Box<dyn ClientConfig>> {
130    match config.extract::<HyperliquidDataClientConfig>(py) {
131        Ok(c) => Ok(Box::new(c)),
132        Err(e) => Err(to_pyvalue_err(format!(
133            "Failed to extract HyperliquidDataClientConfig: {e}"
134        ))),
135    }
136}
137
138#[expect(clippy::needless_pass_by_value)]
139fn extract_hyperliquid_exec_config(
140    py: Python<'_>,
141    config: Py<PyAny>,
142) -> PyResult<Box<dyn ClientConfig>> {
143    match config.extract::<HyperliquidExecFactoryConfig>(py) {
144        Ok(c) => Ok(Box::new(c)),
145        Err(e) => Err(to_pyvalue_err(format!(
146            "Failed to extract HyperliquidExecFactoryConfig: {e}"
147        ))),
148    }
149}
150
151/// Loaded as `nautilus_pyo3.hyperliquid`.
152#[pymodule]
153pub fn hyperliquid(m: &Bound<'_, PyModule>) -> PyResult<()> {
154    m.add(
155        "HYPERLIQUID_POST_ONLY_WOULD_MATCH",
156        HYPERLIQUID_POST_ONLY_WOULD_MATCH,
157    )?;
158    m.add_class::<HyperliquidHttpClient>()?;
159    m.add_class::<HyperliquidWebSocketClient>()?;
160    m.add_class::<HyperliquidProductType>()?;
161    m.add_class::<HyperliquidTpSl>()?;
162    m.add_class::<HyperliquidConditionalOrderType>()?;
163    m.add_class::<HyperliquidTrailingOffsetType>()?;
164    m.add_class::<HyperliquidEnvironment>()?;
165    m.add_function(wrap_pyfunction!(urls::py_get_hyperliquid_http_base_url, m)?)?;
166    m.add_function(wrap_pyfunction!(urls::py_get_hyperliquid_ws_url, m)?)?;
167    m.add_function(wrap_pyfunction!(
168        py_hyperliquid_product_type_from_symbol,
169        m
170    )?)?;
171    m.add_function(wrap_pyfunction!(
172        py_hyperliquid_cloid_from_client_order_id,
173        m
174    )?)?;
175    m.add_function(wrap_pyfunction!(
176        py_hyperliquid_resolve_execution_account_address,
177        m
178    )?)?;
179    m.add_class::<HyperliquidDataClientConfig>()?;
180    m.add_class::<HyperliquidExecClientConfig>()?;
181    m.add_class::<HyperliquidExecFactoryConfig>()?;
182    m.add_class::<HyperliquidDataClientFactory>()?;
183    m.add_class::<HyperliquidExecutionClientFactory>()?;
184    m.add_class::<HyperliquidAllDexsAssetCtxs>()?;
185    m.add_class::<HyperliquidAllMids>()?;
186    m.add_class::<HyperliquidOpenInterest>()?;
187
188    register_hyperliquid_custom_data();
189    let _result = ensure_rust_extractor_registered::<HyperliquidAllDexsAssetCtxs>();
190    let _result = ensure_rust_extractor_registered::<HyperliquidAllMids>();
191    let _result = ensure_rust_extractor_registered::<HyperliquidOpenInterest>();
192
193    let registry = get_global_pyo3_registry();
194
195    if let Err(e) = registry
196        .register_factory_extractor(HYPERLIQUID.to_string(), extract_hyperliquid_data_factory)
197    {
198        return Err(to_pyruntime_err(format!(
199            "Failed to register Hyperliquid data factory extractor: {e}"
200        )));
201    }
202
203    if let Err(e) = registry
204        .register_exec_factory_extractor(HYPERLIQUID.to_string(), extract_hyperliquid_exec_factory)
205    {
206        return Err(to_pyruntime_err(format!(
207            "Failed to register Hyperliquid exec factory extractor: {e}"
208        )));
209    }
210
211    if let Err(e) = registry.register_config_extractor(
212        "HyperliquidDataClientConfig".to_string(),
213        extract_hyperliquid_data_config,
214    ) {
215        return Err(to_pyruntime_err(format!(
216            "Failed to register Hyperliquid data config extractor: {e}"
217        )));
218    }
219
220    if let Err(e) = registry.register_config_extractor(
221        "HyperliquidExecFactoryConfig".to_string(),
222        extract_hyperliquid_exec_config,
223    ) {
224        return Err(to_pyruntime_err(format!(
225            "Failed to register Hyperliquid exec config extractor: {e}"
226        )));
227    }
228
229    Ok(())
230}