diff --git a/Cargo.lock b/Cargo.lock index 609c1a1..44c8c16 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,18 +2,62 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + [[package]] name = "bitflags" version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "cc" +version = "1.2.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17dd265a7d0f31ef544e1b20e03add05d3b45b491b633b10d67145d2acc1a38" +dependencies = [ + "find-msvc-tools", + "shlex", +] + [[package]] name = "cfg-if" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + [[package]] name = "convert_case" version = "0.10.0" @@ -23,6 +67,12 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + [[package]] name = "crossterm" version = "0.29.0" @@ -91,6 +141,71 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "js-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + [[package]] name = "libc" version = "0.2.186" @@ -136,6 +251,21 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + [[package]] name = "parking_lot" version = "0.12.5" @@ -178,6 +308,8 @@ dependencies = [ name = "pulse-trader" version = "0.1.0-alpha.0" dependencies = [ + "chrono", + "crossterm", "pulse-ui", "tokio", ] @@ -230,6 +362,12 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + [[package]] name = "scopeguard" version = "1.2.0" @@ -242,6 +380,12 @@ version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + [[package]] name = "signal-hook" version = "0.3.18" @@ -273,6 +417,12 @@ dependencies = [ "libc", ] +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + [[package]] name = "smallvec" version = "1.15.2" @@ -329,6 +479,51 @@ version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +[[package]] +name = "wasm-bindgen" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" +dependencies = [ + "unicode-ident", +] + [[package]] name = "winapi" version = "0.3.9" @@ -351,12 +546,65 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" version = "0.61.2" diff --git a/Cargo.toml b/Cargo.toml index 772db70..4ea732d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,8 +4,10 @@ version = "0.1.0-alpha.0" edition = "2024" [dependencies] +chrono = "0.4.45" pulse-ui = { workspace = true } tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } +crossterm = { workspace = true } [workspace] members = ["pulse-ui"] diff --git a/pulse-ui/src/widget/input.rs b/pulse-ui/src/widget/input.rs index 051763e..f774d94 100644 --- a/pulse-ui/src/widget/input.rs +++ b/pulse-ui/src/widget/input.rs @@ -1,3 +1,53 @@ +#[cfg(target_os = "macos")] +mod platform { + use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; + + pub fn is_backspace(event: &KeyEvent) -> bool { + matches!(event.code, KeyCode::Backspace) + || matches!( + event, + KeyEvent { + code: KeyCode::Char('h'), + modifiers, + .. + } if modifiers.contains(KeyModifiers::CONTROL) + ) + } + + pub fn is_delete(event: &KeyEvent) -> bool { + matches!(event.code, KeyCode::Delete) + || matches!( + event, + KeyEvent { + code: KeyCode::Char('d'), + modifiers, + .. + } if modifiers.contains(KeyModifiers::CONTROL) + ) + } +} + +#[cfg(not(target_os = "macos"))] +mod platform { + use crossterm::event::{KeyCode, KeyEvent}; + + pub fn is_backspace(event: &KeyEvent) -> bool { + matches!(event.code, KeyCode::Backspace) + || matches!( + event, + KeyEvent { + code: KeyCode::Char('h'), + modifiers, + .. + } if modifiers.contains(KeyModifiers::CONTROL) + ) + } + + pub fn is_delete(event: &KeyEvent) -> bool { + matches!(event.code, KeyCode::Delete) + } +} + use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers}; use crate::widget::Widget; @@ -22,7 +72,12 @@ pub struct Input<'a>(pub &'a str, pub &'a InputState); impl<'a> Widget for Input<'a> { fn render(&self, scope: &mut crate::render::RenderScope) { let mut text = self.1.text.clone(); - text.insert_str(self.1.cursor, "❘"); + if self.1.cursor == text.len() { + text.insert_str(self.1.cursor, "\x1b[47m \x1b[0m"); + } else { + text.insert_str(self.1.cursor + 1, "\x1b[0m"); + text.insert_str(self.1.cursor, "\x1b[47m\x1b[30m"); + } scope.draw_text(0, format!("{}{text}", self.0)); } } @@ -30,6 +85,34 @@ impl<'a> Widget for Input<'a> { impl InputState { pub fn handle_event(&mut self, event: &Event) -> bool { match event { + Event::Key(key) if platform::is_backspace(key) => { + if self.cursor > 0 { + let prev = self.text[..self.cursor] + .char_indices() + .last() + .map(|(i, _)| i) + .unwrap(); + + self.text.remove(prev); + self.cursor = prev; + } + true + } + + Event::Key(key) if platform::is_delete(key) => { + if self.cursor < self.text.len() { + let prev = self.text[..self.cursor + 1] + .char_indices() + .last() + .map(|(i, _)| i) + .unwrap(); + + self.text.remove(prev); + self.cursor = prev; + } + true + } + Event::Key(KeyEvent { code: KeyCode::Left, .. @@ -50,58 +133,6 @@ impl InputState { true } - Event::Key(KeyEvent { - code: KeyCode::Backspace, - .. - }) => { - if self.cursor > 0 { - let prev = self.text[..self.cursor] - .char_indices() - .last() - .map(|(i, _)| i) - .unwrap(); - - self.text.remove(prev); - self.cursor = prev; - } - true - } - - Event::Key(KeyEvent { - code: KeyCode::Char('h'), - modifiers, - .. - }) if modifiers.contains(KeyModifiers::CONTROL) => { - if self.cursor > 0 { - let prev = self.text[..self.cursor] - .char_indices() - .last() - .map(|(i, _)| i) - .unwrap(); - - self.text.remove(prev); - self.cursor = prev; - } - true - } - - Event::Key(KeyEvent { - code: KeyCode::Delete, - .. - }) => { - if self.cursor < self.text.len() { - let prev = self.text[..self.cursor+1] - .char_indices() - .last() - .map(|(i, _)| i) - .unwrap(); - - self.text.remove(prev); - self.cursor = prev; - } - true - } - Event::Key(KeyEvent { code: KeyCode::Char(c), modifiers, diff --git a/pulse-ui/src/widget/spaced.rs b/pulse-ui/src/widget/spaced.rs index 8e28ce3..4623bfa 100644 --- a/pulse-ui/src/widget/spaced.rs +++ b/pulse-ui/src/widget/spaced.rs @@ -67,3 +67,37 @@ impl Widget for SpacedColumns { } } } + +pub struct SpacedRows2(pub Vec<(LayoutItem, Box)>); + +impl Widget for SpacedRows2 { + fn render(&self, scope: &mut crate::render::RenderScope) { + let alloc = scope.rect.allocate(&crate::layout::LayoutItem::Rows { + unit: Percent(100), + items: self.0.iter().map(|v| v.0.clone()).collect(), + }); + + for i in 0..alloc.widgets.len() { + let item = &self.0[i]; + + alloc.draw(i, &*item.1); + } + } +} + +pub struct SpacedColumns2(pub Vec<(LayoutItem, Box)>); + +impl Widget for SpacedColumns2 { + fn render(&self, scope: &mut crate::render::RenderScope) { + let alloc = scope.rect.allocate(&crate::layout::LayoutItem::Columns { + unit: Percent(100), + items: self.0.iter().map(|v| v.0.clone()).collect(), + }); + + for i in 0..alloc.widgets.len() { + let item = &self.0[i]; + + alloc.draw(i, &*item.1); + } + } +} diff --git a/src/command.rs b/src/command.rs new file mode 100644 index 0000000..352f1a1 --- /dev/null +++ b/src/command.rs @@ -0,0 +1,29 @@ +use crate::{PulseTradeApp, types::EventLog}; + +impl PulseTradeApp { + pub async fn execute_command(&mut self, ctx: &pulse_ui::state::Context, command: &str) { + if command.is_empty() { + return; + } + + let (command, _args) = if let Some((command, args)) = command.split_once(" ") { + (command, args.split(" ").collect()) + } else { + (command, Vec::new()) + }; + + match command { + "exit" | "quit" | "leave" => { + ctx.close().await; + } + + _ => { + self.logs.lock().await.push(EventLog { + kind: crate::types::LogKind::Err, + name: "cmd", + message: format!("Command '{}' not found", command), + }); + } + } + } +} diff --git a/src/formatting.rs b/src/formatting.rs index 00b677d..d8b1557 100644 --- a/src/formatting.rs +++ b/src/formatting.rs @@ -1,21 +1,85 @@ use crate::types::{ - Account, ActivePosition, EventLog, LogKind, Signal, SignalKind, System, WatchListItem, + ActivePosition, Alert, AlertLevel, EventLog, InspectTarget, LogKind, MarketOverview, Signal, + SignalKind, Status, WatchListItem, }; pub trait Formatted { fn get_formatted(&self) -> Vec; } +impl Formatted for InspectTarget { + fn get_formatted(&self) -> Vec { + match self { + Self::None => vec!["\x1b[2mnothing to inspect\x1b[0m".to_string()], + + Self::Symbol(boxed) => { + let (watch, overview) = &**boxed; + vec![ + Property("Symbol", format!("\x1b[35m{}\x1b[0m", watch.symbol)), + Property("Price", format!("\x1b[96m{}\x1b[0m", format_f64(watch.price))), + Property("Trend", format!("{}", format_f64(watch.trend))), + Property("Market", format!("{}", overview.trend)), + Property("Volatility", format!("{}", overview.volatility)), + Property( + "Pressure", + format!("{:+.2}%", overview.pressure * 100.0), + ), + Property("Alerts", format!("{}", overview.alerts.len())), + ] + .get_formatted() + } + + Self::Position(pos) => vec![ + Property("Symbol", format!("\x1b[35m{}\x1b[0m", pos.symbol)), + Property("Profit", format!("{:+.2}", pos.profit)), + Property("Amount", format!("{}", pos.amount)), + ] + .get_formatted(), + + Self::Signal(sig) => vec![ + Property("Type", format!("{}", sig.kind)), + Property("Symbol", format!("\x1b[35m{}\x1b[0m", sig.symbol)), + Property("Parameter", format!("{}", sig.param)), + Property("Price", format!("\x1b[96m{}\x1b[0m", format_f64(sig.price))), + ] + .get_formatted(), + + Self::Alert(alert) => vec![ + Property("Level", format!("{}", alert.level)), + Property("Message", alert.message.clone()), + ] + .get_formatted(), + } + } +} + +impl Formatted for Alert { + fn get_formatted(&self) -> Vec { + vec![ + format!( + "{}{}\x1b[0m", + match &self.level { + AlertLevel::Low => "\x1b[34m", + AlertLevel::Medium => "\x1b[33m", + AlertLevel::High => "\x1b[31m", + }, + self.level + ), + self.message.clone(), + ] + } +} + impl Formatted for EventLog { fn get_formatted(&self) -> Vec { vec![ format!( - "{}{:?}\x1b[0m", + "{}{}\x1b[0m", match &self.kind { - LogKind::INFO => "\x1b[34m", - LogKind::DEBUG => "\x1b[36m", - LogKind::ERR => "\x1b[31m", - LogKind::WARN => "\x1b[33m", + LogKind::Info => "\x1b[34m", + LogKind::Debug => "\x1b[36m", + LogKind::Err => "\x1b[31m", + LogKind::Warn => "\x1b[33m", }, self.kind ), @@ -28,13 +92,13 @@ impl Formatted for EventLog { impl Formatted for Signal { fn get_formatted(&self) -> Vec { vec![ - if matches!(self.kind, SignalKind::BUY) { - format!("\x1b[32m{:?}\x1b[0m", self.kind) + if matches!(self.kind, SignalKind::Buy) { + format!("\x1b[32m{}\x1b[0m", self.kind) } else { - format!("\x1b[31m{:?}\x1b[0m", self.kind) + format!("\x1b[31m{}\x1b[0m", self.kind) }, format!("\x1b[35m{}\x1b[0m", self.symbol), - format!("\x1b[34m{:?}\x1b[0m", self.param), + format!("\x1b[34m{}\x1b[0m", self.param), format_f64(self.price), ] } @@ -76,22 +140,27 @@ impl Formatted for ActivePosition { } } -pub struct Property(&'static str, String); +struct Property(&'static str, String); -impl Formatted for Account { +impl Formatted for MarketOverview { fn get_formatted(&self) -> Vec { vec![ - Property("Equity", format_f64(self.equity)), - Property("Liquid", format_f64(self.liquid)), - Property("Unreal", format_f64(self.unreal)), - Property("Realized", format_f64(self.realized)), - Property("Margin", format_f64(self.margin)), + Property("TREND", format!("{}", self.trend)), + Property("VOLATILITY", format!("{}", self.volatility)), + Property( + "PRESSURE", + if self.pressure.is_sign_positive() { + format!("\x1b[32m{:.3}\x1b[0m", self.pressure) + } else { + format!("\x1b[31m{:.3}\x1b[0m", self.pressure) + }, + ), ] .get_formatted() } } -impl Formatted for System { +impl Formatted for Status { fn get_formatted(&self) -> Vec { vec![ Property("Feed", format!("{:?}", self.feed)), diff --git a/src/main.rs b/src/main.rs index d990a7c..1fd204c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,15 +1,17 @@ +pub mod command; pub mod formatting; pub mod types; use std::any::Any; +use chrono::{Local, Utc}; use pulse_ui::{ App, layout::{LayoutItem, layout}, state::{Refresh, State}, unit::Size, widget::{ - align::Center, + align::End, input::{Input, InputState}, outline::{Outline, VLine}, spaced::SpacedColumns, @@ -18,7 +20,10 @@ use pulse_ui::{ use crate::{ formatting::{Formatted, apply_padding}, - types::{Account, ActivePosition, EventLog, Signal, System, WatchListItem}, + types::{ + ActivePosition, Alert, EventLog, InspectTarget, MarketOverview, Signal, Status, + WatchListItem, + }, }; pub struct PulseTradeApp { @@ -27,12 +32,13 @@ pub struct PulseTradeApp { active_positions: State>, logs: State>, signals: State>, - account: State, - system: State, + market_overview: State, + status: State, + inspect: State, } impl App for PulseTradeApp { - async fn init(&mut self, ctx: &pulse_ui::state::Context) { + async fn init(&mut self, _ctx: &pulse_ui::state::Context) { let mut watch_list = self.watch_list.lock().await; watch_list.push(WatchListItem { @@ -82,25 +88,50 @@ impl App for PulseTradeApp { let mut signals = self.signals.lock().await; signals.push(Signal { - kind: types::SignalKind::BUY, + kind: types::SignalKind::Buy, symbol: "BTC".to_string(), - param: types::SignalParameter::LIM, + param: types::SignalParameter::Lim, price: 118_800.0, }); signals.push(Signal { - kind: types::SignalKind::BUY, + kind: types::SignalKind::Buy, symbol: "BTC".to_string(), - param: types::SignalParameter::TAP, + param: types::SignalParameter::Tap, price: 120_000.0, }); signals.push(Signal { - kind: types::SignalKind::BUY, + kind: types::SignalKind::Buy, symbol: "BTC".to_string(), - param: types::SignalParameter::STL, + param: types::SignalParameter::Stl, price: 118_000.0, }); + + let mut logs = self.logs.lock().await; + + logs.push(EventLog { + kind: types::LogKind::Warn, + name: "pulse.init", + message: "We're still not done yet ;)".to_string(), + }); + + let mut market_overview = self.market_overview.lock().await; + + market_overview.alerts.push(Alert { + level: types::AlertLevel::High, + message: "BTC funding rate elevated".to_string(), + }); + + market_overview.alerts.push(Alert { + level: types::AlertLevel::Medium, + message: "Market volatility increasing".to_string(), + }); + + market_overview.alerts.push(Alert { + level: types::AlertLevel::Low, + message: "ETH volatility returning to normal".to_string(), + }); } async fn update(&mut self, ctx: &pulse_ui::state::Context, event: Box) { @@ -111,10 +142,18 @@ impl App for PulseTradeApp { if let Some(event) = event.downcast_ref() { if self.command.value.lock().await.handle_event(event) { return; + } else if let crossterm::event::Event::Key(key) = event { + if key.code.is_enter() { + let mut command = self.command.lock().await; + let command_text = command.text.clone(); + command.cursor = 0; + command.text.clear(); + + drop(command); + self.execute_command(ctx, command_text.trim()).await; + } } } - - ctx.close().await; } async fn layout(&self) -> pulse_ui::layout::LayoutItem { @@ -147,9 +186,19 @@ impl App for PulseTradeApp { async fn render(&mut self, layout: pulse_ui::layout::Allocation) { layout.draw_frame(0, Outline); - layout.draw(0, format!(" PULSE TRADER v0.1.0")); - layout.draw(1, Center("LIVE".to_string())); - layout.draw(2, Center(format!("14:32:51 UTC"))); + layout.draw( + 0, + format!("  PULSE TRADER v{}", env!("CARGO_PKG_VERSION")), + ); + // layout.draw(1, Center("LIVE".to_string())); + layout.draw( + 2, + End(format!( + "{} UTC ({} Local)", + Utc::now().format("%H:%M"), + Local::now().format("%H:%M") + )), + ); layout.draw_frame(3, VLine); layout.draw_frame(4, VLine); layout.draw_frame(5, VLine); @@ -173,23 +222,20 @@ impl App for PulseTradeApp { .join("\n") )), ), - ( - LayoutItem::Widget(Size::Flex(1)), - Box::new(format![ - " ACCOUNT\n{}", - apply_padding(self.account.lock().await.get_formatted()).join("\n") - ]), - ), + (LayoutItem::Widget(Size::Flex(1)), { + let mo = self.market_overview.lock().await; + Box::new(format!( + " MARKET OVERVIEW\n{}\n\n{}", + apply_padding(mo.get_formatted()).join("\n"), + apply_padding(mo.alerts.get_formatted()).join("\n") + )) + }), ]), ); layout.draw( 4, SpacedColumns(vec![ - ( - LayoutItem::Widget(Size::Flex(1)), - Box::new(vec![" ACTIVE STRATEGIES"].join("\n")), - ), ( LayoutItem::Widget(Size::Flex(1)), Box::new(format!( @@ -199,20 +245,27 @@ impl App for PulseTradeApp { ), ( LayoutItem::Widget(Size::Flex(1)), - Box::new(format![ - " SYSTEM\n{}", - apply_padding(self.system.lock().await.get_formatted()).join("\n") - ]), + Box::new(format!( + " INSPECTOR\n{}", + apply_padding(self.inspect.lock().await.get_formatted()).join("\n") + )), + ), + ( + LayoutItem::Widget(Size::Flex(1)), + Box::new(format!( + " STATUS\n{}", + apply_padding(self.status.lock().await.get_formatted()).join("\n") + )), ), ]), ); layout.draw( 5, - format![ + format!( " EVENT LOGS\n{}", apply_padding(self.logs.lock().await.get_formatted()).join("\n") - ], + ), ); layout.draw(6, Input(" > ", &*self.command.lock().await)); @@ -227,14 +280,14 @@ async fn main() { active_positions: ctx.use_state(Vec::new()), signals: ctx.use_state(Vec::new()), logs: ctx.use_state(Vec::new()), - account: ctx.use_state(Account { - equity: 25_483.21, - liquid: 11_928.43, - unreal: 483.12, - realized: 2_182.49, - margin: 0.0, + inspect: ctx.use_state(InspectTarget::None), + market_overview: ctx.use_state(MarketOverview { + trend: types::MarketTrend::Bullish, + volatility: types::Volatility::High, + pressure: 0.324, + alerts: Vec::new(), }), - system: ctx.use_state(System { + status: ctx.use_state(Status { feed: types::Feed::Connected, exchange: "Binance".to_string(), dex: "DEX SCREENER".to_string(), diff --git a/src/types.rs b/src/types.rs index 758d400..2cde269 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1,21 +1,58 @@ +#[derive(Debug, Clone)] pub struct WatchListItem { pub symbol: String, pub price: f64, pub trend: f64, } +#[derive(Debug, Clone)] pub struct ActivePosition { pub symbol: String, pub profit: f64, pub amount: f64, } -pub struct Account { - pub equity: f64, - pub liquid: f64, - pub unreal: f64, - pub realized: f64, - pub margin: f64, +#[derive(Debug, Clone, Copy)] +pub enum MarketTrend { + Bullish, + Bearish, + Neutral, +} + +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"), + } + } +} + +#[derive(Debug, Clone, Copy)] +pub enum Volatility { + Low, + Medium, + High, +} + +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"), + } + } +} + +#[derive(Debug, Clone)] +pub struct MarketOverview { + pub trend: MarketTrend, + pub volatility: Volatility, + pub pressure: f64, + + pub alerts: Vec, } #[derive(Debug, Clone, Copy)] @@ -26,7 +63,7 @@ pub enum Feed { Failed, } -pub struct System { +pub struct Status { pub feed: Feed, pub exchange: String, pub dex: String, @@ -35,18 +72,39 @@ pub struct System { #[derive(Debug, Clone, Copy)] pub enum SignalKind { - BUY, - SELL, + Buy, + Sell, +} + +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"), + } + } } #[derive(Debug, Clone, Copy)] pub enum SignalParameter { - LIM, - STL, - TAP, - CHK, + Lim, + Stl, + Tap, + Chk, } +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"), + } + } +} + +#[derive(Debug, Clone)] pub struct Signal { pub kind: SignalKind, pub symbol: String, @@ -56,10 +114,21 @@ pub struct Signal { #[derive(Debug, Clone, Copy)] pub enum LogKind { - INFO, - WARN, - ERR, - DEBUG, + Info, + Warn, + Err, + Debug, +} + +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"), + } + } } #[derive(Debug, Clone)] @@ -68,3 +137,35 @@ pub struct EventLog { pub name: &'static str, pub message: String, } + +#[derive(Debug, Clone, Copy)] +pub enum AlertLevel { + High, + Medium, + Low, +} + +impl std::fmt::Display for AlertLevel { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::High => write!(f, "H"), + Self::Medium => write!(f, "M"), + Self::Low => write!(f, "L"), + } + } +} + +#[derive(Debug, Clone)] +pub struct Alert { + pub level: AlertLevel, + pub message: String, +} + +#[derive(Debug, Clone)] +pub enum InspectTarget { + None, + Symbol(Box<(WatchListItem, MarketOverview)>), + Position(ActivePosition), + Signal(Signal), + Alert(Alert), +}