From cf4387c60421f444761583287d29f10104cbd45b Mon Sep 17 00:00:00 2001 From: Klesti Selimaj Date: Fri, 24 Jul 2026 20:07:43 +0200 Subject: [PATCH 1/8] Improved logging --- src/engine/engine.rs | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/engine/engine.rs b/src/engine/engine.rs index 0aa96c7..16a9729 100644 --- a/src/engine/engine.rs +++ b/src/engine/engine.rs @@ -51,10 +51,23 @@ impl Engine { ) .await { - eprintln!("Failed to broadcast Hyperliquid watch list: {error}"); + self.terminal_server + .error( + "Broadcaster", + &format!("Failed to broadcast HyperLiquid watch list: {error}"), + ) + .await? } } - Err(error) => eprintln!("Failed to refresh Hyperliquid watch list: {error}"), + + Err(error) => { + self.terminal_server + .error( + "Broadcaster", + &format!("Failed to refresh HyperLiquid watch list: {error}"), + ) + .await? + } } } } From db2f476928b29ee5b993f8fd62ed6a2f595939c1 Mon Sep 17 00:00:00 2001 From: Klesti Selimaj Date: Fri, 24 Jul 2026 23:31:21 +0200 Subject: [PATCH 2/8] Added accounts --- src/engine/engine.rs | 2 +- src/engine/main.rs | 2 +- src/engine/store/accounts.rs | 41 ++++++++++++++++++++++++++++++++ src/engine/{ => store}/config.rs | 18 +------------- src/engine/store/mod.rs | 22 +++++++++++++++++ 5 files changed, 66 insertions(+), 19 deletions(-) create mode 100644 src/engine/store/accounts.rs rename src/engine/{ => store}/config.rs (67%) create mode 100644 src/engine/store/mod.rs diff --git a/src/engine/engine.rs b/src/engine/engine.rs index 16a9729..d029200 100644 --- a/src/engine/engine.rs +++ b/src/engine/engine.rs @@ -2,7 +2,7 @@ use std::sync::Arc; use tokio::{sync::Mutex, task::JoinHandle}; -use crate::{config::Config, terminal::TerminalServer}; +use crate::{store::config::Config, terminal::TerminalServer}; #[derive(Debug, Clone)] pub struct Engine { diff --git a/src/engine/main.rs b/src/engine/main.rs index 2e7cd52..f4f7f64 100644 --- a/src/engine/main.rs +++ b/src/engine/main.rs @@ -1,6 +1,6 @@ -pub mod config; pub mod engine; pub mod fetch; +pub mod store; pub mod terminal; #[tokio::main] diff --git a/src/engine/store/accounts.rs b/src/engine/store/accounts.rs new file mode 100644 index 0000000..f0ef40a --- /dev/null +++ b/src/engine/store/accounts.rs @@ -0,0 +1,41 @@ +use std::collections::HashMap; + +use hypersdk::U160; + +#[derive(Debug, serde::Serialize, serde::Deserialize)] +pub struct Account { + pub address: U160, + pub private_key: String, +} + +#[derive(Debug, Default, serde::Serialize, serde::Deserialize)] +pub struct AccountList(pub HashMap); + +impl AccountList { + pub async fn new() -> tokio::io::Result { + let path = crate::store::pulse_accounts_file()?; + + if !path.exists() { + let default = Self::default(); + + tokio::fs::create_dir_all(path.parent().unwrap()).await?; + tokio::fs::write(&path, default.to_string()?).await?; + + return Ok(default); + } + + let output = tokio::fs::read_to_string(path).await?; + + Self::from_str(&output) + } + + pub fn from_str(s: &str) -> tokio::io::Result { + toml::from_str(s) + .map_err(|v| tokio::io::Error::new(std::io::ErrorKind::InvalidInput, v.to_string())) + } + + pub fn to_string(&self) -> tokio::io::Result { + toml::to_string(self) + .map_err(|v| tokio::io::Error::new(std::io::ErrorKind::Other, v.to_string())) + } +} diff --git a/src/engine/config.rs b/src/engine/store/config.rs similarity index 67% rename from src/engine/config.rs rename to src/engine/store/config.rs index 8555d75..2ead6ae 100644 --- a/src/engine/config.rs +++ b/src/engine/store/config.rs @@ -1,19 +1,3 @@ -use std::path::PathBuf; - -pub fn home_dir() -> tokio::io::Result { - std::env::home_dir().ok_or_else(|| { - tokio::io::Error::new(std::io::ErrorKind::NotFound, "Unable to get home directory") - }) -} - -pub fn pulse_directory() -> tokio::io::Result { - Ok(home_dir()?.join(".config/pulse-trader")) -} - -pub fn pulse_config_directory() -> tokio::io::Result { - Ok(home_dir()?.join(".config/pulse-trader/config.toml")) -} - #[derive(Debug, Default, serde::Serialize, serde::Deserialize)] pub struct WatchList { pub symbols: Vec, @@ -26,7 +10,7 @@ pub struct Config { impl Config { pub async fn new() -> tokio::io::Result { - let path = pulse_config_directory()?; + let path = crate::store::pulse_config_file()?; if !path.exists() { let default = Self::default(); diff --git a/src/engine/store/mod.rs b/src/engine/store/mod.rs new file mode 100644 index 0000000..5299a77 --- /dev/null +++ b/src/engine/store/mod.rs @@ -0,0 +1,22 @@ +use std::path::PathBuf; + +pub mod accounts; +pub mod config; + +pub fn home_dir() -> tokio::io::Result { + std::env::home_dir().ok_or_else(|| { + tokio::io::Error::new(std::io::ErrorKind::NotFound, "Unable to get home directory") + }) +} + +pub fn pulse_directory() -> tokio::io::Result { + Ok(home_dir()?.join(".config").join("pulse-trader")) +} + +pub fn pulse_config_file() -> tokio::io::Result { + Ok(pulse_directory()?.join("config.toml")) +} + +pub fn pulse_accounts_file() -> tokio::io::Result { + Ok(pulse_directory()?.join("accounts.toml")) +} From 895f7f79ba4574aaa3f47cb794a4b53731a39a0e Mon Sep 17 00:00:00 2001 From: Klesti Selimaj Date: Sat, 25 Jul 2026 00:48:37 +0200 Subject: [PATCH 3/8] Account manager --- src/engine/engine.rs | 65 ++++++++++++++++++++++++++++---- src/engine/store/accounts.rs | 72 ++++++++++++++++++++++++++++++++++-- 2 files changed, 125 insertions(+), 12 deletions(-) diff --git a/src/engine/engine.rs b/src/engine/engine.rs index d029200..138a3a4 100644 --- a/src/engine/engine.rs +++ b/src/engine/engine.rs @@ -2,21 +2,27 @@ use std::sync::Arc; use tokio::{sync::Mutex, task::JoinHandle}; -use crate::{store::config::Config, terminal::TerminalServer}; +use crate::{ + store::{accounts::AccountList, config::Config}, + terminal::TerminalServer, +}; #[derive(Debug, Clone)] pub struct Engine { pub terminal_server: Arc, pub config: Arc>, + pub accounts: Arc>, } impl Engine { pub async fn new() -> tokio::io::Result> { let config = Arc::new(Mutex::new(Config::new().await?)); + let accounts = Arc::new(Mutex::new(AccountList::new().await?)); Ok(Arc::new_cyclic(|engine| Self { terminal_server: TerminalServer::new(engine.clone()), config, + accounts, })) } @@ -78,15 +84,15 @@ impl Engine { } } + pub async fn invalid_command_usage(&self, name: &str) -> tokio::io::Result<()> { + self.terminal_server.error(name, "Invalid usage").await + } + pub async fn execute_command(&self, command: &str, args: Vec<&str>) -> tokio::io::Result<()> { match command { - "config" => { - const MESSAGE: &str = "Invalid command arguments, usage: config "; - + "config" | "cfg" => { if args.len() != 1 { - self.terminal_server.error("config", MESSAGE).await?; - - return Ok(()); + return self.invalid_command_usage("config").await; } match args[0] { @@ -95,7 +101,50 @@ impl Engine { } _ => { - self.terminal_server.error("config", MESSAGE).await?; + self.terminal_server + .error("config", "Invalid usage") + .await?; + } + } + } + + "account" | "acc" => { + if args.len() == 0 { + return self.invalid_command_usage("account man").await; + } + + match args[0] { + "reload" => { + *self.accounts.lock().await = AccountList::new().await?; + } + + "list" | "ls" => { + self.terminal_server + .info("account man", "ACCOUNT LIST") + .await?; + + let accounts = self.accounts.lock().await; + + for (name, acc) in &accounts.accounts { + self.terminal_server + .info( + "account man", + &if name == &accounts.active { + format!( + "{} (active) -> {}", + name, + acc.get_truncated_address() + ) + } else { + format!("{} -> {}", name, acc.get_truncated_address()) + }, + ) + .await?; + } + } + + _ => { + return self.invalid_command_usage("account man").await; } } } diff --git a/src/engine/store/accounts.rs b/src/engine/store/accounts.rs index f0ef40a..9da4af6 100644 --- a/src/engine/store/accounts.rs +++ b/src/engine/store/accounts.rs @@ -1,15 +1,56 @@ -use std::collections::HashMap; +use std::{collections::HashMap, str::FromStr}; -use hypersdk::U160; +use hypersdk::{U160, hypercore::PrivateKeySigner}; + +#[derive(Debug)] +pub struct PrivateKey(pub PrivateKeySigner, pub String); + +impl FromStr for PrivateKey { + type Err = ::Err; + + fn from_str(s: &str) -> Result { + Ok(Self(s.parse()?, s.to_string())) + } +} + +impl std::fmt::Display for PrivateKey { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.1.fmt(f) + } +} + +impl<'de> serde::Deserialize<'de> for PrivateKey { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + s.parse().map_err(serde::de::Error::custom) + } +} + +impl serde::Serialize for PrivateKey { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(&self.to_string()) + } +} #[derive(Debug, serde::Serialize, serde::Deserialize)] pub struct Account { pub address: U160, - pub private_key: String, + pub private_key: PrivateKey, } #[derive(Debug, Default, serde::Serialize, serde::Deserialize)] -pub struct AccountList(pub HashMap); +pub struct AccountList { + pub active: String, + + #[serde(flatten)] + pub accounts: HashMap, +} impl AccountList { pub async fn new() -> tokio::io::Result { @@ -39,3 +80,26 @@ impl AccountList { .map_err(|v| tokio::io::Error::new(std::io::ErrorKind::Other, v.to_string())) } } + +impl Account { + pub fn get_truncated_address(&self) -> String { + let s = format!("{:x}", self.address); + + if s.len() <= 6 { + return format!("INVALID"); + } + + let first: String = s.chars().take(4).collect(); + + let last: String = s + .chars() + .rev() + .take(4) + .collect::>() + .into_iter() + .rev() + .collect(); + + format!("{}..{}", first, last) + } +} From 2cbcdc710856d5ec46c26ad6e83e71804814ace0 Mon Sep 17 00:00:00 2001 From: Klesti Selimaj Date: Sat, 25 Jul 2026 00:58:09 +0200 Subject: [PATCH 4/8] Set active account --- src/engine/engine.rs | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/engine/engine.rs b/src/engine/engine.rs index 138a3a4..1bc2f1a 100644 --- a/src/engine/engine.rs +++ b/src/engine/engine.rs @@ -143,6 +143,32 @@ impl Engine { } } + "use" | "set" => { + if args.len() < 2 { + return self.invalid_command_usage("account man").await; + } + + let new_active = args[1]; + + let mut accounts = self.accounts.lock().await; + + if !accounts.accounts.contains_key(new_active) { + return self + .terminal_server + .error("account man", &format!("Account not found ({new_active})")) + .await; + } + + accounts.active = new_active.to_string(); + + self.terminal_server + .info( + "account man", + &format!("Account set to {new_active} successfully!"), + ) + .await?; + } + _ => { return self.invalid_command_usage("account man").await; } From e61dd25e72fa2e9e121f277fd151edc8e7a50678 Mon Sep 17 00:00:00 2001 From: Klesti Selimaj Date: Sat, 25 Jul 2026 01:57:27 +0200 Subject: [PATCH 5/8] Positions --- pulse-wire/src/terminal.rs | 1 - src/engine/engine.rs | 44 +++++++++++++++++++++++++++++++++++- src/engine/fetch.rs | 7 ++++-- src/engine/store/accounts.rs | 8 +++++-- src/terminal/formatting.rs | 1 - 5 files changed, 54 insertions(+), 7 deletions(-) diff --git a/pulse-wire/src/terminal.rs b/pulse-wire/src/terminal.rs index 2224a84..f76fd43 100644 --- a/pulse-wire/src/terminal.rs +++ b/pulse-wire/src/terminal.rs @@ -44,7 +44,6 @@ pub struct ActivePosition { pub symbol: String, pub size: f64, pub entry_price: f64, - pub mark_price: f64, pub profit: f64, } diff --git a/src/engine/engine.rs b/src/engine/engine.rs index 1bc2f1a..d186484 100644 --- a/src/engine/engine.rs +++ b/src/engine/engine.rs @@ -1,5 +1,6 @@ use std::sync::Arc; +use pulse_wire::terminal::ActivePosition; use tokio::{sync::Mutex, task::JoinHandle}; use crate::{ @@ -41,12 +42,14 @@ impl Engine { pub async fn run_broadcaster(&self) -> tokio::io::Result<()> { let mut refresh = tokio::time::interval(tokio::time::Duration::from_secs(5)); + let client = hypersdk::hypercore::mainnet(); + loop { refresh.tick().await; let watch_list = &self.config.lock().await.watchlist.symbols; - match crate::fetch::fetch_watch_list(watch_list).await { + match crate::fetch::fetch_watch_list(&client, watch_list).await { Ok(watch_list) => { if let Err(error) = self .terminal_server @@ -75,6 +78,45 @@ impl Engine { .await? } } + + let accounts = self.accounts.lock().await; + + if let Some(acc) = accounts.get_active() { + match client.clearinghouse_state(acc.address, None).await { + Ok(state) => { + self.terminal_server + .broadcast( + pulse_wire::terminal::TerminalServerMessage::PositionsUpdated( + state + .asset_positions + .into_iter() + .map(|position| ActivePosition { + symbol: position.position.coin, + size: position.position.szi.as_f64(), + entry_price: position + .position + .entry_px + .map(|px| px.as_f64()) + .unwrap_or(0.0), + profit: position.position.unrealized_pnl.as_f64(), + }) + .collect(), + ), + ) + .await?; + } + Err(e) => { + self.terminal_server + .error("orders", &format!("Unable to get open orders: {e}")) + .await?; + } + } + } else { + self.terminal_server.error( + "orders", + "Unable to get active account, make sure you have configured accounts properly", + ).await?; + } } } diff --git a/src/engine/fetch.rs b/src/engine/fetch.rs index 9f7b271..9e91a79 100644 --- a/src/engine/fetch.rs +++ b/src/engine/fetch.rs @@ -12,8 +12,11 @@ fn number(value: &Value, field: &str) -> Result { .map_err(|error| format!("could not parse asset context field {field} ({raw}): {error}")) } -pub async fn fetch_watch_list(symbols: &[String]) -> Result, String> { - let response = hypersdk::hypercore::mainnet() +pub async fn fetch_watch_list( + client: &hypersdk::hypercore::HttpClient, + symbols: &[String], +) -> Result, String> { + let response = client .meta_and_asset_ctxs(None) .await .map_err(|error| format!("Hyperliquid metaAndAssetCtxs request failed: {error}"))?; diff --git a/src/engine/store/accounts.rs b/src/engine/store/accounts.rs index 9da4af6..a2ef132 100644 --- a/src/engine/store/accounts.rs +++ b/src/engine/store/accounts.rs @@ -1,6 +1,6 @@ use std::{collections::HashMap, str::FromStr}; -use hypersdk::{U160, hypercore::PrivateKeySigner}; +use hypersdk::{Address, hypercore::PrivateKeySigner}; #[derive(Debug)] pub struct PrivateKey(pub PrivateKeySigner, pub String); @@ -40,7 +40,7 @@ impl serde::Serialize for PrivateKey { #[derive(Debug, serde::Serialize, serde::Deserialize)] pub struct Account { - pub address: U160, + pub address: Address, pub private_key: PrivateKey, } @@ -79,6 +79,10 @@ impl AccountList { toml::to_string(self) .map_err(|v| tokio::io::Error::new(std::io::ErrorKind::Other, v.to_string())) } + + pub fn get_active<'a>(&'a self) -> Option<&'a Account> { + self.accounts.get(&self.active) + } } impl Account { diff --git a/src/terminal/formatting.rs b/src/terminal/formatting.rs index 05bc89d..223c31b 100644 --- a/src/terminal/formatting.rs +++ b/src/terminal/formatting.rs @@ -101,7 +101,6 @@ impl Formatted for ActivePosition { format!("\x1b[35m{}\x1b[0m", self.symbol), format_f64(self.size), format_f64(self.entry_price), - format_f64(self.mark_price), format!( "{} {}", if self.profit.is_sign_positive() { From cde2abf5f14089c00c2c70d3b3f58132df3d2cee Mon Sep 17 00:00:00 2001 From: Klesti Selimaj Date: Sat, 25 Jul 2026 02:41:25 +0200 Subject: [PATCH 6/8] Using Option for statuses --- pulse-wire/src/terminal.rs | 2 +- src/terminal/main.rs | 43 ++++++++++++++++++++++++-------------- src/terminal/terminal.rs | 6 +++--- 3 files changed, 31 insertions(+), 20 deletions(-) diff --git a/pulse-wire/src/terminal.rs b/pulse-wire/src/terminal.rs index f76fd43..3e2af62 100644 --- a/pulse-wire/src/terminal.rs +++ b/pulse-wire/src/terminal.rs @@ -19,7 +19,7 @@ pub enum TerminalServerMessage { Inspect(InspectTarget), // Status - SetStatus(Status), + StatusUpdated(Status), // Logs SetLogs(Vec), diff --git a/src/terminal/main.rs b/src/terminal/main.rs index c0d3faf..61697e0 100644 --- a/src/terminal/main.rs +++ b/src/terminal/main.rs @@ -30,12 +30,14 @@ pub struct PulseTradeApp { command: State, scroll: State>, + + market_overview: State>, + status: State>, + watch_list: State>, active_positions: State>, logs: State>, signals: State>, - market_overview: State, - status: State, inspect: State, } @@ -135,7 +137,7 @@ impl App for PulseTradeApp { ( LayoutItem::Widget(Size::Flex(1)), Box::new( - advanced_draw(&self.scroll, 2, "MARKET OVERVIEW", &self.market_overview) + advanced_option_draw(&self.scroll, 2, "MARKET OVERVIEW", &self.market_overview) .await, ), ), @@ -155,7 +157,7 @@ impl App for PulseTradeApp { ), ( LayoutItem::Widget(Size::Flex(1)), - Box::new(advanced_draw(&self.scroll, 5, "STATUS", &self.status).await), + Box::new(advanced_option_draw(&self.scroll, 5, "STATUS", &self.status).await), ), ]), ); @@ -183,18 +185,8 @@ async fn main() -> tokio::io::Result<()> { signals: ctx.use_state(Vec::new()), logs: ctx.use_state(Vec::new()), inspect: ctx.use_state(InspectTarget::None), - market_overview: ctx.use_state(MarketOverview { - trend: pulse_wire::terminal::MarketTrend::Bullish, - volatility: pulse_wire::terminal::Volatility::High, - pressure: 0.324, - alerts: Vec::new(), - }), - status: ctx.use_state(Status { - feed: pulse_wire::terminal::Feed::Connected, - exchange: "Binance".to_string(), - dex: "DEX SCREENER".to_string(), - latency: 18, - }), + market_overview: ctx.use_state(None), + status: ctx.use_state(None), }) }) .await; @@ -216,3 +208,22 @@ pub async fn advanced_draw( apply_padding(state.lock().await.get_formatted()).join("\n"), ) } + +pub async fn advanced_option_draw( + scroll: &State>, + index: usize, + title: &'static str, + state: &State>, +) -> ScrollText { + let scroll = scroll.lock().await; + + scroll.scroll( + index, + format!(" {}{title}\x1b[0m", scroll.get_selected(index)), + if let Some(state) = &*state.lock().await { + apply_padding(state.get_formatted()).join("\n") + } else { + " Loading..".to_string() + }, + ) +} diff --git a/src/terminal/terminal.rs b/src/terminal/terminal.rs index 617979d..763cae7 100644 --- a/src/terminal/terminal.rs +++ b/src/terminal/terminal.rs @@ -78,7 +78,7 @@ impl TerminalClient { } TerminalServerMessage::OverviewUpdated(v) => { - *market_overview.lock().await = v; + *market_overview.lock().await = Some(v); } TerminalServerMessage::SignalsUpdated(v) => { @@ -89,8 +89,8 @@ impl TerminalClient { *inspect.lock().await = v; } - TerminalServerMessage::SetStatus(v) => { - *status.lock().await = v; + TerminalServerMessage::StatusUpdated(v) => { + *status.lock().await = Some(v); } TerminalServerMessage::SetLogs(v) => { From 6b6085ef25fed2c8aeb29e619a2fa684fc6dd071 Mon Sep 17 00:00:00 2001 From: Klesti Selimaj Date: Sat, 25 Jul 2026 04:08:48 +0200 Subject: [PATCH 7/8] Refactored types --- pulse-wire/src/general.rs | 97 +++++++++++++++++++++ pulse-wire/src/lib.rs | 1 + pulse-wire/src/terminal.rs | 172 ++++++++----------------------------- src/engine/engine.rs | 4 +- src/engine/fetch.rs | 6 +- src/engine/terminal.rs | 5 +- src/terminal/formatting.rs | 12 +-- src/terminal/main.rs | 21 +++-- 8 files changed, 158 insertions(+), 160 deletions(-) create mode 100644 pulse-wire/src/general.rs diff --git a/pulse-wire/src/general.rs b/pulse-wire/src/general.rs new file mode 100644 index 0000000..d7e88d9 --- /dev/null +++ b/pulse-wire/src/general.rs @@ -0,0 +1,97 @@ +use pulse_macros::pwp; + +use crate::PulseWire; + +#[pwp] +pub enum MarketTrend { + Bullish, + Bearish, + Neutral, +} + +#[pwp] +pub enum Volatility { + Low, + Medium, + High, +} + +#[pwp] +pub enum Direction { + Buy, + Sell, +} + +#[pwp] +pub struct Signal { + pub symbol: String, + pub kind: Direction, + pub confidence: f32, + pub size: f64, + pub price: f64, + pub take_profit: f64, + pub take_stop_loss: f64, +} + +#[pwp] +pub enum LogKind { + Info, + Warn, + Err, + Debug, +} + +#[pwp] +pub struct EventLog { + pub kind: LogKind, + pub name: String, + pub message: String, +} + +#[pwp] +pub struct Position { + pub symbol: String, + pub size: f64, + pub entry_price: f64, + pub profit: f64, +} + +impl std::fmt::Display for Direction { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Buy => write!(f, "BUY"), + Self::Sell => write!(f, "SELL"), + } + } +} + + +impl std::fmt::Display for MarketTrend { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Bullish => write!(f, "Bullish"), + Self::Bearish => write!(f, "Bearish"), + Self::Neutral => write!(f, "Neutral"), + } + } +} + +impl std::fmt::Display for Volatility { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Low => write!(f, "Low"), + Self::Medium => write!(f, "Medium"), + Self::High => write!(f, "High"), + } + } +} +impl std::fmt::Display for LogKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Info => write!(f, "INFO"), + Self::Warn => write!(f, "WARN"), + Self::Err => write!(f, "ERR"), + Self::Debug => write!(f, "DEBUG"), + } + } +} diff --git a/pulse-wire/src/lib.rs b/pulse-wire/src/lib.rs index 5a53401..168b737 100644 --- a/pulse-wire/src/lib.rs +++ b/pulse-wire/src/lib.rs @@ -2,6 +2,7 @@ use std::path::PathBuf; pub mod terminal; +pub mod general; pub fn server_path() -> PathBuf { PathBuf::from("/tmp/pulse-engine.sock") diff --git a/pulse-wire/src/terminal.rs b/pulse-wire/src/terminal.rs index 3e2af62..4a4f1ff 100644 --- a/pulse-wire/src/terminal.rs +++ b/pulse-wire/src/terminal.rs @@ -1,13 +1,16 @@ -use crate::PulseWire; +use crate::{ + PulseWire, + general::{EventLog, MarketTrend, Position, Signal, Volatility}, +}; use pulse_macros::pwp; #[pwp] pub enum TerminalServerMessage { // WatchList - WatchListUpdated(Vec), + WatchListUpdated(Vec), // Positions - PositionsUpdated(Vec), + PositionsUpdated(Vec), // Overview OverviewUpdated(MarketOverview), @@ -32,95 +35,19 @@ pub enum TerminalClientMessage { } #[pwp] -pub struct WatchListItem { - symbol: String, - price: f64, - trend: f64, - volume_24h: f64, -} - -#[pwp] -pub struct ActivePosition { +pub struct MarketItem { pub symbol: String, - pub size: f64, - pub entry_price: f64, - pub profit: f64, -} - -#[pwp] -pub enum MarketTrend { - Bullish, - Bearish, - Neutral, -} - -#[pwp] -pub enum Volatility { - Low, - Medium, - High, + pub price: f64, + pub trend: f64, + pub volume_24h: f64, } #[pwp] pub struct MarketOverview { - trend: MarketTrend, - volatility: Volatility, - pressure: f64, - - alerts: Vec, -} - -#[pwp] -pub enum Feed { - Connected, - Disconnected, - Connecting, - Failed, -} - -#[pwp] -pub struct Status { - feed: Feed, - exchange: String, - dex: String, - latency: u16, -} - -#[pwp] -pub enum SignalKind { - Buy, - Sell, -} - -#[pwp] -pub enum SignalParameter { - Lim, - Stl, - Tap, - Chk, -} - -#[pwp] -pub struct Signal { - kind: SignalKind, - symbol: String, - param: SignalParameter, - price: f64, -} - -#[pwp] -pub enum LogKind { - Info, - Warn, - Err, - Debug, -} - -#[pwp] -pub struct EventLog { - kind: LogKind, - name: String, - message: String, + pub trend: MarketTrend, + pub volatility: Volatility, + pub pressure: f64, + pub alerts: Vec, } #[pwp] @@ -129,13 +56,19 @@ pub enum AlertLevel { Medium, Low, } - #[pwp] pub struct Alert { level: AlertLevel, message: String, } +#[pwp] +pub struct Balance { + pub asset: String, + pub amount: f64, + pub value: f64, +} + #[pwp] pub enum InspectTarget { None, @@ -150,14 +83,20 @@ pub enum InspectItem { F64(f64), } -impl std::fmt::Display for MarketTrend { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Bullish => write!(f, "Bullish"), - Self::Bearish => write!(f, "Bearish"), - Self::Neutral => write!(f, "Neutral"), - } - } +#[pwp] +pub struct Status { + feed: Feed, + exchange: String, + dex: String, + latency: u16, +} + +#[pwp] +pub enum Feed { + Connected, + Disconnected, + Connecting, + Failed, } impl std::fmt::Display for AlertLevel { @@ -169,44 +108,3 @@ impl std::fmt::Display for AlertLevel { } } } - -impl std::fmt::Display for Volatility { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Low => write!(f, "Low"), - Self::Medium => write!(f, "Medium"), - Self::High => write!(f, "High"), - } - } -} - -impl std::fmt::Display for LogKind { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Info => write!(f, "INFO"), - Self::Warn => write!(f, "WARN"), - Self::Err => write!(f, "ERR"), - Self::Debug => write!(f, "DEBUG"), - } - } -} - -impl std::fmt::Display for SignalKind { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Buy => write!(f, "BUY"), - Self::Sell => write!(f, "SELL"), - } - } -} - -impl std::fmt::Display for SignalParameter { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Lim => write!(f, "LIM"), - Self::Stl => write!(f, "STL"), - Self::Tap => write!(f, "TAP"), - Self::Chk => write!(f, "CHK"), - } - } -} diff --git a/src/engine/engine.rs b/src/engine/engine.rs index d186484..04f9a8d 100644 --- a/src/engine/engine.rs +++ b/src/engine/engine.rs @@ -1,6 +1,6 @@ use std::sync::Arc; -use pulse_wire::terminal::ActivePosition; +use pulse_wire::general::Position; use tokio::{sync::Mutex, task::JoinHandle}; use crate::{ @@ -90,7 +90,7 @@ impl Engine { state .asset_positions .into_iter() - .map(|position| ActivePosition { + .map(|position| Position { symbol: position.position.coin, size: position.position.szi.as_f64(), entry_price: position diff --git a/src/engine/fetch.rs b/src/engine/fetch.rs index 9e91a79..8179732 100644 --- a/src/engine/fetch.rs +++ b/src/engine/fetch.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; -use pulse_wire::terminal::WatchListItem; +use pulse_wire::terminal::MarketItem; use serde_json::Value; fn number(value: &Value, field: &str) -> Result { @@ -15,7 +15,7 @@ fn number(value: &Value, field: &str) -> Result { pub async fn fetch_watch_list( client: &hypersdk::hypercore::HttpClient, symbols: &[String], -) -> Result, String> { +) -> Result, String> { let response = client .meta_and_asset_ctxs(None) .await @@ -63,7 +63,7 @@ pub async fn fetch_watch_list( by_symbol.insert( symbol, - WatchListItem { + MarketItem { symbol: symbol.to_owned(), price, trend: ((price / previous_day_price) - 1.0) * 100.0, diff --git a/src/engine/terminal.rs b/src/engine/terminal.rs index 4e3a503..72666c2 100644 --- a/src/engine/terminal.rs +++ b/src/engine/terminal.rs @@ -3,10 +3,7 @@ use std::{ sync::{Arc, Weak}, }; -use pulse_wire::{ - PulseWire, - terminal::{EventLog, LogKind, TerminalClientMessage}, -}; +use pulse_wire::{PulseWire, general::{EventLog, LogKind}, terminal::TerminalClientMessage}; use tokio::{ io::{AsyncReadExt, AsyncWriteExt}, net::{ diff --git a/src/terminal/formatting.rs b/src/terminal/formatting.rs index 223c31b..3495220 100644 --- a/src/terminal/formatting.rs +++ b/src/terminal/formatting.rs @@ -1,8 +1,9 @@ use pulse_wire::terminal::{ - ActivePosition, Alert, AlertLevel, EventLog, InspectItem, InspectTarget, LogKind, - MarketOverview, Signal, SignalKind, Status, WatchListItem, + Alert, AlertLevel, InspectItem, InspectTarget, MarketItem, MarketOverview, Status, }; +use pulse_wire::general::{Direction, EventLog, LogKind, Position, Signal}; + pub trait Formatted { fn get_formatted(&self) -> Vec; } @@ -64,19 +65,18 @@ impl Formatted for EventLog { impl Formatted for Signal { fn get_formatted(&self) -> Vec { vec![ - if matches!(self.kind, SignalKind::Buy) { + if matches!(self.kind, Direction::Buy) { format!("\x1b[32m{}\x1b[0m", self.kind) } else { format!("\x1b[31m{}\x1b[0m", self.kind) }, format!("\x1b[35m{}\x1b[0m", self.symbol), - format!("\x1b[34m{}\x1b[0m", self.param), format_f64(self.price), ] } } -impl Formatted for WatchListItem { +impl Formatted for MarketItem { fn get_formatted(&self) -> Vec { vec![ format!("\x1b[35m{}\x1b[0m", self.symbol), @@ -95,7 +95,7 @@ impl Formatted for WatchListItem { } } -impl Formatted for ActivePosition { +impl Formatted for Position { fn get_formatted(&self) -> Vec { vec![ format!("\x1b[35m{}\x1b[0m", self.symbol), diff --git a/src/terminal/main.rs b/src/terminal/main.rs index 61697e0..fa224d0 100644 --- a/src/terminal/main.rs +++ b/src/terminal/main.rs @@ -21,8 +21,9 @@ use pulse_ui::{ use crate::formatting::{Formatted, apply_padding}; -use pulse_wire::terminal::{ - ActivePosition, EventLog, InspectTarget, MarketOverview, Signal, Status, WatchListItem, +use pulse_wire::{ + general::{EventLog, Position, Signal}, + terminal::{InspectTarget, MarketItem, MarketOverview, Status}, }; pub struct PulseTradeApp { @@ -34,8 +35,8 @@ pub struct PulseTradeApp { market_overview: State>, status: State>, - watch_list: State>, - active_positions: State>, + watch_list: State>, + active_positions: State>, logs: State>, signals: State>, inspect: State, @@ -130,15 +131,19 @@ impl App for PulseTradeApp { ( LayoutItem::Widget(Size::Flex(1)), Box::new( - advanced_draw(&self.scroll, 1, "ACTIVE POSITIONS", &self.active_positions) - .await, + advanced_draw(&self.scroll, 1, "POSITIONS", &self.active_positions).await, ), ), ( LayoutItem::Widget(Size::Flex(1)), Box::new( - advanced_option_draw(&self.scroll, 2, "MARKET OVERVIEW", &self.market_overview) - .await, + advanced_option_draw( + &self.scroll, + 2, + "MARKET OVERVIEW", + &self.market_overview, + ) + .await, ), ), ]), From f615d8dfbbccfdd9f841a3eb42f702c3794fc676 Mon Sep 17 00:00:00 2001 From: Klesti Selimaj Date: Sat, 25 Jul 2026 04:30:58 +0200 Subject: [PATCH 8/8] typed Syntax and improved formatting --- pulse-wire/src/general.rs | 18 +++---- pulse-wire/src/lib.rs | 9 +++- pulse-wire/src/terminal.rs | 7 +-- pulse-wire/src/units.rs | 98 ++++++++++++++++++++++++++++++++++++++ src/engine/engine.rs | 13 +++-- src/engine/fetch.rs | 11 +++-- src/terminal/formatting.rs | 78 +++++------------------------- 7 files changed, 147 insertions(+), 87 deletions(-) create mode 100644 pulse-wire/src/units.rs diff --git a/pulse-wire/src/general.rs b/pulse-wire/src/general.rs index d7e88d9..7f286a4 100644 --- a/pulse-wire/src/general.rs +++ b/pulse-wire/src/general.rs @@ -1,6 +1,9 @@ use pulse_macros::pwp; -use crate::PulseWire; +use crate::{ + PulseWire, + units::{Symbol, USD}, +}; #[pwp] pub enum MarketTrend { @@ -28,9 +31,9 @@ pub struct Signal { pub kind: Direction, pub confidence: f32, pub size: f64, - pub price: f64, - pub take_profit: f64, - pub take_stop_loss: f64, + pub price: USD, + pub take_profit: USD, + pub stop_loss: USD, } #[pwp] @@ -50,10 +53,10 @@ pub struct EventLog { #[pwp] pub struct Position { - pub symbol: String, + pub symbol: Symbol, pub size: f64, - pub entry_price: f64, - pub profit: f64, + pub entry_price: USD, + pub profit: USD, } impl std::fmt::Display for Direction { @@ -65,7 +68,6 @@ impl std::fmt::Display for Direction { } } - impl std::fmt::Display for MarketTrend { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { diff --git a/pulse-wire/src/lib.rs b/pulse-wire/src/lib.rs index 168b737..fba6f5d 100644 --- a/pulse-wire/src/lib.rs +++ b/pulse-wire/src/lib.rs @@ -1,8 +1,15 @@ #[cfg(target_os = "macos")] use std::path::PathBuf; -pub mod terminal; pub mod general; +pub mod terminal; +pub mod units; + +pub mod prelude { + pub use crate::general::*; + pub use crate::terminal::*; + pub use crate::units::*; +} pub fn server_path() -> PathBuf { PathBuf::from("/tmp/pulse-engine.sock") diff --git a/pulse-wire/src/terminal.rs b/pulse-wire/src/terminal.rs index 4a4f1ff..79d901b 100644 --- a/pulse-wire/src/terminal.rs +++ b/pulse-wire/src/terminal.rs @@ -1,6 +1,7 @@ use crate::{ PulseWire, general::{EventLog, MarketTrend, Position, Signal, Volatility}, + units::{Symbol, USD}, }; use pulse_macros::pwp; @@ -36,10 +37,10 @@ pub enum TerminalClientMessage { #[pwp] pub struct MarketItem { - pub symbol: String, - pub price: f64, + pub symbol: Symbol, + pub price: USD, pub trend: f64, - pub volume_24h: f64, + pub volume_24h: USD, } #[pwp] diff --git a/pulse-wire/src/units.rs b/pulse-wire/src/units.rs new file mode 100644 index 0000000..b43000a --- /dev/null +++ b/pulse-wire/src/units.rs @@ -0,0 +1,98 @@ +use crate::PulseWire; + +#[derive(Debug, Clone)] +pub struct Symbol(pub String); + +#[derive(Debug, Clone, Copy)] +pub struct USD(pub f64); + +impl std::fmt::Display for Symbol { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "\x1b[35m{}\x1b[0m", self.0) + } +} + +impl std::fmt::Display for USD { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if self.0 > 0.0 { + write!(f, "\x1b[32m${}\x1b[0m", format_f64(self.0)) + } else { + write!(f, "\x1b[31m${}\x1b[0m", format_f64(self.0)) + } + } +} + +impl PulseWire for Symbol { + fn from_com(com: &mut Vec) -> Self { + Self(String::from_com(com)) + } + + fn to_com(&self) -> Vec { + self.0.to_com() + } +} + +impl PulseWire for USD { + fn from_com(com: &mut Vec) -> Self { + Self(f64::from_com(com)) + } + + fn to_com(&self) -> Vec { + self.0.to_com() + } +} + +pub fn format_f64(value: f64) -> String { + let abs = value.abs(); + + let (divisor, suffix) = if abs >= 1_000_000_000.0 { + (1_000_000_000.0, "B") + } else if abs >= 1_000_000.0 { + (1_000_000.0, "M") + } else if abs >= 1_000.0 { + (1_000.0, "K") + } else { + (1.0, "") + }; + + if divisor != 1.0 { + let formatted = value / divisor; + + // Remove unnecessary trailing zeros + let s = format!("{:.2}", formatted) + .trim_end_matches('0') + .trim_end_matches('.') + .to_string(); + + return format!("{}{}", s, suffix); + } + + let val = format!("{:.3}", value); + let parts: Vec<&str> = val.split('.').collect(); + + let int = parts[0].to_string(); + let negative = int.starts_with('-'); + + let start = if negative { 1 } else { 0 }; + let mut result = String::new(); + + for (i, c) in int[start..].chars().rev().enumerate() { + if i > 0 && i % 3 == 0 { + result.push(','); + } + result.push(c); + } + + let mut formatted: String = result.chars().rev().collect(); + + if negative { + formatted.insert(0, '-'); + } + + if parts.len() > 1 { + formatted.push('.'); + formatted.push_str(parts[1]); + } + + formatted +} diff --git a/src/engine/engine.rs b/src/engine/engine.rs index 04f9a8d..9b3dfe7 100644 --- a/src/engine/engine.rs +++ b/src/engine/engine.rs @@ -1,6 +1,9 @@ use std::sync::Arc; -use pulse_wire::general::Position; +use pulse_wire::{ + general::Position, + units::{Symbol, USD}, +}; use tokio::{sync::Mutex, task::JoinHandle}; use crate::{ @@ -91,14 +94,14 @@ impl Engine { .asset_positions .into_iter() .map(|position| Position { - symbol: position.position.coin, + symbol: Symbol(position.position.coin), size: position.position.szi.as_f64(), - entry_price: position + entry_price: USD(position .position .entry_px .map(|px| px.as_f64()) - .unwrap_or(0.0), - profit: position.position.unrealized_pnl.as_f64(), + .unwrap_or(0.0)), + profit: USD(position.position.unrealized_pnl.as_f64()), }) .collect(), ), diff --git a/src/engine/fetch.rs b/src/engine/fetch.rs index 8179732..d09be91 100644 --- a/src/engine/fetch.rs +++ b/src/engine/fetch.rs @@ -1,6 +1,9 @@ use std::collections::HashMap; -use pulse_wire::terminal::MarketItem; +use pulse_wire::{ + terminal::MarketItem, + units::{Symbol, USD}, +}; use serde_json::Value; fn number(value: &Value, field: &str) -> Result { @@ -64,10 +67,10 @@ pub async fn fetch_watch_list( by_symbol.insert( symbol, MarketItem { - symbol: symbol.to_owned(), - price, + symbol: Symbol(symbol.to_owned()), + price: USD(price), + volume_24h: USD(volume_24h), trend: ((price / previous_day_price) - 1.0) * 100.0, - volume_24h, }, ); } diff --git a/src/terminal/formatting.rs b/src/terminal/formatting.rs index 3495220..6a21e0a 100644 --- a/src/terminal/formatting.rs +++ b/src/terminal/formatting.rs @@ -3,6 +3,7 @@ use pulse_wire::terminal::{ }; use pulse_wire::general::{Direction, EventLog, LogKind, Position, Signal}; +use pulse_wire::units::format_f64; pub trait Formatted { fn get_formatted(&self) -> Vec; @@ -71,7 +72,7 @@ impl Formatted for Signal { format!("\x1b[31m{}\x1b[0m", self.kind) }, format!("\x1b[35m{}\x1b[0m", self.symbol), - format_f64(self.price), + self.price.to_string(), ] } } @@ -79,9 +80,9 @@ impl Formatted for Signal { impl Formatted for MarketItem { fn get_formatted(&self) -> Vec { vec![ - format!("\x1b[35m{}\x1b[0m", self.symbol), - format!("${}", format_f64(self.price)), - format!("${}", format_f64(self.volume_24h)), + self.symbol.to_string(), + self.price.to_string(), + self.volume_24h.to_string(), format!( "{} {}", if self.trend.is_sign_positive() { @@ -98,17 +99,17 @@ impl Formatted for MarketItem { impl Formatted for Position { fn get_formatted(&self) -> Vec { vec![ - format!("\x1b[35m{}\x1b[0m", self.symbol), + self.symbol.to_string(), format_f64(self.size), - format_f64(self.entry_price), + self.entry_price.to_string(), format!( - "{} {}", - if self.profit.is_sign_positive() { - "\x1b[32m▲\x1b[0m" + "{}{}\x1b[0m", + if self.profit.0.is_sign_positive() { + "\x1b[32m" } else { - "\x1b[31m▼\x1b[0m" + "\x1b[31m" }, - format_f64(self.profit.abs()) + self.profit, ), ] } @@ -206,58 +207,3 @@ pub fn apply_padding(mut items: Vec) -> Vec { items } - -pub fn format_f64(value: f64) -> String { - let abs = value.abs(); - - let (divisor, suffix) = if abs >= 1_000_000_000.0 { - (1_000_000_000.0, "B") - } else if abs >= 1_000_000.0 { - (1_000_000.0, "M") - } else if abs >= 1_000.0 { - (1_000.0, "K") - } else { - (1.0, "") - }; - - if divisor != 1.0 { - let formatted = value / divisor; - - // Remove unnecessary trailing zeros - let s = format!("{:.2}", formatted) - .trim_end_matches('0') - .trim_end_matches('.') - .to_string(); - - return format!("{}{}", s, suffix); - } - - let val = format!("{:.3}", value); - let parts: Vec<&str> = val.split('.').collect(); - - let int = parts[0].to_string(); - let negative = int.starts_with('-'); - - let start = if negative { 1 } else { 0 }; - let mut result = String::new(); - - for (i, c) in int[start..].chars().rev().enumerate() { - if i > 0 && i % 3 == 0 { - result.push(','); - } - result.push(c); - } - - let mut formatted: String = result.chars().rev().collect(); - - if negative { - formatted.insert(0, '-'); - } - - if parts.len() > 1 { - formatted.push('.'); - formatted.push_str(parts[1]); - } - - formatted -}