diff --git a/Cargo.lock b/Cargo.lock index fc34e9b..8495645 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3907,6 +3907,7 @@ name = "pulse-wire" version = "0.1.0-alpha.0" dependencies = [ "pulse-macros", + "serde", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index f70095e..8780172 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,7 +21,7 @@ hypersdk = "0.2.14" serde_json = "1" rand = "0.8.7" toml = "1.1.3" -serde = { version = "1.0.229", features = ["serde_derive"] } +serde = { workspace = true } [workspace] members = ["pulse-macros", "pulse-ui", "pulse-wire"] @@ -33,6 +33,7 @@ pulse-wire = { path = "pulse-wire", version = "0.1.0-alpha.0" } tokio = "1.52.3" crossterm = "0.29.0" +serde = { version = "1.0.229", features = ["serde_derive"] } [[bin]] name = "pulse-trader" diff --git a/pulse-wire/Cargo.toml b/pulse-wire/Cargo.toml index 449b94f..81b1d0c 100644 --- a/pulse-wire/Cargo.toml +++ b/pulse-wire/Cargo.toml @@ -5,3 +5,4 @@ edition = "2024" [dependencies] pulse-macros = { workspace = true } +serde = { workspace = true } diff --git a/pulse-wire/src/general.rs b/pulse-wire/src/general.rs index 7f286a4..353cb24 100644 --- a/pulse-wire/src/general.rs +++ b/pulse-wire/src/general.rs @@ -2,7 +2,7 @@ use pulse_macros::pwp; use crate::{ PulseWire, - units::{Symbol, USD}, + units::{Direction, Symbol, USD}, }; #[pwp] @@ -13,16 +13,11 @@ pub enum MarketTrend { } #[pwp] -pub enum Volatility { - Low, - Medium, - High, -} - -#[pwp] -pub enum Direction { - Buy, - Sell, +pub enum LogKind { + Info, + Warn, + Err, + Debug, } #[pwp] @@ -36,14 +31,6 @@ pub struct Signal { pub stop_loss: USD, } -#[pwp] -pub enum LogKind { - Info, - Warn, - Err, - Debug, -} - #[pwp] pub struct EventLog { pub kind: LogKind, @@ -59,15 +46,6 @@ pub struct Position { pub profit: USD, } -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 { @@ -78,15 +56,6 @@ impl std::fmt::Display for MarketTrend { } } -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 { diff --git a/pulse-wire/src/lib.rs b/pulse-wire/src/lib.rs index fba6f5d..689db77 100644 --- a/pulse-wire/src/lib.rs +++ b/pulse-wire/src/lib.rs @@ -2,11 +2,15 @@ use std::path::PathBuf; pub mod general; +pub mod strategy; pub mod terminal; pub mod units; pub mod prelude { + pub use crate::PulseWire; pub use crate::general::*; + pub use crate::server_path; + pub use crate::strategy::*; pub use crate::terminal::*; pub use crate::units::*; } diff --git a/pulse-wire/src/strategy.rs b/pulse-wire/src/strategy.rs new file mode 100644 index 0000000..757ac70 --- /dev/null +++ b/pulse-wire/src/strategy.rs @@ -0,0 +1,25 @@ +use crate::{PulseWire, units::TimeFrame}; +use pulse_macros::pwp; + +#[pwp] +#[derive(serde::Deserialize, serde::Serialize)] +pub struct StrategyManifest { + name: String, + description: String, + author: String, + version: String, + + timeframes: Vec, +} + +#[pwp] +#[derive(serde::Deserialize, serde::Serialize)] +pub struct RiskManifest { + name: String, + description: String, + author: String, + version: String, + + max_loss: u8, + cooldown: TimeFrame, +} diff --git a/pulse-wire/src/terminal.rs b/pulse-wire/src/terminal.rs index 79d901b..5b26c2c 100644 --- a/pulse-wire/src/terminal.rs +++ b/pulse-wire/src/terminal.rs @@ -1,7 +1,8 @@ use crate::{ PulseWire, - general::{EventLog, MarketTrend, Position, Signal, Volatility}, - units::{Symbol, USD}, + general::{EventLog, MarketTrend, Position, Signal}, + strategy::{RiskManifest, StrategyManifest}, + units::{Symbol, TimeFrame, USD, Volatility}, }; use pulse_macros::pwp; @@ -13,8 +14,8 @@ pub enum TerminalServerMessage { // Positions PositionsUpdated(Vec), - // Overview - OverviewUpdated(MarketOverview), + // Strategy + StrategyUpdated(Strategy), // Signals SignalsUpdated(Vec), @@ -86,18 +87,33 @@ pub enum InspectItem { #[pwp] pub struct Status { - feed: Feed, + feed: Mode, exchange: String, dex: String, latency: u16, } #[pwp] -pub enum Feed { - Connected, - Disconnected, - Connecting, - Failed, +pub enum Mode { + Auto, + Manual, +} + +#[pwp] +pub enum ItemState { + Running, + Off, + Error, +} + +#[pwp] +pub struct Strategy { + strategy: StrategyManifest, + risk: RiskManifest, + + mode: Mode, + state: ItemState, + cooldown: TimeFrame, } impl std::fmt::Display for AlertLevel { @@ -109,3 +125,22 @@ impl std::fmt::Display for AlertLevel { } } } + +impl std::fmt::Display for Mode { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Auto => write!(f, "\x1b[96mAUTO\x1b[0m"), + Self::Manual => write!(f, "\x1b[97mMANUAL\x1b[0m"), + } + } +} + +impl std::fmt::Display for ItemState { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Running => write!(f, "\x1b[92mRUNNING\x1b[0m"), + Self::Off => write!(f, "\x1b[90mOFF\x1b[0m"), + Self::Error => write!(f, "\x1b[91mERROR\x1b[0m"), + } + } +} diff --git a/pulse-wire/src/units.rs b/pulse-wire/src/units.rs index b43000a..06e84ae 100644 --- a/pulse-wire/src/units.rs +++ b/pulse-wire/src/units.rs @@ -1,3 +1,5 @@ +use pulse_macros::pwp; + use crate::PulseWire; #[derive(Debug, Clone)] @@ -6,6 +8,55 @@ pub struct Symbol(pub String); #[derive(Debug, Clone, Copy)] pub struct USD(pub f64); +#[pwp] +#[derive(Copy, serde::Deserialize, serde::Serialize)] +pub enum TimeFrame { + #[serde(rename = "1m")] + M1, + #[serde(rename = "3m")] + M3, + #[serde(rename = "5m")] + M5, + #[serde(rename = "15m")] + M15, + #[serde(rename = "30m")] + M30, + + #[serde(rename = "1h")] + H1, + #[serde(rename = "2h")] + H2, + #[serde(rename = "4h")] + H4, + #[serde(rename = "8h")] + H8, + #[serde(rename = "12h")] + H12, + + #[serde(rename = "1d")] + D1, + #[serde(rename = "3d")] + D3, + + #[serde(rename = "1w")] + W1, + #[serde(rename = "1M")] + Month1, +} + +#[pwp] +pub enum Direction { + Buy, + Sell, +} + +#[pwp] +pub enum Volatility { + Low, + Medium, + High, +} + 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) @@ -96,3 +147,46 @@ pub fn format_f64(value: f64) -> String { formatted } + +impl TimeFrame { + pub fn as_str(&self) -> &'static str { + match self { + Self::M1 => "1m", + Self::M3 => "3m", + Self::M5 => "5m", + Self::M15 => "15m", + Self::M30 => "30m", + + Self::H1 => "1h", + Self::H2 => "2h", + Self::H4 => "4h", + Self::H8 => "8h", + Self::H12 => "12h", + + Self::D1 => "1d", + Self::D3 => "3d", + + Self::W1 => "1w", + Self::Month1 => "1M", + } + } +} + +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 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"), + } + } +} diff --git a/src/engine/engine.rs b/src/engine/engine.rs index 9b3dfe7..438b06c 100644 --- a/src/engine/engine.rs +++ b/src/engine/engine.rs @@ -1,15 +1,10 @@ -use std::sync::Arc; - -use pulse_wire::{ - general::Position, - units::{Symbol, USD}, -}; -use tokio::{sync::Mutex, task::JoinHandle}; - use crate::{ store::{accounts::AccountList, config::Config}, terminal::TerminalServer, }; +use pulse_wire::prelude::*; +use std::sync::Arc; +use tokio::{sync::Mutex, task::JoinHandle}; #[derive(Debug, Clone)] pub struct Engine { diff --git a/src/engine/fetch.rs b/src/engine/fetch.rs index d09be91..28a3733 100644 --- a/src/engine/fetch.rs +++ b/src/engine/fetch.rs @@ -1,10 +1,6 @@ -use std::collections::HashMap; - -use pulse_wire::{ - terminal::MarketItem, - units::{Symbol, USD}, -}; +use pulse_wire::prelude::*; use serde_json::Value; +use std::collections::HashMap; fn number(value: &Value, field: &str) -> Result { let raw = value[field] diff --git a/src/engine/terminal.rs b/src/engine/terminal.rs index 72666c2..7de2c44 100644 --- a/src/engine/terminal.rs +++ b/src/engine/terminal.rs @@ -1,9 +1,9 @@ +use crate::engine::Engine; +use pulse_wire::prelude::*; use std::{ collections::HashMap, sync::{Arc, Weak}, }; - -use pulse_wire::{PulseWire, general::{EventLog, LogKind}, terminal::TerminalClientMessage}; use tokio::{ io::{AsyncReadExt, AsyncWriteExt}, net::{ @@ -13,8 +13,6 @@ use tokio::{ sync::Mutex, }; -use crate::engine::Engine; - #[derive(Debug)] pub struct TerminalServer { clients: Mutex>, diff --git a/src/terminal/formatting.rs b/src/terminal/formatting.rs index 6a21e0a..43a6ffc 100644 --- a/src/terminal/formatting.rs +++ b/src/terminal/formatting.rs @@ -1,9 +1,4 @@ -use pulse_wire::terminal::{ - Alert, AlertLevel, InspectItem, InspectTarget, MarketItem, MarketOverview, Status, -}; - -use pulse_wire::general::{Direction, EventLog, LogKind, Position, Signal}; -use pulse_wire::units::format_f64; +use pulse_wire::prelude::*; pub trait Formatted { fn get_formatted(&self) -> Vec; @@ -115,16 +110,18 @@ impl Formatted for Position { } } -struct Property(&'static str, String); +struct Pair<'a>(&'a str, &'a str); + +struct Triple<'a>(&'a str, &'a str, &'a str); impl Formatted for MarketOverview { fn get_formatted(&self) -> Vec { let mut o = vec![ - Property("TREND", format!("{}", self.trend)), - Property("VOLATILITY", format!("{}", self.volatility)), - Property( + Pair("TREND", &format!("{}", self.trend)), + Pair("VOLATILITY", &format!("{}", self.volatility)), + Pair( "PRESSURE", - if self.pressure.is_sign_positive() { + &if self.pressure.is_sign_positive() { format!("\x1b[32m{:.3}\x1b[0m", self.pressure) } else { format!("\x1b[31m{:.3}\x1b[0m", self.pressure) @@ -145,21 +142,50 @@ impl Formatted for MarketOverview { impl Formatted for Status { fn get_formatted(&self) -> Vec { vec![ - Property("Feed", format!("{:?}", self.feed)), - Property("Exchange", self.exchange.clone()), - Property("DEX", self.dex.clone()), - Property("Latency", format!("{} ms", self.latency)), + Pair("Feed", &format!("{:?}", self.feed)), + Pair("Exchange", &self.exchange.clone()), + Pair("DEX", &self.dex.clone()), + Pair("Latency", &format!("{} ms", self.latency)), ] .get_formatted() } } -impl Formatted for Property { +impl<'a> Formatted for Pair<'a> { fn get_formatted(&self) -> Vec { - vec![format!("\x1b[91m{}\x1b[0m", self.0), self.1.clone()] + vec![self.0.to_string(), self.1.to_string()] } } +impl<'a> Formatted for Triple<'a> { + fn get_formatted(&self) -> Vec { + vec![self.0.to_string(), self.1.to_string(), self.2.to_string()] + } +} + +fn visible_len(s: &str) -> usize { + let mut len = 0; + let mut chars = s.chars().peekable(); + + while let Some(c) = chars.next() { + if c == '\x1b' { + if chars.peek() == Some(&'[') { + chars.next(); + + while let Some(c) = chars.next() { + if c.is_ascii_alphabetic() { + break; + } + } + } + } else { + len += c.len_utf8(); + } + } + + len +} + impl Formatted for Vec { fn get_formatted(&self) -> Vec { let mut output = Vec::new(); @@ -176,7 +202,7 @@ impl Formatted for Vec { for row in &rows { for (i, col) in row.iter().enumerate() { - widths[i] = widths[i].max(col.len()); + widths[i] = widths[i].max(visible_len(col)); } } @@ -186,7 +212,8 @@ impl Formatted for Vec { for (col_idx, col) in row.iter().enumerate() { let width = widths[col_idx]; - row_out.push_str(&format!("{: Formatted for Vec { } } +impl Formatted for Strategy { + fn get_formatted(&self) -> Vec { + vec![ + Triple( + "\x1b[2mName\x1b[0m", + "\x1b[2mRisk\x1b[0m", + "\x1b[2mStrat Ver\x1b[0m", + ), + Triple( + &format!("\x1b[97m{}\x1b[0m", self.strategy.name), + &format!("\x1b[93m{}\x1b[0m", self.risk.name), + &format!("\x1b[90m{}\x1b[0m", self.strategy.version), + ), + Triple("", "", ""), + Triple( + "\x1b[2mMode\x1b[0m", + "\x1b[2mState\x1b[0m", + "\x1b[2mRisk Ver\x1b[0m", + ), + Triple( + &self.mode.to_string(), + &self.state.to_string(), + &format!("\x1b[90m{}\x1b[0m", self.risk.version), + ), + Triple("", "", ""), + Triple( + "\x1b[2mCooldown\x1b[0m", + "\x1b[2mTimeframes\x1b[0m", + "\x1b[2mMax loss\x1b[0m", + ), + Triple( + &format!( + "\x1b[96m{:?}\x1b[0m (\x1b[90m{:?} rec\x1b[0m)", + self.cooldown, self.risk.cooldown + ), + &format!("\x1b[96m{:?}\x1b[0m", self.strategy.timeframes), + &format!("\x1b[93m{}%\x1b[0m", self.risk.max_loss), + ), + ] + .get_formatted() + } +} + pub fn apply_padding(mut items: Vec) -> Vec { for item in &mut items { item.insert(0, ' '); diff --git a/src/terminal/main.rs b/src/terminal/main.rs index fa224d0..da08d43 100644 --- a/src/terminal/main.rs +++ b/src/terminal/main.rs @@ -21,10 +21,7 @@ use pulse_ui::{ use crate::formatting::{Formatted, apply_padding}; -use pulse_wire::{ - general::{EventLog, Position, Signal}, - terminal::{InspectTarget, MarketItem, MarketOverview, Status}, -}; +use pulse_wire::prelude::*; pub struct PulseTradeApp { sock: Option, @@ -32,7 +29,7 @@ pub struct PulseTradeApp { command: State, scroll: State>, - market_overview: State>, + strategy: State>, status: State>, watch_list: State>, @@ -136,15 +133,7 @@ impl App for PulseTradeApp { ), ( LayoutItem::Widget(Size::Flex(1)), - Box::new( - advanced_option_draw( - &self.scroll, - 2, - "MARKET OVERVIEW", - &self.market_overview, - ) - .await, - ), + Box::new(advanced_draw(&self.scroll, 2, "SIGNALS", &self.signals).await), ), ]), ); @@ -154,7 +143,9 @@ impl App for PulseTradeApp { SpacedColumns(vec![ ( LayoutItem::Widget(Size::Flex(1)), - Box::new(advanced_draw(&self.scroll, 3, "SIGNALS", &self.signals).await), + Box::new( + advanced_option_draw(&self.scroll, 3, "STRATEGY", &self.strategy).await, + ), ), ( LayoutItem::Widget(Size::Flex(1)), @@ -190,7 +181,32 @@ 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(None), + strategy: ctx.use_state(Some(Strategy { + strategy: StrategyManifest { + name: "Liquidity Sweep".to_string(), + description: + "Detects liquidity grabs around key support and resistance levels." + .to_string(), + author: "Klesty Selimaj".to_string(), + version: "1.0.0".to_string(), + + timeframes: vec![TimeFrame::M5, TimeFrame::M15, TimeFrame::H1], + }, + + risk: RiskManifest { + name: "Aggressive".to_string(), + description: "High-risk profile with larger position sizing.".to_string(), + author: "Klesty Selimaj".to_string(), + version: "1.0.0".to_string(), + + max_loss: 5, + cooldown: TimeFrame::M15, + }, + + mode: Mode::Auto, + state: ItemState::Running, + cooldown: TimeFrame::M15, + })), status: ctx.use_state(None), }) }) diff --git a/src/terminal/terminal.rs b/src/terminal/terminal.rs index 763cae7..468a20d 100644 --- a/src/terminal/terminal.rs +++ b/src/terminal/terminal.rs @@ -1,4 +1,5 @@ -use pulse_wire::{PulseWire, server_path, terminal::TerminalServerMessage}; +use pulse_wire::prelude::*; + use tokio::{ io::{AsyncReadExt, AsyncWriteExt}, net::{ @@ -47,7 +48,7 @@ impl TerminalClient { let active_positions = app.active_positions.clone(); let logs = app.logs.clone(); let signals = app.signals.clone(); - let market_overview = app.market_overview.clone(); + let market_overview = app.strategy.clone(); let status = app.status.clone(); let inspect = app.inspect.clone(); @@ -77,7 +78,7 @@ impl TerminalClient { *active_positions.lock().await = v; } - TerminalServerMessage::OverviewUpdated(v) => { + TerminalServerMessage::StrategyUpdated(v) => { *market_overview.lock().await = Some(v); }