Merge pull request #13 from selimaj-dev/accounts-and-positions
Accounts and positions
This commit is contained in:
@@ -0,0 +1,99 @@
|
|||||||
|
use pulse_macros::pwp;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
PulseWire,
|
||||||
|
units::{Symbol, USD},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[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: USD,
|
||||||
|
pub take_profit: USD,
|
||||||
|
pub stop_loss: USD,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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: Symbol,
|
||||||
|
pub size: f64,
|
||||||
|
pub entry_price: USD,
|
||||||
|
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 {
|
||||||
|
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"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,15 @@
|
|||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
pub mod general;
|
||||||
pub mod terminal;
|
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 {
|
pub fn server_path() -> PathBuf {
|
||||||
PathBuf::from("/tmp/pulse-engine.sock")
|
PathBuf::from("/tmp/pulse-engine.sock")
|
||||||
|
|||||||
+38
-140
@@ -1,13 +1,17 @@
|
|||||||
use crate::PulseWire;
|
use crate::{
|
||||||
|
PulseWire,
|
||||||
|
general::{EventLog, MarketTrend, Position, Signal, Volatility},
|
||||||
|
units::{Symbol, USD},
|
||||||
|
};
|
||||||
use pulse_macros::pwp;
|
use pulse_macros::pwp;
|
||||||
|
|
||||||
#[pwp]
|
#[pwp]
|
||||||
pub enum TerminalServerMessage {
|
pub enum TerminalServerMessage {
|
||||||
// WatchList
|
// WatchList
|
||||||
WatchListUpdated(Vec<WatchListItem>),
|
WatchListUpdated(Vec<MarketItem>),
|
||||||
|
|
||||||
// Positions
|
// Positions
|
||||||
PositionsUpdated(Vec<ActivePosition>),
|
PositionsUpdated(Vec<Position>),
|
||||||
|
|
||||||
// Overview
|
// Overview
|
||||||
OverviewUpdated(MarketOverview),
|
OverviewUpdated(MarketOverview),
|
||||||
@@ -19,7 +23,7 @@ pub enum TerminalServerMessage {
|
|||||||
Inspect(InspectTarget),
|
Inspect(InspectTarget),
|
||||||
|
|
||||||
// Status
|
// Status
|
||||||
SetStatus(Status),
|
StatusUpdated(Status),
|
||||||
|
|
||||||
// Logs
|
// Logs
|
||||||
SetLogs(Vec<EventLog>),
|
SetLogs(Vec<EventLog>),
|
||||||
@@ -32,96 +36,19 @@ pub enum TerminalClientMessage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[pwp]
|
#[pwp]
|
||||||
pub struct WatchListItem {
|
pub struct MarketItem {
|
||||||
symbol: String,
|
pub symbol: Symbol,
|
||||||
price: f64,
|
pub price: USD,
|
||||||
trend: f64,
|
pub trend: f64,
|
||||||
volume_24h: f64,
|
pub volume_24h: USD,
|
||||||
}
|
|
||||||
|
|
||||||
#[pwp]
|
|
||||||
pub struct ActivePosition {
|
|
||||||
pub symbol: String,
|
|
||||||
pub size: f64,
|
|
||||||
pub entry_price: f64,
|
|
||||||
pub mark_price: f64,
|
|
||||||
pub profit: f64,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[pwp]
|
|
||||||
pub enum MarketTrend {
|
|
||||||
Bullish,
|
|
||||||
Bearish,
|
|
||||||
Neutral,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[pwp]
|
|
||||||
pub enum Volatility {
|
|
||||||
Low,
|
|
||||||
Medium,
|
|
||||||
High,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pwp]
|
#[pwp]
|
||||||
pub struct MarketOverview {
|
pub struct MarketOverview {
|
||||||
trend: MarketTrend,
|
pub trend: MarketTrend,
|
||||||
volatility: Volatility,
|
pub volatility: Volatility,
|
||||||
pressure: f64,
|
pub pressure: f64,
|
||||||
|
pub alerts: Vec<Alert>,
|
||||||
alerts: Vec<Alert>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[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,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pwp]
|
#[pwp]
|
||||||
@@ -130,13 +57,19 @@ pub enum AlertLevel {
|
|||||||
Medium,
|
Medium,
|
||||||
Low,
|
Low,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pwp]
|
#[pwp]
|
||||||
pub struct Alert {
|
pub struct Alert {
|
||||||
level: AlertLevel,
|
level: AlertLevel,
|
||||||
message: String,
|
message: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[pwp]
|
||||||
|
pub struct Balance {
|
||||||
|
pub asset: String,
|
||||||
|
pub amount: f64,
|
||||||
|
pub value: f64,
|
||||||
|
}
|
||||||
|
|
||||||
#[pwp]
|
#[pwp]
|
||||||
pub enum InspectTarget {
|
pub enum InspectTarget {
|
||||||
None,
|
None,
|
||||||
@@ -151,14 +84,20 @@ pub enum InspectItem {
|
|||||||
F64(f64),
|
F64(f64),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for MarketTrend {
|
#[pwp]
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
pub struct Status {
|
||||||
match self {
|
feed: Feed,
|
||||||
Self::Bullish => write!(f, "Bullish"),
|
exchange: String,
|
||||||
Self::Bearish => write!(f, "Bearish"),
|
dex: String,
|
||||||
Self::Neutral => write!(f, "Neutral"),
|
latency: u16,
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
#[pwp]
|
||||||
|
pub enum Feed {
|
||||||
|
Connected,
|
||||||
|
Disconnected,
|
||||||
|
Connecting,
|
||||||
|
Failed,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for AlertLevel {
|
impl std::fmt::Display for AlertLevel {
|
||||||
@@ -170,44 +109,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"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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<u8>) -> Self {
|
||||||
|
Self(String::from_com(com))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_com(&self) -> Vec<u8> {
|
||||||
|
self.0.to_com()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PulseWire for USD {
|
||||||
|
fn from_com(com: &mut Vec<u8>) -> Self {
|
||||||
|
Self(f64::from_com(com))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_com(&self) -> Vec<u8> {
|
||||||
|
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
|
||||||
|
}
|
||||||
+144
-11
@@ -1,22 +1,32 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use pulse_wire::{
|
||||||
|
general::Position,
|
||||||
|
units::{Symbol, USD},
|
||||||
|
};
|
||||||
use tokio::{sync::Mutex, task::JoinHandle};
|
use tokio::{sync::Mutex, task::JoinHandle};
|
||||||
|
|
||||||
use crate::{config::Config, terminal::TerminalServer};
|
use crate::{
|
||||||
|
store::{accounts::AccountList, config::Config},
|
||||||
|
terminal::TerminalServer,
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Engine {
|
pub struct Engine {
|
||||||
pub terminal_server: Arc<TerminalServer>,
|
pub terminal_server: Arc<TerminalServer>,
|
||||||
pub config: Arc<Mutex<Config>>,
|
pub config: Arc<Mutex<Config>>,
|
||||||
|
pub accounts: Arc<Mutex<AccountList>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Engine {
|
impl Engine {
|
||||||
pub async fn new() -> tokio::io::Result<Arc<Self>> {
|
pub async fn new() -> tokio::io::Result<Arc<Self>> {
|
||||||
let config = Arc::new(Mutex::new(Config::new().await?));
|
let config = Arc::new(Mutex::new(Config::new().await?));
|
||||||
|
let accounts = Arc::new(Mutex::new(AccountList::new().await?));
|
||||||
|
|
||||||
Ok(Arc::new_cyclic(|engine| Self {
|
Ok(Arc::new_cyclic(|engine| Self {
|
||||||
terminal_server: TerminalServer::new(engine.clone()),
|
terminal_server: TerminalServer::new(engine.clone()),
|
||||||
config,
|
config,
|
||||||
|
accounts,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,12 +45,14 @@ impl Engine {
|
|||||||
pub async fn run_broadcaster(&self) -> tokio::io::Result<()> {
|
pub async fn run_broadcaster(&self) -> tokio::io::Result<()> {
|
||||||
let mut refresh = tokio::time::interval(tokio::time::Duration::from_secs(5));
|
let mut refresh = tokio::time::interval(tokio::time::Duration::from_secs(5));
|
||||||
|
|
||||||
|
let client = hypersdk::hypercore::mainnet();
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
refresh.tick().await;
|
refresh.tick().await;
|
||||||
|
|
||||||
let watch_list = &self.config.lock().await.watchlist.symbols;
|
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) => {
|
Ok(watch_list) => {
|
||||||
if let Err(error) = self
|
if let Err(error) = self
|
||||||
.terminal_server
|
.terminal_server
|
||||||
@@ -51,10 +63,62 @@ impl Engine {
|
|||||||
)
|
)
|
||||||
.await
|
.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?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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| Position {
|
||||||
|
symbol: Symbol(position.position.coin),
|
||||||
|
size: position.position.szi.as_f64(),
|
||||||
|
entry_price: USD(position
|
||||||
|
.position
|
||||||
|
.entry_px
|
||||||
|
.map(|px| px.as_f64())
|
||||||
|
.unwrap_or(0.0)),
|
||||||
|
profit: USD(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?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -65,15 +129,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<()> {
|
pub async fn execute_command(&self, command: &str, args: Vec<&str>) -> tokio::io::Result<()> {
|
||||||
match command {
|
match command {
|
||||||
"config" => {
|
"config" | "cfg" => {
|
||||||
const MESSAGE: &str = "Invalid command arguments, usage: config <reload>";
|
|
||||||
|
|
||||||
if args.len() != 1 {
|
if args.len() != 1 {
|
||||||
self.terminal_server.error("config", MESSAGE).await?;
|
return self.invalid_command_usage("config").await;
|
||||||
|
|
||||||
return Ok(());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
match args[0] {
|
match args[0] {
|
||||||
@@ -82,7 +146,76 @@ 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?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-7
@@ -1,6 +1,9 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use pulse_wire::terminal::WatchListItem;
|
use pulse_wire::{
|
||||||
|
terminal::MarketItem,
|
||||||
|
units::{Symbol, USD},
|
||||||
|
};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
fn number(value: &Value, field: &str) -> Result<f64, String> {
|
fn number(value: &Value, field: &str) -> Result<f64, String> {
|
||||||
@@ -12,8 +15,11 @@ fn number(value: &Value, field: &str) -> Result<f64, String> {
|
|||||||
.map_err(|error| format!("could not parse asset context field {field} ({raw}): {error}"))
|
.map_err(|error| format!("could not parse asset context field {field} ({raw}): {error}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn fetch_watch_list(symbols: &[String]) -> Result<Vec<WatchListItem>, String> {
|
pub async fn fetch_watch_list(
|
||||||
let response = hypersdk::hypercore::mainnet()
|
client: &hypersdk::hypercore::HttpClient,
|
||||||
|
symbols: &[String],
|
||||||
|
) -> Result<Vec<MarketItem>, String> {
|
||||||
|
let response = client
|
||||||
.meta_and_asset_ctxs(None)
|
.meta_and_asset_ctxs(None)
|
||||||
.await
|
.await
|
||||||
.map_err(|error| format!("Hyperliquid metaAndAssetCtxs request failed: {error}"))?;
|
.map_err(|error| format!("Hyperliquid metaAndAssetCtxs request failed: {error}"))?;
|
||||||
@@ -60,11 +66,11 @@ pub async fn fetch_watch_list(symbols: &[String]) -> Result<Vec<WatchListItem>,
|
|||||||
|
|
||||||
by_symbol.insert(
|
by_symbol.insert(
|
||||||
symbol,
|
symbol,
|
||||||
WatchListItem {
|
MarketItem {
|
||||||
symbol: symbol.to_owned(),
|
symbol: Symbol(symbol.to_owned()),
|
||||||
price,
|
price: USD(price),
|
||||||
|
volume_24h: USD(volume_24h),
|
||||||
trend: ((price / previous_day_price) - 1.0) * 100.0,
|
trend: ((price / previous_day_price) - 1.0) * 100.0,
|
||||||
volume_24h,
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
pub mod config;
|
|
||||||
pub mod engine;
|
pub mod engine;
|
||||||
pub mod fetch;
|
pub mod fetch;
|
||||||
|
pub mod store;
|
||||||
pub mod terminal;
|
pub mod terminal;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
use std::{collections::HashMap, str::FromStr};
|
||||||
|
|
||||||
|
use hypersdk::{Address, hypercore::PrivateKeySigner};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct PrivateKey(pub PrivateKeySigner, pub String);
|
||||||
|
|
||||||
|
impl FromStr for PrivateKey {
|
||||||
|
type Err = <PrivateKeySigner as FromStr>::Err;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
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<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: serde::Serializer,
|
||||||
|
{
|
||||||
|
serializer.serialize_str(&self.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||||
|
pub struct Account {
|
||||||
|
pub address: Address,
|
||||||
|
pub private_key: PrivateKey,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, serde::Serialize, serde::Deserialize)]
|
||||||
|
pub struct AccountList {
|
||||||
|
pub active: String,
|
||||||
|
|
||||||
|
#[serde(flatten)]
|
||||||
|
pub accounts: HashMap<String, Account>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AccountList {
|
||||||
|
pub async fn new() -> tokio::io::Result<Self> {
|
||||||
|
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<Self> {
|
||||||
|
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<String> {
|
||||||
|
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 {
|
||||||
|
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::<Vec<char>>()
|
||||||
|
.into_iter()
|
||||||
|
.rev()
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
format!("{}..{}", first, last)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,19 +1,3 @@
|
|||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
pub fn home_dir() -> tokio::io::Result<PathBuf> {
|
|
||||||
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<PathBuf> {
|
|
||||||
Ok(home_dir()?.join(".config/pulse-trader"))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn pulse_config_directory() -> tokio::io::Result<PathBuf> {
|
|
||||||
Ok(home_dir()?.join(".config/pulse-trader/config.toml"))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Default, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Default, serde::Serialize, serde::Deserialize)]
|
||||||
pub struct WatchList {
|
pub struct WatchList {
|
||||||
pub symbols: Vec<String>,
|
pub symbols: Vec<String>,
|
||||||
@@ -26,7 +10,7 @@ pub struct Config {
|
|||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
pub async fn new() -> tokio::io::Result<Self> {
|
pub async fn new() -> tokio::io::Result<Self> {
|
||||||
let path = pulse_config_directory()?;
|
let path = crate::store::pulse_config_file()?;
|
||||||
|
|
||||||
if !path.exists() {
|
if !path.exists() {
|
||||||
let default = Self::default();
|
let default = Self::default();
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
pub mod accounts;
|
||||||
|
pub mod config;
|
||||||
|
|
||||||
|
pub fn home_dir() -> tokio::io::Result<PathBuf> {
|
||||||
|
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<PathBuf> {
|
||||||
|
Ok(home_dir()?.join(".config").join("pulse-trader"))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn pulse_config_file() -> tokio::io::Result<PathBuf> {
|
||||||
|
Ok(pulse_directory()?.join("config.toml"))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn pulse_accounts_file() -> tokio::io::Result<PathBuf> {
|
||||||
|
Ok(pulse_directory()?.join("accounts.toml"))
|
||||||
|
}
|
||||||
@@ -3,10 +3,7 @@ use std::{
|
|||||||
sync::{Arc, Weak},
|
sync::{Arc, Weak},
|
||||||
};
|
};
|
||||||
|
|
||||||
use pulse_wire::{
|
use pulse_wire::{PulseWire, general::{EventLog, LogKind}, terminal::TerminalClientMessage};
|
||||||
PulseWire,
|
|
||||||
terminal::{EventLog, LogKind, TerminalClientMessage},
|
|
||||||
};
|
|
||||||
use tokio::{
|
use tokio::{
|
||||||
io::{AsyncReadExt, AsyncWriteExt},
|
io::{AsyncReadExt, AsyncWriteExt},
|
||||||
net::{
|
net::{
|
||||||
|
|||||||
+18
-73
@@ -1,8 +1,10 @@
|
|||||||
use pulse_wire::terminal::{
|
use pulse_wire::terminal::{
|
||||||
ActivePosition, Alert, AlertLevel, EventLog, InspectItem, InspectTarget, LogKind,
|
Alert, AlertLevel, InspectItem, InspectTarget, MarketItem, MarketOverview, Status,
|
||||||
MarketOverview, Signal, SignalKind, Status, WatchListItem,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use pulse_wire::general::{Direction, EventLog, LogKind, Position, Signal};
|
||||||
|
use pulse_wire::units::format_f64;
|
||||||
|
|
||||||
pub trait Formatted {
|
pub trait Formatted {
|
||||||
fn get_formatted(&self) -> Vec<String>;
|
fn get_formatted(&self) -> Vec<String>;
|
||||||
}
|
}
|
||||||
@@ -64,24 +66,23 @@ 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, Direction::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),
|
self.price.to_string(),
|
||||||
format_f64(self.price),
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Formatted for WatchListItem {
|
impl Formatted for MarketItem {
|
||||||
fn get_formatted(&self) -> Vec<String> {
|
fn get_formatted(&self) -> Vec<String> {
|
||||||
vec![
|
vec![
|
||||||
format!("\x1b[35m{}\x1b[0m", self.symbol),
|
self.symbol.to_string(),
|
||||||
format!("${}", format_f64(self.price)),
|
self.price.to_string(),
|
||||||
format!("${}", format_f64(self.volume_24h)),
|
self.volume_24h.to_string(),
|
||||||
format!(
|
format!(
|
||||||
"{} {}",
|
"{} {}",
|
||||||
if self.trend.is_sign_positive() {
|
if self.trend.is_sign_positive() {
|
||||||
@@ -95,21 +96,20 @@ impl Formatted for WatchListItem {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Formatted for ActivePosition {
|
impl Formatted for Position {
|
||||||
fn get_formatted(&self) -> Vec<String> {
|
fn get_formatted(&self) -> Vec<String> {
|
||||||
vec![
|
vec![
|
||||||
format!("\x1b[35m{}\x1b[0m", self.symbol),
|
self.symbol.to_string(),
|
||||||
format_f64(self.size),
|
format_f64(self.size),
|
||||||
format_f64(self.entry_price),
|
self.entry_price.to_string(),
|
||||||
format_f64(self.mark_price),
|
|
||||||
format!(
|
format!(
|
||||||
"{} {}",
|
"{}{}\x1b[0m",
|
||||||
if self.profit.is_sign_positive() {
|
if self.profit.0.is_sign_positive() {
|
||||||
"\x1b[32m▲\x1b[0m"
|
"\x1b[32m"
|
||||||
} else {
|
} else {
|
||||||
"\x1b[31m▼\x1b[0m"
|
"\x1b[31m"
|
||||||
},
|
},
|
||||||
format_f64(self.profit.abs())
|
self.profit,
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -207,58 +207,3 @@ pub fn apply_padding(mut items: Vec<String>) -> Vec<String> {
|
|||||||
|
|
||||||
items
|
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
|
|
||||||
}
|
|
||||||
|
|||||||
+39
-23
@@ -21,8 +21,9 @@ use pulse_ui::{
|
|||||||
|
|
||||||
use crate::formatting::{Formatted, apply_padding};
|
use crate::formatting::{Formatted, apply_padding};
|
||||||
|
|
||||||
use pulse_wire::terminal::{
|
use pulse_wire::{
|
||||||
ActivePosition, EventLog, InspectTarget, MarketOverview, Signal, Status, WatchListItem,
|
general::{EventLog, Position, Signal},
|
||||||
|
terminal::{InspectTarget, MarketItem, MarketOverview, Status},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct PulseTradeApp {
|
pub struct PulseTradeApp {
|
||||||
@@ -30,12 +31,14 @@ pub struct PulseTradeApp {
|
|||||||
|
|
||||||
command: State<InputState>,
|
command: State<InputState>,
|
||||||
scroll: State<ScrollState<7>>,
|
scroll: State<ScrollState<7>>,
|
||||||
watch_list: State<Vec<WatchListItem>>,
|
|
||||||
active_positions: State<Vec<ActivePosition>>,
|
market_overview: State<Option<MarketOverview>>,
|
||||||
|
status: State<Option<Status>>,
|
||||||
|
|
||||||
|
watch_list: State<Vec<MarketItem>>,
|
||||||
|
active_positions: State<Vec<Position>>,
|
||||||
logs: State<Vec<EventLog>>,
|
logs: State<Vec<EventLog>>,
|
||||||
signals: State<Vec<Signal>>,
|
signals: State<Vec<Signal>>,
|
||||||
market_overview: State<MarketOverview>,
|
|
||||||
status: State<Status>,
|
|
||||||
inspect: State<InspectTarget>,
|
inspect: State<InspectTarget>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,15 +131,19 @@ impl App for PulseTradeApp {
|
|||||||
(
|
(
|
||||||
LayoutItem::Widget(Size::Flex(1)),
|
LayoutItem::Widget(Size::Flex(1)),
|
||||||
Box::new(
|
Box::new(
|
||||||
advanced_draw(&self.scroll, 1, "ACTIVE POSITIONS", &self.active_positions)
|
advanced_draw(&self.scroll, 1, "POSITIONS", &self.active_positions).await,
|
||||||
.await,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
LayoutItem::Widget(Size::Flex(1)),
|
LayoutItem::Widget(Size::Flex(1)),
|
||||||
Box::new(
|
Box::new(
|
||||||
advanced_draw(&self.scroll, 2, "MARKET OVERVIEW", &self.market_overview)
|
advanced_option_draw(
|
||||||
.await,
|
&self.scroll,
|
||||||
|
2,
|
||||||
|
"MARKET OVERVIEW",
|
||||||
|
&self.market_overview,
|
||||||
|
)
|
||||||
|
.await,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
]),
|
]),
|
||||||
@@ -155,7 +162,7 @@ impl App for PulseTradeApp {
|
|||||||
),
|
),
|
||||||
(
|
(
|
||||||
LayoutItem::Widget(Size::Flex(1)),
|
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 +190,8 @@ async fn main() -> tokio::io::Result<()> {
|
|||||||
signals: ctx.use_state(Vec::new()),
|
signals: ctx.use_state(Vec::new()),
|
||||||
logs: ctx.use_state(Vec::new()),
|
logs: ctx.use_state(Vec::new()),
|
||||||
inspect: ctx.use_state(InspectTarget::None),
|
inspect: ctx.use_state(InspectTarget::None),
|
||||||
market_overview: ctx.use_state(MarketOverview {
|
market_overview: ctx.use_state(None),
|
||||||
trend: pulse_wire::terminal::MarketTrend::Bullish,
|
status: ctx.use_state(None),
|
||||||
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,
|
|
||||||
}),
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
@@ -216,3 +213,22 @@ pub async fn advanced_draw<const N: usize, T: Formatted>(
|
|||||||
apply_padding(state.lock().await.get_formatted()).join("\n"),
|
apply_padding(state.lock().await.get_formatted()).join("\n"),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn advanced_option_draw<const N: usize, T: Formatted>(
|
||||||
|
scroll: &State<ScrollState<N>>,
|
||||||
|
index: usize,
|
||||||
|
title: &'static str,
|
||||||
|
state: &State<Option<T>>,
|
||||||
|
) -> 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()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ impl TerminalClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
TerminalServerMessage::OverviewUpdated(v) => {
|
TerminalServerMessage::OverviewUpdated(v) => {
|
||||||
*market_overview.lock().await = v;
|
*market_overview.lock().await = Some(v);
|
||||||
}
|
}
|
||||||
|
|
||||||
TerminalServerMessage::SignalsUpdated(v) => {
|
TerminalServerMessage::SignalsUpdated(v) => {
|
||||||
@@ -89,8 +89,8 @@ impl TerminalClient {
|
|||||||
*inspect.lock().await = v;
|
*inspect.lock().await = v;
|
||||||
}
|
}
|
||||||
|
|
||||||
TerminalServerMessage::SetStatus(v) => {
|
TerminalServerMessage::StatusUpdated(v) => {
|
||||||
*status.lock().await = v;
|
*status.lock().await = Some(v);
|
||||||
}
|
}
|
||||||
|
|
||||||
TerminalServerMessage::SetLogs(v) => {
|
TerminalServerMessage::SetLogs(v) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user