hyperliquid_http_exec/
http_exec.rs

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