hyperliquid_http_exec/
http_exec.rs1use 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 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 let wallet_address = client
63 .get_user_address()
64 .expect("Failed to get wallet address");
65 tracing::info!("Wallet address: {}", wallet_address);
66
67 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 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 let action = HyperliquidExecAction::Order {
117 orders: vec![order],
118 grouping: HyperliquidExecGrouping::Na,
119 builder: None,
120 };
121
122 tracing::debug!("ExchangeAction: {:?}", action);
123
124 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 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}