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        builder_fee::{approve_from_env, revoke_from_env},
40        consts::{HYPERLIQUID, HYPERLIQUID_POST_ONLY_WOULD_MATCH},
41        enums::{
42            HyperliquidConditionalOrderType, HyperliquidEnvironment, HyperliquidProductType,
43            HyperliquidTpSl, HyperliquidTrailingOffsetType,
44        },
45    },
46    config::{HyperliquidDataClientConfig, HyperliquidExecClientConfig},
47    data_types::{
48        HyperliquidAllDexsAssetCtxs, HyperliquidAllMids, HyperliquidOpenInterest,
49        register_hyperliquid_custom_data,
50    },
51    factories::{
52        HyperliquidDataClientFactory, HyperliquidExecFactoryConfig,
53        HyperliquidExecutionClientFactory,
54    },
55    http::{HyperliquidHttpClient, models::Cloid},
56    websocket::HyperliquidWebSocketClient,
57};
58
59/// Approve the Nautilus builder fee for Hyperliquid trading.
60///
61/// One-time setup signing an `ApproveBuilderFee` action at a 0% max fee rate,
62/// enabling the zero-fee builder attribution. Reads the private key from
63/// `HYPERLIQUID_PK` (mainnet) or `HYPERLIQUID_TESTNET_PK` (testnet); set
64/// `HYPERLIQUID_TESTNET=true` to use testnet.
65///
66/// Does not prompt for confirmation; review the active environment variables
67/// before calling.
68///
69/// # Errors
70///
71/// Returns an error if the async runtime cannot be created.
72#[pyfunction]
73#[pyo3_stub_gen::derive::gen_stub_pyfunction(module = "nautilus_trader.adapters.hyperliquid")]
74#[pyo3(name = "builder_fee_approve")]
75fn py_builder_fee_approve() -> PyResult<bool> {
76    std::thread::spawn(move || {
77        let runtime = tokio::runtime::Builder::new_current_thread()
78            .enable_all()
79            .build()
80            .map_err(|e| to_pyruntime_err(format!("Failed to create runtime: {e}")))?;
81
82        Ok(runtime.block_on(approve_from_env(true)))
83    })
84    .join()
85    .map_err(|_| to_pyruntime_err("Thread panicked"))?
86}
87
88/// Revoke the Nautilus builder fee approval for Hyperliquid trading.
89///
90/// Signs an `ApproveBuilderFee` action at a 0% max fee rate, capping any
91/// previously approved builder fee at zero. Reads the private key from
92/// `HYPERLIQUID_PK` (mainnet) or `HYPERLIQUID_TESTNET_PK` (testnet); set
93/// `HYPERLIQUID_TESTNET=true` to use testnet.
94///
95/// Does not prompt for confirmation; review the active environment variables
96/// before calling.
97///
98/// # Errors
99///
100/// Returns an error if the async runtime cannot be created.
101#[pyfunction]
102#[pyo3_stub_gen::derive::gen_stub_pyfunction(module = "nautilus_trader.adapters.hyperliquid")]
103#[pyo3(name = "builder_fee_revoke")]
104fn py_builder_fee_revoke() -> PyResult<bool> {
105    std::thread::spawn(move || {
106        let runtime = tokio::runtime::Builder::new_current_thread()
107            .enable_all()
108            .build()
109            .map_err(|e| to_pyruntime_err(format!("Failed to create runtime: {e}")))?;
110
111        Ok(runtime.block_on(revoke_from_env(true)))
112    })
113    .join()
114    .map_err(|_| to_pyruntime_err("Thread panicked"))?
115}
116
117/// Compute the deterministic CLOID from a client_order_id.
118///
119/// The CLOID is derived from a keccak256 hash of the client_order_id,
120/// truncated to 16 bytes, represented as a hex string with `0x` prefix.
121#[pyfunction]
122#[pyo3_stub_gen::derive::gen_stub_pyfunction(module = "nautilus_trader.adapters.hyperliquid")]
123#[pyo3(name = "hyperliquid_cloid_from_client_order_id")]
124fn py_hyperliquid_cloid_from_client_order_id(client_order_id: ClientOrderId) -> String {
125    Cloid::from_client_order_id(client_order_id).to_hex()
126}
127
128/// Extract product type from a Hyperliquid symbol.
129///
130/// # Errors
131///
132/// Returns an error if the symbol does not contain a valid Hyperliquid product type suffix.
133#[pyfunction]
134#[pyo3_stub_gen::derive::gen_stub_pyfunction(module = "nautilus_trader.adapters.hyperliquid")]
135#[pyo3(name = "hyperliquid_product_type_from_symbol")]
136fn py_hyperliquid_product_type_from_symbol(symbol: &str) -> PyResult<HyperliquidProductType> {
137    HyperliquidProductType::from_symbol(symbol).map_err(to_pyvalue_err)
138}
139
140/// Resolve the Hyperliquid execution account address for REST queries and WebSocket subscriptions.
141///
142/// # Errors
143///
144/// Returns an error if the selected vault address or private key is invalid.
145#[pyfunction]
146#[pyo3_stub_gen::derive::gen_stub_pyfunction(module = "nautilus_trader.adapters.hyperliquid")]
147#[pyo3(name = "hyperliquid_resolve_execution_account_address", signature = (private_key=None, vault_address=None, account_address=None, environment=HyperliquidEnvironment::Mainnet))]
148fn py_hyperliquid_resolve_execution_account_address(
149    private_key: Option<&str>,
150    vault_address: Option<&str>,
151    account_address: Option<&str>,
152    environment: HyperliquidEnvironment,
153) -> PyResult<Option<String>> {
154    resolve_execution_account_address(private_key, vault_address, account_address, environment)
155        .map_err(to_pyvalue_err)
156}
157
158#[expect(clippy::needless_pass_by_value)]
159fn extract_hyperliquid_data_factory(
160    py: Python<'_>,
161    factory: Py<PyAny>,
162) -> PyResult<Box<dyn DataClientFactory>> {
163    match factory.extract::<HyperliquidDataClientFactory>(py) {
164        Ok(f) => Ok(Box::new(f)),
165        Err(e) => Err(to_pyvalue_err(format!(
166            "Failed to extract HyperliquidDataClientFactory: {e}"
167        ))),
168    }
169}
170
171#[expect(clippy::needless_pass_by_value)]
172fn extract_hyperliquid_exec_factory(
173    py: Python<'_>,
174    factory: Py<PyAny>,
175) -> PyResult<Box<dyn ExecutionClientFactory>> {
176    match factory.extract::<HyperliquidExecutionClientFactory>(py) {
177        Ok(f) => Ok(Box::new(f)),
178        Err(e) => Err(to_pyvalue_err(format!(
179            "Failed to extract HyperliquidExecutionClientFactory: {e}"
180        ))),
181    }
182}
183
184#[expect(clippy::needless_pass_by_value)]
185fn extract_hyperliquid_data_config(
186    py: Python<'_>,
187    config: Py<PyAny>,
188) -> PyResult<Box<dyn ClientConfig>> {
189    match config.extract::<HyperliquidDataClientConfig>(py) {
190        Ok(c) => Ok(Box::new(c)),
191        Err(e) => Err(to_pyvalue_err(format!(
192            "Failed to extract HyperliquidDataClientConfig: {e}"
193        ))),
194    }
195}
196
197#[expect(clippy::needless_pass_by_value)]
198fn extract_hyperliquid_exec_config(
199    py: Python<'_>,
200    config: Py<PyAny>,
201) -> PyResult<Box<dyn ClientConfig>> {
202    match config.extract::<HyperliquidExecFactoryConfig>(py) {
203        Ok(c) => Ok(Box::new(c)),
204        Err(e) => Err(to_pyvalue_err(format!(
205            "Failed to extract HyperliquidExecFactoryConfig: {e}"
206        ))),
207    }
208}
209
210/// Loaded as `nautilus_pyo3.hyperliquid`.
211#[pymodule]
212pub fn hyperliquid(m: &Bound<'_, PyModule>) -> PyResult<()> {
213    m.add(
214        "HYPERLIQUID_POST_ONLY_WOULD_MATCH",
215        HYPERLIQUID_POST_ONLY_WOULD_MATCH,
216    )?;
217    m.add_class::<HyperliquidHttpClient>()?;
218    m.add_class::<HyperliquidWebSocketClient>()?;
219    m.add_class::<HyperliquidProductType>()?;
220    m.add_class::<HyperliquidTpSl>()?;
221    m.add_class::<HyperliquidConditionalOrderType>()?;
222    m.add_class::<HyperliquidTrailingOffsetType>()?;
223    m.add_class::<HyperliquidEnvironment>()?;
224    m.add_function(wrap_pyfunction!(urls::py_get_hyperliquid_http_base_url, m)?)?;
225    m.add_function(wrap_pyfunction!(urls::py_get_hyperliquid_ws_url, m)?)?;
226    m.add_function(wrap_pyfunction!(
227        py_hyperliquid_product_type_from_symbol,
228        m
229    )?)?;
230    m.add_function(wrap_pyfunction!(
231        py_hyperliquid_cloid_from_client_order_id,
232        m
233    )?)?;
234    m.add_function(wrap_pyfunction!(
235        py_hyperliquid_resolve_execution_account_address,
236        m
237    )?)?;
238    m.add_function(wrap_pyfunction!(py_builder_fee_approve, m)?)?;
239    m.add_function(wrap_pyfunction!(py_builder_fee_revoke, m)?)?;
240    m.add_class::<HyperliquidDataClientConfig>()?;
241    m.add_class::<HyperliquidExecClientConfig>()?;
242    m.add_class::<HyperliquidExecFactoryConfig>()?;
243    m.add_class::<HyperliquidDataClientFactory>()?;
244    m.add_class::<HyperliquidExecutionClientFactory>()?;
245    m.add_class::<HyperliquidAllDexsAssetCtxs>()?;
246    m.add_class::<HyperliquidAllMids>()?;
247    m.add_class::<HyperliquidOpenInterest>()?;
248
249    register_hyperliquid_custom_data();
250    let _result = ensure_rust_extractor_registered::<HyperliquidAllDexsAssetCtxs>();
251    let _result = ensure_rust_extractor_registered::<HyperliquidAllMids>();
252    let _result = ensure_rust_extractor_registered::<HyperliquidOpenInterest>();
253
254    let registry = get_global_pyo3_registry();
255
256    if let Err(e) = registry
257        .register_factory_extractor(HYPERLIQUID.to_string(), extract_hyperliquid_data_factory)
258    {
259        return Err(to_pyruntime_err(format!(
260            "Failed to register Hyperliquid data factory extractor: {e}"
261        )));
262    }
263
264    if let Err(e) = registry
265        .register_exec_factory_extractor(HYPERLIQUID.to_string(), extract_hyperliquid_exec_factory)
266    {
267        return Err(to_pyruntime_err(format!(
268            "Failed to register Hyperliquid exec factory extractor: {e}"
269        )));
270    }
271
272    if let Err(e) = registry.register_config_extractor(
273        "HyperliquidDataClientConfig".to_string(),
274        extract_hyperliquid_data_config,
275    ) {
276        return Err(to_pyruntime_err(format!(
277            "Failed to register Hyperliquid data config extractor: {e}"
278        )));
279    }
280
281    if let Err(e) = registry.register_config_extractor(
282        "HyperliquidExecFactoryConfig".to_string(),
283        extract_hyperliquid_exec_config,
284    ) {
285        return Err(to_pyruntime_err(format!(
286            "Failed to register Hyperliquid exec config extractor: {e}"
287        )));
288    }
289
290    Ok(())
291}