301 lines
7.8 KiB
Rust
301 lines
7.8 KiB
Rust
use hypersdk::hypercore::Side;
|
|
use pulse_sdk::prelude::*;
|
|
|
|
pub trait Formatted {
|
|
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::Some(items) => items
|
|
.iter()
|
|
.map(|item| match item {
|
|
InspectItem::F64(v) => format_f64(*v),
|
|
InspectItem::USD(v) => format!("${}", format_f64(*v)),
|
|
InspectItem::Symbol(v) => format!("\x1b[35m{v}\x1b[0m"),
|
|
InspectItem::String(v) => v.clone(),
|
|
})
|
|
.collect(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Formatted for EventLog {
|
|
fn get_formatted(&self) -> Vec<String> {
|
|
vec![
|
|
format!(
|
|
"{}{}\x1b[0m",
|
|
match &self.kind {
|
|
LogKind::Info => "\x1b[34m",
|
|
LogKind::Debug => "\x1b[36m",
|
|
LogKind::Err => "\x1b[31m",
|
|
LogKind::Warn => "\x1b[33m",
|
|
},
|
|
self.kind
|
|
),
|
|
format!("(\x1b[37m{}\x1b[0m)", self.name),
|
|
self.message.clone(),
|
|
]
|
|
}
|
|
}
|
|
|
|
impl Formatted for SignalStatus {
|
|
fn get_formatted(&self) -> Vec<String> {
|
|
match self {
|
|
Ok(signal) => {
|
|
vec![
|
|
format!("\x1b[33mOK\x1b[0m"),
|
|
if matches!(signal.side, Side::Ask) {
|
|
format!("\x1b[32mBUY\x1b[0m")
|
|
} else {
|
|
format!("\x1b[31mSELL\x1b[0m")
|
|
},
|
|
format_symbol(&signal.symbol),
|
|
format_usd(signal.price.as_f64()),
|
|
format!("\x1b[33mAPR {}\x1b[0m", signal.confidence),
|
|
]
|
|
}
|
|
|
|
Err(signal) => {
|
|
vec![
|
|
format!("\x1b[31mERR\x1b[0m"),
|
|
if matches!(signal.side, Side::Ask) {
|
|
format!("\x1b[32mBUY\x1b[0m")
|
|
} else {
|
|
format!("\x1b[31mSELL\x1b[0m")
|
|
},
|
|
format_symbol(&signal.symbol),
|
|
format_usd(signal.price.as_f64()),
|
|
format!("\x1b[33mAPR {}\x1b[0m", signal.confidence),
|
|
]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Formatted for MarketItem {
|
|
fn get_formatted(&self) -> Vec<String> {
|
|
vec![
|
|
format_symbol(&self.symbol),
|
|
format_usd(self.price.as_f64()),
|
|
format_usd(self.volume_24h.as_f64()),
|
|
format!(
|
|
"{} {}%",
|
|
if self.trend.is_sign_positive() {
|
|
"\x1b[32m▲\x1b[0m"
|
|
} else {
|
|
"\x1b[31m▼\x1b[0m"
|
|
},
|
|
format_f64(self.trend.as_f64().abs())
|
|
),
|
|
]
|
|
}
|
|
}
|
|
|
|
impl Formatted for Position {
|
|
fn get_formatted(&self) -> Vec<String> {
|
|
vec![
|
|
format_symbol(&self.symbol),
|
|
format_f64(self.size.as_f64()),
|
|
self.entry_price.to_string(),
|
|
format!(
|
|
"{}{}\x1b[0m",
|
|
if self.pnl.is_sign_positive() {
|
|
"\x1b[32m"
|
|
} else {
|
|
"\x1b[31m"
|
|
},
|
|
self.pnl,
|
|
),
|
|
]
|
|
}
|
|
}
|
|
|
|
struct Pair<'a>(&'a str, &'a str);
|
|
|
|
struct Triple<'a>(&'a str, &'a str, &'a str);
|
|
|
|
impl Formatted for Status {
|
|
fn get_formatted(&self) -> Vec<String> {
|
|
vec![
|
|
Pair("Feed", &format!("{:?}", self.feed)),
|
|
Pair("Exchange", &self.exchange.clone()),
|
|
Pair("DEX", &self.dex.clone()),
|
|
Pair("Latency", &format!("{} ms", self.latency)),
|
|
]
|
|
.get_formatted()
|
|
}
|
|
}
|
|
|
|
impl<'a> Formatted for Pair<'a> {
|
|
fn get_formatted(&self) -> Vec<String> {
|
|
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();
|
|
|
|
let rows: Vec<Vec<String>> = self.iter().map(|v| v.get_formatted()).collect();
|
|
|
|
if rows.is_empty() {
|
|
return output;
|
|
}
|
|
|
|
let column_count = rows.iter().map(|r| r.len()).max().unwrap_or(0);
|
|
|
|
let mut widths = vec![0; column_count];
|
|
|
|
for row in &rows {
|
|
for (i, col) in row.iter().enumerate() {
|
|
widths[i] = widths[i].max(visible_len(col));
|
|
}
|
|
}
|
|
|
|
for row in rows {
|
|
let mut row_out = String::new();
|
|
|
|
for (col_idx, col) in row.iter().enumerate() {
|
|
let width = widths[col_idx];
|
|
|
|
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(" ");
|
|
}
|
|
}
|
|
|
|
output.push(row_out);
|
|
}
|
|
|
|
output
|
|
}
|
|
}
|
|
|
|
impl Formatted for StrategyStatus {
|
|
fn get_formatted(&self) -> Vec<String> {
|
|
vec![
|
|
Triple(
|
|
"\x1b[2mStrategy\x1b[0m",
|
|
"\x1b[2mState\x1b[0m",
|
|
"\x1b[2mMode\x1b[0m",
|
|
),
|
|
Triple(
|
|
&format!("\x1b[97m{}\x1b[0m", self.strategy.name),
|
|
&self.state.to_string(),
|
|
&self.mode.to_string(),
|
|
),
|
|
]
|
|
.get_formatted()
|
|
}
|
|
}
|
|
|
|
pub fn apply_padding(mut items: Vec<String>) -> Vec<String> {
|
|
for item in &mut items {
|
|
item.insert(0, ' ');
|
|
}
|
|
|
|
items
|
|
}
|
|
|
|
pub fn format_symbol(value: &str) -> String {
|
|
format!("\x1b[35m{value}\x1b[0m")
|
|
}
|
|
|
|
pub fn format_usd(value: f64) -> String {
|
|
if value.is_sign_positive() {
|
|
format!("\x1b[32m${}\x1b[0m", format_f64(value))
|
|
} else {
|
|
format!("\x1b[31m${}\x1b[0m", format_f64(value))
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|