Merge pull request #7 from selimaj-dev/ui

UI
This commit is contained in:
2026-07-15 22:54:53 +02:00
committed by GitHub
8 changed files with 695 additions and 128 deletions
Generated
+248
View File
@@ -2,18 +2,62 @@
# It is not intended for manual editing. # It is not intended for manual editing.
version = 4 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]] [[package]]
name = "bitflags" name = "bitflags"
version = "2.13.0" version = "2.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" 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]] [[package]]
name = "cfg-if" name = "cfg-if"
version = "1.0.4" version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" 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]] [[package]]
name = "convert_case" name = "convert_case"
version = "0.10.0" version = "0.10.0"
@@ -23,6 +67,12 @@ dependencies = [
"unicode-segmentation", "unicode-segmentation",
] ]
[[package]]
name = "core-foundation-sys"
version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]] [[package]]
name = "crossterm" name = "crossterm"
version = "0.29.0" version = "0.29.0"
@@ -91,6 +141,71 @@ dependencies = [
"windows-sys", "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]] [[package]]
name = "libc" name = "libc"
version = "0.2.186" version = "0.2.186"
@@ -136,6 +251,21 @@ dependencies = [
"windows-sys", "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]] [[package]]
name = "parking_lot" name = "parking_lot"
version = "0.12.5" version = "0.12.5"
@@ -178,6 +308,8 @@ dependencies = [
name = "pulse-trader" name = "pulse-trader"
version = "0.1.0-alpha.0" version = "0.1.0-alpha.0"
dependencies = [ dependencies = [
"chrono",
"crossterm",
"pulse-ui", "pulse-ui",
"tokio", "tokio",
] ]
@@ -230,6 +362,12 @@ dependencies = [
"windows-sys", "windows-sys",
] ]
[[package]]
name = "rustversion"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
[[package]] [[package]]
name = "scopeguard" name = "scopeguard"
version = "1.2.0" version = "1.2.0"
@@ -242,6 +380,12 @@ version = "1.0.28"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
[[package]]
name = "shlex"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
[[package]] [[package]]
name = "signal-hook" name = "signal-hook"
version = "0.3.18" version = "0.3.18"
@@ -273,6 +417,12 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "slab"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
[[package]] [[package]]
name = "smallvec" name = "smallvec"
version = "1.15.2" 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" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" 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]] [[package]]
name = "winapi" name = "winapi"
version = "0.3.9" version = "0.3.9"
@@ -351,12 +546,65 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 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]] [[package]]
name = "windows-link" name = "windows-link"
version = "0.2.1" version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" 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]] [[package]]
name = "windows-sys" name = "windows-sys"
version = "0.61.2" version = "0.61.2"
+2
View File
@@ -4,8 +4,10 @@ version = "0.1.0-alpha.0"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
chrono = "0.4.45"
pulse-ui = { workspace = true } pulse-ui = { workspace = true }
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
crossterm = { workspace = true }
[workspace] [workspace]
members = ["pulse-ui"] members = ["pulse-ui"]
+84 -53
View File
@@ -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 crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers};
use crate::widget::Widget; use crate::widget::Widget;
@@ -22,7 +72,12 @@ pub struct Input<'a>(pub &'a str, pub &'a InputState);
impl<'a> Widget for Input<'a> { impl<'a> Widget for Input<'a> {
fn render(&self, scope: &mut crate::render::RenderScope) { fn render(&self, scope: &mut crate::render::RenderScope) {
let mut text = self.1.text.clone(); 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)); scope.draw_text(0, format!("{}{text}", self.0));
} }
} }
@@ -30,6 +85,34 @@ impl<'a> Widget for Input<'a> {
impl InputState { impl InputState {
pub fn handle_event(&mut self, event: &Event) -> bool { pub fn handle_event(&mut self, event: &Event) -> bool {
match event { 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 { Event::Key(KeyEvent {
code: KeyCode::Left, code: KeyCode::Left,
.. ..
@@ -50,58 +133,6 @@ impl InputState {
true 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 { Event::Key(KeyEvent {
code: KeyCode::Char(c), code: KeyCode::Char(c),
modifiers, modifiers,
+34
View File
@@ -67,3 +67,37 @@ impl Widget for SpacedColumns {
} }
} }
} }
pub struct SpacedRows2(pub Vec<(LayoutItem, Box<dyn Widget>)>);
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<dyn Widget>)>);
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);
}
}
}
+29
View File
@@ -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),
});
}
}
}
}
+87 -18
View File
@@ -1,21 +1,85 @@
use crate::types::{ 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 { pub trait Formatted {
fn get_formatted(&self) -> Vec<String>; fn get_formatted(&self) -> Vec<String>;
} }
impl Formatted for InspectTarget {
fn get_formatted(&self) -> Vec<String> {
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<String> {
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 { impl Formatted for EventLog {
fn get_formatted(&self) -> Vec<String> { fn get_formatted(&self) -> Vec<String> {
vec![ vec![
format!( format!(
"{}{:?}\x1b[0m", "{}{}\x1b[0m",
match &self.kind { match &self.kind {
LogKind::INFO => "\x1b[34m", LogKind::Info => "\x1b[34m",
LogKind::DEBUG => "\x1b[36m", LogKind::Debug => "\x1b[36m",
LogKind::ERR => "\x1b[31m", LogKind::Err => "\x1b[31m",
LogKind::WARN => "\x1b[33m", LogKind::Warn => "\x1b[33m",
}, },
self.kind self.kind
), ),
@@ -28,13 +92,13 @@ impl Formatted for EventLog {
impl Formatted for Signal { impl Formatted for Signal {
fn get_formatted(&self) -> Vec<String> { fn get_formatted(&self) -> Vec<String> {
vec![ vec![
if matches!(self.kind, SignalKind::BUY) { if matches!(self.kind, SignalKind::Buy) {
format!("\x1b[32m{:?}\x1b[0m", self.kind) format!("\x1b[32m{}\x1b[0m", self.kind)
} else { } else {
format!("\x1b[31m{:?}\x1b[0m", self.kind) format!("\x1b[31m{}\x1b[0m", self.kind)
}, },
format!("\x1b[35m{}\x1b[0m", self.symbol), 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), 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<String> { fn get_formatted(&self) -> Vec<String> {
vec![ vec![
Property("Equity", format_f64(self.equity)), Property("TREND", format!("{}", self.trend)),
Property("Liquid", format_f64(self.liquid)), Property("VOLATILITY", format!("{}", self.volatility)),
Property("Unreal", format_f64(self.unreal)), Property(
Property("Realized", format_f64(self.realized)), "PRESSURE",
Property("Margin", format_f64(self.margin)), 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() .get_formatted()
} }
} }
impl Formatted for System { impl Formatted for Status {
fn get_formatted(&self) -> Vec<String> { fn get_formatted(&self) -> Vec<String> {
vec![ vec![
Property("Feed", format!("{:?}", self.feed)), Property("Feed", format!("{:?}", self.feed)),
+94 -41
View File
@@ -1,15 +1,17 @@
pub mod command;
pub mod formatting; pub mod formatting;
pub mod types; pub mod types;
use std::any::Any; use std::any::Any;
use chrono::{Local, Utc};
use pulse_ui::{ use pulse_ui::{
App, App,
layout::{LayoutItem, layout}, layout::{LayoutItem, layout},
state::{Refresh, State}, state::{Refresh, State},
unit::Size, unit::Size,
widget::{ widget::{
align::Center, align::End,
input::{Input, InputState}, input::{Input, InputState},
outline::{Outline, VLine}, outline::{Outline, VLine},
spaced::SpacedColumns, spaced::SpacedColumns,
@@ -18,7 +20,10 @@ use pulse_ui::{
use crate::{ use crate::{
formatting::{Formatted, apply_padding}, formatting::{Formatted, apply_padding},
types::{Account, ActivePosition, EventLog, Signal, System, WatchListItem}, types::{
ActivePosition, Alert, EventLog, InspectTarget, MarketOverview, Signal, Status,
WatchListItem,
},
}; };
pub struct PulseTradeApp { pub struct PulseTradeApp {
@@ -27,12 +32,13 @@ pub struct PulseTradeApp {
active_positions: State<Vec<ActivePosition>>, active_positions: State<Vec<ActivePosition>>,
logs: State<Vec<EventLog>>, logs: State<Vec<EventLog>>,
signals: State<Vec<Signal>>, signals: State<Vec<Signal>>,
account: State<Account>, market_overview: State<MarketOverview>,
system: State<System>, status: State<Status>,
inspect: State<InspectTarget>,
} }
impl App for PulseTradeApp { 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; let mut watch_list = self.watch_list.lock().await;
watch_list.push(WatchListItem { watch_list.push(WatchListItem {
@@ -82,25 +88,50 @@ impl App for PulseTradeApp {
let mut signals = self.signals.lock().await; let mut signals = self.signals.lock().await;
signals.push(Signal { signals.push(Signal {
kind: types::SignalKind::BUY, kind: types::SignalKind::Buy,
symbol: "BTC".to_string(), symbol: "BTC".to_string(),
param: types::SignalParameter::LIM, param: types::SignalParameter::Lim,
price: 118_800.0, price: 118_800.0,
}); });
signals.push(Signal { signals.push(Signal {
kind: types::SignalKind::BUY, kind: types::SignalKind::Buy,
symbol: "BTC".to_string(), symbol: "BTC".to_string(),
param: types::SignalParameter::TAP, param: types::SignalParameter::Tap,
price: 120_000.0, price: 120_000.0,
}); });
signals.push(Signal { signals.push(Signal {
kind: types::SignalKind::BUY, kind: types::SignalKind::Buy,
symbol: "BTC".to_string(), symbol: "BTC".to_string(),
param: types::SignalParameter::STL, param: types::SignalParameter::Stl,
price: 118_000.0, 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<dyn Any + Send + Sync>) { async fn update(&mut self, ctx: &pulse_ui::state::Context, event: Box<dyn Any + Send + Sync>) {
@@ -111,10 +142,18 @@ impl App for PulseTradeApp {
if let Some(event) = event.downcast_ref() { if let Some(event) = event.downcast_ref() {
if self.command.value.lock().await.handle_event(event) { if self.command.value.lock().await.handle_event(event) {
return; 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();
ctx.close().await; drop(command);
self.execute_command(ctx, command_text.trim()).await;
}
}
}
} }
async fn layout(&self) -> pulse_ui::layout::LayoutItem { 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) { async fn render(&mut self, layout: pulse_ui::layout::Allocation) {
layout.draw_frame(0, Outline); layout.draw_frame(0, Outline);
layout.draw(0, format!(" PULSE TRADER v0.1.0")); layout.draw(
layout.draw(1, Center("LIVE".to_string())); 0,
layout.draw(2, Center(format!("14:32:51 UTC"))); 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(3, VLine);
layout.draw_frame(4, VLine); layout.draw_frame(4, VLine);
layout.draw_frame(5, VLine); layout.draw_frame(5, VLine);
@@ -173,23 +222,20 @@ impl App for PulseTradeApp {
.join("\n") .join("\n")
)), )),
), ),
( (LayoutItem::Widget(Size::Flex(1)), {
LayoutItem::Widget(Size::Flex(1)), let mo = self.market_overview.lock().await;
Box::new(format![ Box::new(format!(
" ACCOUNT\n{}", " MARKET OVERVIEW\n{}\n\n{}",
apply_padding(self.account.lock().await.get_formatted()).join("\n") apply_padding(mo.get_formatted()).join("\n"),
]), apply_padding(mo.alerts.get_formatted()).join("\n")
), ))
}),
]), ]),
); );
layout.draw( layout.draw(
4, 4,
SpacedColumns(vec![ SpacedColumns(vec![
(
LayoutItem::Widget(Size::Flex(1)),
Box::new(vec![" ACTIVE STRATEGIES"].join("\n")),
),
( (
LayoutItem::Widget(Size::Flex(1)), LayoutItem::Widget(Size::Flex(1)),
Box::new(format!( Box::new(format!(
@@ -199,20 +245,27 @@ impl App for PulseTradeApp {
), ),
( (
LayoutItem::Widget(Size::Flex(1)), LayoutItem::Widget(Size::Flex(1)),
Box::new(format![ Box::new(format!(
" SYSTEM\n{}", " INSPECTOR\n{}",
apply_padding(self.system.lock().await.get_formatted()).join("\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( layout.draw(
5, 5,
format![ format!(
" EVENT LOGS\n{}", " EVENT LOGS\n{}",
apply_padding(self.logs.lock().await.get_formatted()).join("\n") apply_padding(self.logs.lock().await.get_formatted()).join("\n")
], ),
); );
layout.draw(6, Input(" > ", &*self.command.lock().await)); layout.draw(6, Input(" > ", &*self.command.lock().await));
@@ -227,14 +280,14 @@ async fn main() {
active_positions: ctx.use_state(Vec::new()), active_positions: ctx.use_state(Vec::new()),
signals: ctx.use_state(Vec::new()), signals: ctx.use_state(Vec::new()),
logs: ctx.use_state(Vec::new()), logs: ctx.use_state(Vec::new()),
account: ctx.use_state(Account { inspect: ctx.use_state(InspectTarget::None),
equity: 25_483.21, market_overview: ctx.use_state(MarketOverview {
liquid: 11_928.43, trend: types::MarketTrend::Bullish,
unreal: 483.12, volatility: types::Volatility::High,
realized: 2_182.49, pressure: 0.324,
margin: 0.0, alerts: Vec::new(),
}), }),
system: ctx.use_state(System { status: ctx.use_state(Status {
feed: types::Feed::Connected, feed: types::Feed::Connected,
exchange: "Binance".to_string(), exchange: "Binance".to_string(),
dex: "DEX SCREENER".to_string(), dex: "DEX SCREENER".to_string(),
+118 -17
View File
@@ -1,21 +1,58 @@
#[derive(Debug, Clone)]
pub struct WatchListItem { pub struct WatchListItem {
pub symbol: String, pub symbol: String,
pub price: f64, pub price: f64,
pub trend: f64, pub trend: f64,
} }
#[derive(Debug, Clone)]
pub struct ActivePosition { pub struct ActivePosition {
pub symbol: String, pub symbol: String,
pub profit: f64, pub profit: f64,
pub amount: f64, pub amount: f64,
} }
pub struct Account { #[derive(Debug, Clone, Copy)]
pub equity: f64, pub enum MarketTrend {
pub liquid: f64, Bullish,
pub unreal: f64, Bearish,
pub realized: f64, Neutral,
pub margin: 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"),
}
}
}
#[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<Alert>,
} }
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
@@ -26,7 +63,7 @@ pub enum Feed {
Failed, Failed,
} }
pub struct System { pub struct Status {
pub feed: Feed, pub feed: Feed,
pub exchange: String, pub exchange: String,
pub dex: String, pub dex: String,
@@ -35,18 +72,39 @@ pub struct System {
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
pub enum SignalKind { pub enum SignalKind {
BUY, Buy,
SELL, 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)] #[derive(Debug, Clone, Copy)]
pub enum SignalParameter { pub enum SignalParameter {
LIM, Lim,
STL, Stl,
TAP, Tap,
CHK, 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 struct Signal {
pub kind: SignalKind, pub kind: SignalKind,
pub symbol: String, pub symbol: String,
@@ -56,10 +114,21 @@ pub struct Signal {
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
pub enum LogKind { pub enum LogKind {
INFO, Info,
WARN, Warn,
ERR, Err,
DEBUG, 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)] #[derive(Debug, Clone)]
@@ -68,3 +137,35 @@ pub struct EventLog {
pub name: &'static str, pub name: &'static str,
pub message: String, 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),
}