feat: enhance HyperliquidMonitor to parse and emit all trade actions: Open, Increase, Decrease, Close

This commit is contained in:
0xcathiefish
2026-05-30 09:10:27 +00:00
parent e6dc20f845
commit df09672130
5 changed files with 84 additions and 39 deletions
+11 -3
View File
@@ -28,11 +28,19 @@ async fn main() -> Result<()> {
} }
}); });
info!("Listening for position open events. Close with Ctrl+C..."); info!("Listening for position trade events. Close with Ctrl+C...");
while let Some(event) = rx.recv().await { while let Some(event) = rx.recv().await {
info!( info!(
"DEMO RECEIVED POSITION OPEN: coin={}, side={}, px={}, sz={}, time={}, tid={}", "DEMO RECEIVED POSITION TRADE: coin={}, side={}, px={}, sz={}, action={:?}, start_pos={}, end_pos={}, time={}, tid={}",
event.coin, event.side, event.px, event.sz, event.time, event.tid event.coin,
event.side,
event.px,
event.sz,
event.action,
event.start_pos,
event.end_pos,
event.time,
event.tid
); );
} }
+32 -14
View File
@@ -1,4 +1,4 @@
use crate::structs::PositionOpenEvent; use crate::structs::{PositionTradeEvent, TradeAction};
use alloy::primitives::Address; use alloy::primitives::Address;
use hyperliquid_rust_sdk::{BaseUrl, InfoClient, Message, Subscription, UserData}; use hyperliquid_rust_sdk::{BaseUrl, InfoClient, Message, Subscription, UserData};
use thiserror::Error; use thiserror::Error;
@@ -31,7 +31,7 @@ impl HyperliquidMonitor {
/// Runs the monitor WebSocket subscription in a loop, reconnecting if disconnected. /// Runs the monitor WebSocket subscription in a loop, reconnecting if disconnected.
pub async fn run( pub async fn run(
&self, &self,
event_tx: UnboundedSender<PositionOpenEvent>, event_tx: UnboundedSender<PositionTradeEvent>,
) -> Result<(), HyperliquidMonitorError> { ) -> Result<(), HyperliquidMonitorError> {
let base_url = if self.is_testnet { let base_url = if self.is_testnet {
BaseUrl::Testnet BaseUrl::Testnet
@@ -90,36 +90,54 @@ impl HyperliquidMonitor {
debug!(user_msg = ?user_msg, "Received user event message"); debug!(user_msg = ?user_msg, "Received user event message");
if let UserData::Fills(fills) = user_msg.data { if let UserData::Fills(fills) = user_msg.data {
for fill in fills { for fill in fills {
// Check if start_position is 0, which indicates a new position is opening // Parse position details
let start_pos: f64 = fill.start_position.parse().unwrap_or(0.0); let start_pos: f64 = fill.start_position.parse().unwrap_or(0.0);
if start_pos == 0.0 { let sz: f64 = fill.sz.parse().unwrap_or(0.0);
let dir = if fill.side == "B" { 1.0 } else { -1.0 };
let change = dir * sz;
let end_pos = start_pos + change;
// Determine the trade action
let action = if start_pos == 0.0 {
TradeAction::Open
} else if end_pos == 0.0 {
TradeAction::Close
} else if start_pos.signum() != end_pos.signum() {
TradeAction::Decrease
} else if end_pos.abs() > start_pos.abs() {
TradeAction::Increase
} else {
TradeAction::Decrease
};
info!( info!(
coin = %fill.coin, coin = %fill.coin,
side = %fill.side, side = %fill.side,
px = %fill.px, px = %fill.px,
sz = %fill.sz, sz = %fill.sz,
action = ?action,
start_pos = start_pos,
end_pos = end_pos,
tid = fill.tid, tid = fill.tid,
"Position opening detected!" "Wallet trade fill detected!"
); );
let event = PositionOpenEvent {
let event = PositionTradeEvent {
coin: fill.coin, coin: fill.coin,
side: fill.side, side: fill.side,
px: fill.px, px: fill.px,
sz: fill.sz, sz: fill.sz,
time: fill.time, time: fill.time,
tid: fill.tid, tid: fill.tid,
action,
start_pos: fill.start_position.clone(),
end_pos: format!("{:.5}", end_pos),
}; };
if let Err(e) = event_tx.send(event) { if let Err(e) = event_tx.send(event) {
error!( error!(
error = ?e, error = ?e,
"Failed to send PositionOpenEvent through channel" "Failed to send PositionTradeEvent through channel"
);
}
} else {
debug!(
coin = %fill.coin,
start_pos = start_pos,
"Ignore non-opening trade fill"
); );
} }
} }
+1 -1
View File
@@ -2,4 +2,4 @@ pub mod hyperliquid;
pub mod structs; pub mod structs;
pub use hyperliquid::{HyperliquidMonitor, HyperliquidMonitorError}; pub use hyperliquid::{HyperliquidMonitor, HyperliquidMonitorError};
pub use structs::PositionOpenEvent; pub use structs::{PositionTradeEvent, TradeAction};
+4 -1
View File
@@ -48,9 +48,12 @@ async fn main() -> Result<()> {
side = %event.side, side = %event.side,
px = %event.px, px = %event.px,
sz = %event.sz, sz = %event.sz,
action = ?event.action,
start_pos = %event.start_pos,
end_pos = %event.end_pos,
time = event.time, time = event.time,
tid = event.tid, tid = event.tid,
"Captured position opening event!" "Captured position trade event!"
); );
// Notion database integration will be wired here in the future // Notion database integration will be wired here in the future
} }
+20 -4
View File
@@ -1,10 +1,26 @@
/// Represents a detected position opening event on Hyperliquid. /// The action type of a trade fill relative to the position.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TradeAction {
/// Opening a position (initial position size was 0)
Open,
/// Adding to an existing position (absolute position size increases)
Increase,
/// Reducing an existing position (absolute position size decreases but remains non-zero)
Decrease,
/// Fully closing an existing position (resulting position size becomes 0)
Close,
}
/// Represents a detected trade event on Hyperliquid affecting a wallet's position.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct PositionOpenEvent { pub struct PositionTradeEvent {
pub coin: String, pub coin: String,
pub side: String, // "B" (Buy) or "S" (Sell) pub side: String, // "B" (Buy) or "S" (Sell)
pub px: String, // Price as string pub px: String, // Price as string
pub sz: String, // Size as string pub sz: String, // Trade size as string
pub time: u64, // Epoch timestamp in milliseconds pub time: u64, // Epoch timestamp in milliseconds
pub tid: u64, // Unique trade/fill ID pub tid: u64, // Unique trade ID
pub action: TradeAction,
pub start_pos: String, // Position size before the trade
pub end_pos: String, // Position size after the trade
} }