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