diff --git a/examples/demo.rs b/examples/demo.rs index 8fc1d20..9f70e8a 100644 --- a/examples/demo.rs +++ b/examples/demo.rs @@ -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 { info!( - "DEMO RECEIVED POSITION OPEN: coin={}, side={}, px={}, sz={}, time={}, tid={}", - event.coin, event.side, event.px, event.sz, event.time, event.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.action, + event.start_pos, + event.end_pos, + event.time, + event.tid ); } diff --git a/src/hyperliquid.rs b/src/hyperliquid.rs index eb1a7b8..cbda7e5 100644 --- a/src/hyperliquid.rs +++ b/src/hyperliquid.rs @@ -1,4 +1,4 @@ -use crate::structs::PositionOpenEvent; +use crate::structs::{PositionTradeEvent, TradeAction}; use alloy::primitives::Address; use hyperliquid_rust_sdk::{BaseUrl, InfoClient, Message, Subscription, UserData}; use thiserror::Error; @@ -31,7 +31,7 @@ impl HyperliquidMonitor { /// Runs the monitor WebSocket subscription in a loop, reconnecting if disconnected. pub async fn run( &self, - event_tx: UnboundedSender, + event_tx: UnboundedSender, ) -> Result<(), HyperliquidMonitorError> { let base_url = if self.is_testnet { BaseUrl::Testnet @@ -90,36 +90,54 @@ impl HyperliquidMonitor { debug!(user_msg = ?user_msg, "Received user event message"); if let UserData::Fills(fills) = user_msg.data { 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); - if start_pos == 0.0 { - info!( - coin = %fill.coin, - side = %fill.side, - px = %fill.px, - sz = %fill.sz, - tid = fill.tid, - "Position opening detected!" - ); - let event = PositionOpenEvent { - coin: fill.coin, - side: fill.side, - px: fill.px, - sz: fill.sz, - time: fill.time, - tid: fill.tid, - }; - if let Err(e) = event_tx.send(event) { - error!( - error = ?e, - "Failed to send PositionOpenEvent through channel" - ); - } + 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 { - debug!( - coin = %fill.coin, - start_pos = start_pos, - "Ignore non-opening trade fill" + TradeAction::Decrease + }; + + info!( + coin = %fill.coin, + side = %fill.side, + px = %fill.px, + sz = %fill.sz, + action = ?action, + start_pos = start_pos, + end_pos = end_pos, + tid = fill.tid, + "Wallet trade fill detected!" + ); + + let event = PositionTradeEvent { + coin: fill.coin, + side: fill.side, + px: fill.px, + sz: fill.sz, + time: fill.time, + tid: fill.tid, + action, + start_pos: fill.start_position.clone(), + end_pos: format!("{:.5}", end_pos), + }; + + if let Err(e) = event_tx.send(event) { + error!( + error = ?e, + "Failed to send PositionTradeEvent through channel" ); } } diff --git a/src/lib.rs b/src/lib.rs index f103aae..0cd5007 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,4 +2,4 @@ pub mod hyperliquid; pub mod structs; pub use hyperliquid::{HyperliquidMonitor, HyperliquidMonitorError}; -pub use structs::PositionOpenEvent; +pub use structs::{PositionTradeEvent, TradeAction}; diff --git a/src/main.rs b/src/main.rs index 9dc045d..06c2d3d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -48,9 +48,12 @@ async fn main() -> Result<()> { side = %event.side, px = %event.px, sz = %event.sz, + action = ?event.action, + start_pos = %event.start_pos, + end_pos = %event.end_pos, time = event.time, tid = event.tid, - "Captured position opening event!" + "Captured position trade event!" ); // Notion database integration will be wired here in the future } diff --git a/src/structs.rs b/src/structs.rs index 3b95fa1..2eb4ff4 100644 --- a/src/structs.rs +++ b/src/structs.rs @@ -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)] -pub struct PositionOpenEvent { +pub struct PositionTradeEvent { pub coin: String, pub side: String, // "B" (Buy) or "S" (Sell) 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 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 }