Merge pull request #14 from selimaj-dev/strategy-ui-and-manifest

Strategy UI and manifest
This commit is contained in:
2026-07-25 19:02:42 +02:00
committed by GitHub
14 changed files with 310 additions and 104 deletions
Generated
+1
View File
@@ -3907,6 +3907,7 @@ name = "pulse-wire"
version = "0.1.0-alpha.0"
dependencies = [
"pulse-macros",
"serde",
]
[[package]]
+2 -1
View File
@@ -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"
+1
View File
@@ -5,3 +5,4 @@ edition = "2024"
[dependencies]
pulse-macros = { workspace = true }
serde = { workspace = true }
+6 -37
View File
@@ -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 {
+4
View File
@@ -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::*;
}
+25
View File
@@ -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<TimeFrame>,
}
#[pwp]
#[derive(serde::Deserialize, serde::Serialize)]
pub struct RiskManifest {
name: String,
description: String,
author: String,
version: String,
max_loss: u8,
cooldown: TimeFrame,
}
+45 -10
View File
@@ -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<Position>),
// Overview
OverviewUpdated(MarketOverview),
// Strategy
StrategyUpdated(Strategy),
// Signals
SignalsUpdated(Vec<Signal>),
@@ -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"),
}
}
}
+94
View File
@@ -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"),
}
}
}
+3 -8
View File
@@ -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 {
+2 -6
View File
@@ -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<f64, String> {
let raw = value[field]
+2 -4
View File
@@ -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<HashMap<usize, OwnedWriteHalf>>,
+89 -19
View File
@@ -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<String>;
@@ -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<String> {
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<String> {
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<String> {
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<String> {
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<T: Formatted> Formatted for Vec<T> {
fn get_formatted(&self) -> Vec<String> {
let mut output = Vec::new();
@@ -176,7 +202,7 @@ impl<T: Formatted> Formatted for Vec<T> {
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<T: Formatted> Formatted for Vec<T> {
for (col_idx, col) in row.iter().enumerate() {
let width = widths[col_idx];
row_out.push_str(&format!("{:<width$}", col, width = width));
row_out.push_str(col);
row_out.push_str(&" ".repeat(width.saturating_sub(visible_len(col))));
if col_idx + 1 != row.len() {
row_out.push_str(" ");
@@ -200,6 +227,49 @@ impl<T: Formatted> Formatted for Vec<T> {
}
}
impl Formatted for Strategy {
fn get_formatted(&self) -> Vec<String> {
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<String>) -> Vec<String> {
for item in &mut items {
item.insert(0, ' ');
+32 -16
View File
@@ -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<terminal::TerminalClient>,
@@ -32,7 +29,7 @@ pub struct PulseTradeApp {
command: State<InputState>,
scroll: State<ScrollState<7>>,
market_overview: State<Option<MarketOverview>>,
strategy: State<Option<Strategy>>,
status: State<Option<Status>>,
watch_list: State<Vec<MarketItem>>,
@@ -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),
})
})
+4 -3
View File
@@ -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);
}