hyperliquid_http_exec/
http_exec.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 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::{env, str::FromStr};
17
18use nautilus_hyperliquid::http::{
19    client::HyperliquidHttpClient,
20    models::{
21        HyperliquidExecAction, HyperliquidExecGrouping, HyperliquidExecLimitParams,
22        HyperliquidExecOrderKind, HyperliquidExecPlaceOrderRequest, HyperliquidExecTif,
23    },
24};
25use rust_decimal::Decimal;
26use rust_decimal_macros::dec;
27use tracing_subscriber::{EnvFilter, fmt};
28
29#[tokio::main]
30async fn main() -> Result<(), Box<dyn std::error::Error>> {
31    let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
32    fmt().with_target(false).with_env_filter(filter).init();
33
34    let _ = env::var("HYPERLIQUID_TESTNET_PK")
35        .expect("HYPERLIQUID_TESTNET_PK environment variable not set");
36
37    tracing::info!("Starting Hyperliquid Testnet Order Placer");
38
39    let client = match HyperliquidHttpClient::from_env() {
40        Ok(client) => {
41            tracing::info!("Client created (testnet: {})", client.is_testnet());
42            client
43        }
44        Err(e) => {
45            tracing::error!("Failed to create client: {}", e);
46            return Err(e.into());
47        }
48    };
49
50    tracing::info!("Fetching market metadata...");
51    let meta = client.info_meta().await?;
52
53    // Debug: Print all assets
54    tracing::debug!("Available assets:");
55    for (idx, asset) in meta.universe.iter().enumerate() {
56        tracing::debug!(
57            "  [{}] {} (sz_decimals: {})",
58            idx,
59            asset.name,
60            asset.sz_decimals
61        );
62    }
63
64    let btc_asset_id = meta
65        .universe
66        .iter()
67        .position(|asset| asset.name == "BTC")
68        .expect("BTC not found in universe");
69
70    tracing::info!("BTC asset ID: {}", btc_asset_id);
71    tracing::info!(
72        "BTC sz_decimals: {}",
73        meta.universe[btc_asset_id].sz_decimals
74    );
75
76    // Get the wallet address to verify authentication
77    let wallet_address = client
78        .get_user_address()
79        .expect("Failed to get wallet address");
80    tracing::info!("Wallet address: {}", wallet_address);
81
82    // Check account state before placing order
83    tracing::info!("Fetching account state...");
84    match client.info_clearinghouse_state(&wallet_address).await {
85        Ok(state) => {
86            tracing::info!(
87                "Account state: {}",
88                serde_json::to_string_pretty(&state).unwrap_or_else(|_| "N/A".to_string())
89            );
90        }
91        Err(e) => {
92            tracing::warn!("Failed to fetch account state: {}", e);
93        }
94    }
95
96    tracing::info!("Fetching BTC order book...");
97    let book = client.info_l2_book("BTC").await?;
98
99    let best_bid_str = &book.levels[0][0].px;
100    let best_bid = Decimal::from_str(best_bid_str)?;
101
102    tracing::info!("Best bid: ${}", best_bid);
103
104    // BTC prices on Hyperliquid must be whole dollars (no decimal places)
105    let limit_price = (best_bid * dec!(0.95)).round();
106    tracing::info!("Limit order price: ${}", limit_price);
107
108    let order = HyperliquidExecPlaceOrderRequest {
109        asset: btc_asset_id as u32,
110        is_buy: true,
111        price: limit_price,
112        size: dec!(0.001),
113        reduce_only: false,
114        kind: HyperliquidExecOrderKind::Limit {
115            limit: HyperliquidExecLimitParams {
116                tif: HyperliquidExecTif::Gtc,
117            },
118        },
119        cloid: None,
120    };
121
122    tracing::info!("Order details:");
123    tracing::info!("  Asset: {} (BTC)", btc_asset_id);
124    tracing::info!("  Side: BUY");
125    tracing::info!("  Price: ${}", limit_price);
126    tracing::info!("  Size: 0.001 BTC");
127
128    tracing::info!("Placing order...");
129
130    // Create the action using the typed HyperliquidExecAction enum
131    let action = HyperliquidExecAction::Order {
132        orders: vec![order],
133        grouping: HyperliquidExecGrouping::Na,
134        builder: None,
135    };
136
137    tracing::debug!("ExchangeAction: {:?}", action);
138
139    // Also log the action as JSON
140    if let Ok(action_json) = serde_json::to_value(&action) {
141        tracing::debug!(
142            "Action JSON: {}",
143            serde_json::to_string_pretty(&action_json)?
144        );
145    }
146
147    match client.post_action_exec(&action).await {
148        Ok(response) => {
149            tracing::info!("Order placed successfully!");
150            tracing::info!("Response: {:#?}", response);
151
152            // Also log as JSON for easier reading
153            if let Ok(json) = serde_json::to_string_pretty(&response) {
154                tracing::info!("Response JSON:\n{}", json);
155            }
156        }
157        Err(e) => {
158            tracing::error!("Failed to place order: {}", e);
159            tracing::error!("Error details: {:?}", e);
160            return Err(e.into());
161        }
162    }
163    tracing::info!("Done!");
164    Ok(())
165}