From 8cc5a6ec84923bd8205ae4dd943046f72e4c0902 Mon Sep 17 00:00:00 2001 From: 0xcathiefish <72328723+0xcathiefish@users.noreply.github.com> Date: Sat, 30 May 2026 09:44:05 +0000 Subject: [PATCH] feat: implement Level and Order Type calculation and integrate with Notion writer --- src/hyperliquid.rs | 3 ++ src/main.rs | 32 +++++++++++++++------ src/notion.rs | 26 +++++++++++++++++ src/structs.rs | 70 ++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 122 insertions(+), 9 deletions(-) diff --git a/src/hyperliquid.rs b/src/hyperliquid.rs index 5e10e5c..21f458a 100644 --- a/src/hyperliquid.rs +++ b/src/hyperliquid.rs @@ -93,6 +93,7 @@ impl HyperliquidMonitor { accumulated_px_sz: f64, time: u64, tid: u64, + crossed: bool, } let mut active_opening_orders = @@ -122,6 +123,7 @@ impl HyperliquidMonitor { accumulated_px_sz: px * sz, time: fill.time, tid: fill.tid, + crossed: fill.crossed, }); info!( coin = %fill.coin, @@ -206,6 +208,7 @@ impl HyperliquidMonitor { action: TradeAction::Open, start_pos: "0.0".to_string(), end_pos: format!("{:.5}", state.accumulated_sz), + crossed: state.crossed, }; if let Err(e) = event_tx.send(event) { diff --git a/src/main.rs b/src/main.rs index 766d3e8..77a5a3a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -53,15 +53,6 @@ async fn main() -> Result<()> { info!("Service running. Monitoring position openings and writing to Notion..."); while let Some(event) = rx.recv().await { - info!( - coin = %event.coin, - side = %event.side, - px = %event.px, - sz = %event.sz, - oid = event.oid, - "Captured position open event, writing to Notion..." - ); - // Format Date/Time to "YYYY/MM/DD HH:MM" let dt = Utc .timestamp_millis_opt(event.time as i64) @@ -79,6 +70,27 @@ async fn main() -> Result<()> { "Short".to_string() }; + // Parse quantity and price to calculate USD value + let sz: f64 = event.sz.parse().unwrap_or(0.0); + let px: f64 = event.px.parse().unwrap_or(0.0); + let usd_val = sz * px; + + // Calculate Level and Determine Order Type using NotionRowData helpers + let level_str = NotionRowData::calculate_level(usd_val); + let order_type_str = NotionRowData::determine_order_type(event.crossed); + + info!( + coin = %event.coin, + side = %event.side, + px = %event.px, + sz = %event.sz, + usd_val = %format!("{:.2}", usd_val), + level = %level_str, + order_type = %order_type_str, + oid = event.oid, + "Captured position open event, writing to Notion..." + ); + // Construct Notion Row Data let row = NotionRowData { symbol: symbol_formatted, @@ -90,6 +102,8 @@ async fn main() -> Result<()> { time: event.time, order_id: event.oid, check: false, + level: level_str, + order_type: order_type_str, }; // Write row to Notion diff --git a/src/notion.rs b/src/notion.rs index 483296a..553e6f3 100644 --- a/src/notion.rs +++ b/src/notion.rs @@ -161,6 +161,32 @@ impl NotionWriter { }, ); + // 9. Level (Select column) + properties.insert( + "Level".to_string(), + PageProperty::Select { + id: None, + select: Some(SelectPropertyValue { + id: None, + name: Some(data.level), + color: None, + }), + }, + ); + + // 10. Order Type (Select column) + properties.insert( + "Order Type".to_string(), + PageProperty::Select { + id: None, + select: Some(SelectPropertyValue { + id: None, + name: Some(data.order_type), + color: None, + }), + }, + ); + // Build CreateAPageRequest and create page let request = CreateAPageRequestBuilder::default() .parent(Parent::DatabaseId { diff --git a/src/structs.rs b/src/structs.rs index da87d6c..64ce9af 100644 --- a/src/structs.rs +++ b/src/structs.rs @@ -24,6 +24,7 @@ pub struct PositionTradeEvent { pub action: TradeAction, pub start_pos: String, // Position size before the trade pub end_pos: String, // Position size after the trade + pub crossed: bool, // True if taker (market), false if maker (limit) } /// Represents the structured row data formatted for Notion database insertion. @@ -38,4 +39,73 @@ pub struct NotionRowData { pub time: u64, // Unix timestamp in milliseconds pub order_id: u64, // Hyperliquid order ID (oid) pub check: bool, // false + pub level: String, // e.g. "0", "1", "2" + pub order_type: String, // "MARKET" or "LIMIT" } + +impl NotionRowData { + /// Calculate Level from USD value: + /// level 0 = 2.5$, level 1 = 5$, level 2 = 10$, and so on (scaling logarithmically by factor of 2) + pub fn calculate_level(usd_val: f64) -> String { + if usd_val <= 0.0 { + "0".to_string() + } else { + let ratio = usd_val / 2.5; + let lvl = ratio.log2().round() as i32; + let lvl = lvl.max(0); + lvl.to_string() + } + } + + /// Determine Order Type from the crossed flag: + /// crossed == true represents MARKET (taker), crossed == false represents LIMIT (maker) + pub fn determine_order_type(crossed: bool) -> String { + if crossed { + "MARKET".to_string() + } else { + "LIMIT".to_string() + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_calculate_level() { + // Test base cases specified by user + assert_eq!(NotionRowData::calculate_level(2.5), "0"); + assert_eq!(NotionRowData::calculate_level(5.0), "1"); + assert_eq!(NotionRowData::calculate_level(10.0), "2"); + + // Test boundary limits (midpoints between levels) + // Midpoint of 2.5 and 5.0 is geometric mean sqrt(2.5 * 5.0) = ~3.535 + // log2(3.5 / 2.5) = log2(1.4) = ~0.485 (rounds to 0) -> level 0 + // log2(3.6 / 2.5) = log2(1.44) = ~0.526 (rounds to 1) -> level 1 + assert_eq!(NotionRowData::calculate_level(3.5), "0"); + assert_eq!(NotionRowData::calculate_level(3.6), "1"); + + // Midpoint of 5.0 and 10.0 is geometric mean sqrt(5 * 10) = ~7.07 + // log2(7.0 / 2.5) = log2(2.8) = ~1.485 (rounds to 1) -> level 1 + // log2(7.1 / 2.5) = log2(2.84) = ~1.506 (rounds to 2) -> level 2 + assert_eq!(NotionRowData::calculate_level(7.0), "1"); + assert_eq!(NotionRowData::calculate_level(7.1), "2"); + + // Test edge cases + assert_eq!(NotionRowData::calculate_level(0.0), "0"); + assert_eq!(NotionRowData::calculate_level(-5.5), "0"); + assert_eq!(NotionRowData::calculate_level(1.0), "0"); + + // Test higher levels + assert_eq!(NotionRowData::calculate_level(20.0), "3"); + assert_eq!(NotionRowData::calculate_level(40.0), "4"); + } + + #[test] + fn test_determine_order_type() { + assert_eq!(NotionRowData::determine_order_type(true), "MARKET"); + assert_eq!(NotionRowData::determine_order_type(false), "LIMIT"); + } +} +