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
18pub mod enums;
19pub mod http;
20pub mod urls;
21pub mod websocket;
22
23use nautilus_core::python::{to_pyruntime_err, to_pyvalue_err};
24use nautilus_model::identifiers::ClientOrderId;
25use pyo3::prelude::*;
26
27use crate::{
28    common::builder_fee::{
29        BuilderFeeInfo, approve_from_env, revoke_from_env, verify_from_env_or_address,
30    },
31    http::models::Cloid,
32};
33
34/// Compute the cloid (hex hash) from a client_order_id.
35///
36/// The cloid is a keccak256 hash of the client_order_id, truncated to 16 bytes,
37/// represented as a hex string with `0x` prefix.
38#[pyfunction]
39#[pyo3(name = "hyperliquid_cloid_from_client_order_id")]
40fn py_hyperliquid_cloid_from_client_order_id(client_order_id: ClientOrderId) -> String {
41    Cloid::from_client_order_id(client_order_id).to_hex()
42}
43
44/// Extract product type from a Hyperliquid symbol.
45///
46/// # Errors
47///
48/// Returns an error if the symbol does not contain a valid Hyperliquid product type suffix.
49#[pyfunction]
50#[pyo3(name = "hyperliquid_product_type_from_symbol")]
51fn py_hyperliquid_product_type_from_symbol(
52    symbol: &str,
53) -> PyResult<crate::common::HyperliquidProductType> {
54    crate::common::HyperliquidProductType::from_symbol(symbol).map_err(to_pyvalue_err)
55}
56
57/// Get Hyperliquid builder fee configuration information.
58///
59/// Returns a JSON string with the builder address and fee rates.
60#[pyfunction]
61#[pyo3(name = "get_hyperliquid_builder_fee_info")]
62fn py_get_hyperliquid_builder_fee_info() -> PyResult<String> {
63    let info = BuilderFeeInfo::new();
64    serde_json::to_string(&info).map_err(to_pyvalue_err)
65}
66
67/// Print Hyperliquid builder fee configuration to stdout.
68#[pyfunction]
69#[pyo3(name = "print_hyperliquid_builder_fee_info")]
70fn py_print_hyperliquid_builder_fee_info() {
71    BuilderFeeInfo::new().print();
72}
73
74/// Approve the Nautilus builder fee for a wallet.
75///
76/// This signs an EIP-712 `ApproveBuilderFee` action and submits it to Hyperliquid.
77/// The approval allows NautilusTrader to include builder fees on orders for this wallet.
78///
79/// This is a ONE-TIME setup step required before trading on Hyperliquid.
80///
81/// Reads private key from environment:
82/// - Testnet: `HYPERLIQUID_TESTNET_PK`
83/// - Mainnet: `HYPERLIQUID_PK`
84///
85/// Set `HYPERLIQUID_TESTNET=true` to use testnet.
86///
87/// # Returns
88///
89/// `true` if approval succeeded, `false` otherwise.
90#[pyfunction]
91#[pyo3(name = "approve_hyperliquid_builder_fee", signature = (non_interactive=false))]
92fn py_approve_hyperliquid_builder_fee(non_interactive: bool) -> PyResult<bool> {
93    std::thread::spawn(move || {
94        let runtime = tokio::runtime::Builder::new_current_thread()
95            .enable_all()
96            .build()
97            .map_err(|e| to_pyruntime_err(format!("Failed to create runtime: {e}")))?;
98
99        Ok(runtime.block_on(approve_from_env(non_interactive)))
100    })
101    .join()
102    .map_err(|_| to_pyruntime_err("Thread panicked"))?
103}
104
105/// Revoke the Nautilus builder fee approval for your wallet.
106///
107/// This signs an `ApproveBuilderFee` action with a 0% rate and submits it to Hyperliquid,
108/// effectively revoking the builder's permission.
109///
110/// Reads private key from environment:
111/// - Testnet: `HYPERLIQUID_TESTNET_PK`
112/// - Mainnet: `HYPERLIQUID_PK`
113///
114/// Set `HYPERLIQUID_TESTNET=true` to use testnet.
115///
116/// WARNING: After revoking, you will not be able to trade on Hyperliquid via
117/// NautilusTrader until you re-approve.
118///
119/// # Returns
120///
121/// `true` if revocation succeeded, `false` otherwise.
122#[pyfunction]
123#[pyo3(name = "revoke_hyperliquid_builder_fee", signature = (non_interactive=false))]
124fn py_revoke_hyperliquid_builder_fee(non_interactive: bool) -> PyResult<bool> {
125    std::thread::spawn(move || {
126        let runtime = tokio::runtime::Builder::new_current_thread()
127            .enable_all()
128            .build()
129            .map_err(|e| to_pyruntime_err(format!("Failed to create runtime: {e}")))?;
130
131        Ok(runtime.block_on(revoke_from_env(non_interactive)))
132    })
133    .join()
134    .map_err(|_| to_pyruntime_err("Thread panicked"))?
135}
136
137/// Verify the Nautilus builder fee approval status for a wallet.
138///
139/// Queries Hyperliquid's `maxBuilderFee` endpoint to check if the wallet
140/// has approved the Nautilus builder fee at the required rate.
141///
142/// If `wallet_address` is provided, uses it directly. Otherwise reads private key
143/// from environment to derive wallet address:
144/// - Testnet: `HYPERLIQUID_TESTNET_PK`
145/// - Mainnet: `HYPERLIQUID_PK`
146///
147/// Set `HYPERLIQUID_TESTNET=true` to use testnet.
148///
149/// # Returns
150///
151/// `true` if builder fee is approved at the required rate, `false` otherwise.
152#[pyfunction]
153#[pyo3(name = "verify_hyperliquid_builder_fee", signature = (wallet_address=None))]
154fn py_verify_hyperliquid_builder_fee(wallet_address: Option<String>) -> PyResult<bool> {
155    std::thread::spawn(move || {
156        let runtime = tokio::runtime::Builder::new_current_thread()
157            .enable_all()
158            .build()
159            .map_err(|e| to_pyruntime_err(format!("Failed to create runtime: {e}")))?;
160
161        Ok(runtime.block_on(verify_from_env_or_address(wallet_address)))
162    })
163    .join()
164    .map_err(|_| to_pyruntime_err("Thread panicked"))?
165}
166
167/// Loaded as `nautilus_pyo3.hyperliquid`.
168#[pymodule]
169pub fn hyperliquid(m: &Bound<'_, PyModule>) -> PyResult<()> {
170    m.add(
171        "HYPERLIQUID_POST_ONLY_WOULD_MATCH",
172        crate::common::consts::HYPERLIQUID_POST_ONLY_WOULD_MATCH,
173    )?;
174    m.add(
175        "HYPERLIQUID_BUILDER_FEE_NOT_APPROVED",
176        crate::common::consts::HYPERLIQUID_BUILDER_FEE_NOT_APPROVED,
177    )?;
178    m.add_class::<crate::http::HyperliquidHttpClient>()?;
179    m.add_class::<crate::websocket::HyperliquidWebSocketClient>()?;
180    m.add_class::<crate::common::enums::HyperliquidProductType>()?;
181    m.add_class::<crate::common::enums::HyperliquidTpSl>()?;
182    m.add_class::<crate::common::enums::HyperliquidConditionalOrderType>()?;
183    m.add_class::<crate::common::enums::HyperliquidTrailingOffsetType>()?;
184    m.add_function(wrap_pyfunction!(urls::py_get_hyperliquid_http_base_url, m)?)?;
185    m.add_function(wrap_pyfunction!(urls::py_get_hyperliquid_ws_url, m)?)?;
186    m.add_function(wrap_pyfunction!(
187        py_hyperliquid_product_type_from_symbol,
188        m
189    )?)?;
190    m.add_function(wrap_pyfunction!(
191        py_hyperliquid_cloid_from_client_order_id,
192        m
193    )?)?;
194    m.add_function(wrap_pyfunction!(py_get_hyperliquid_builder_fee_info, m)?)?;
195    m.add_function(wrap_pyfunction!(py_print_hyperliquid_builder_fee_info, m)?)?;
196    m.add_function(wrap_pyfunction!(py_approve_hyperliquid_builder_fee, m)?)?;
197    m.add_function(wrap_pyfunction!(py_revoke_hyperliquid_builder_fee, m)?)?;
198    m.add_function(wrap_pyfunction!(py_verify_hyperliquid_builder_fee, m)?)?;
199
200    Ok(())
201}