Working engine broadcaster
This commit is contained in:
Generated
+5448
-16
File diff suppressed because it is too large
Load Diff
@@ -10,6 +10,8 @@ pulse-wire = { workspace = true }
|
||||
chrono = "0.4.45"
|
||||
tokio = { workspace = true, features = ["rt-multi-thread", "macros", "net", "fs", "io-util", "time"] }
|
||||
crossterm = { workspace = true }
|
||||
hypersdk = "0.2.14"
|
||||
serde_json = "1"
|
||||
|
||||
[workspace]
|
||||
members = ["pulse-macros", "pulse-ui", "pulse-wire"]
|
||||
|
||||
@@ -36,6 +36,7 @@ pub struct WatchListItem {
|
||||
symbol: String,
|
||||
price: f64,
|
||||
trend: f64,
|
||||
volume_24h: f64,
|
||||
}
|
||||
|
||||
#[pwp]
|
||||
|
||||
+106
-1
@@ -1,5 +1,12 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use pulse_wire::terminal::WatchListItem;
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::terminal::TerminalServer;
|
||||
|
||||
const WATCH_LIST_SYMBOLS: &[&str] = &["BTC", "ETH", "SOL", "XRP"];
|
||||
|
||||
pub struct Engine {
|
||||
pub terminal_server: TerminalServer,
|
||||
}
|
||||
@@ -26,8 +33,26 @@ impl Engine {
|
||||
let terminal_server = self.terminal_server.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
let mut refresh = tokio::time::interval(tokio::time::Duration::from_secs(5));
|
||||
|
||||
loop {
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(5000)).await;
|
||||
refresh.tick().await;
|
||||
|
||||
match fetch_watch_list(WATCH_LIST_SYMBOLS).await {
|
||||
Ok(watch_list) => {
|
||||
if let Err(error) = terminal_server
|
||||
.broadcast(
|
||||
pulse_wire::terminal::TerminalServerMessage::WatchListUpdated(
|
||||
watch_list,
|
||||
),
|
||||
)
|
||||
.await
|
||||
{
|
||||
eprintln!("Failed to broadcast Hyperliquid watch list: {error}");
|
||||
}
|
||||
}
|
||||
Err(error) => eprintln!("Failed to refresh Hyperliquid watch list: {error}"),
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -40,3 +65,83 @@ impl Engine {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
async fn fetch_watch_list(symbols: &[&str]) -> Result<Vec<WatchListItem>, String> {
|
||||
let response = hypersdk::hypercore::mainnet()
|
||||
.meta_and_asset_ctxs(None)
|
||||
.await
|
||||
.map_err(|error| format!("Hyperliquid metaAndAssetCtxs request failed: {error}"))?;
|
||||
|
||||
normalize_watch_list(&response, symbols)
|
||||
}
|
||||
|
||||
fn normalize_watch_list(response: &Value, symbols: &[&str]) -> Result<Vec<WatchListItem>, String> {
|
||||
let response = response
|
||||
.as_array()
|
||||
.ok_or("metaAndAssetCtxs response must be an array")?;
|
||||
|
||||
if response.len() != 2 {
|
||||
return Err(format!(
|
||||
"metaAndAssetCtxs response must contain meta and contexts, got {} entries",
|
||||
response.len()
|
||||
));
|
||||
}
|
||||
|
||||
let universe = response[0]["universe"]
|
||||
.as_array()
|
||||
.ok_or("metaAndAssetCtxs response is missing meta.universe")?;
|
||||
let contexts = response[1]
|
||||
.as_array()
|
||||
.ok_or("metaAndAssetCtxs response contexts must be an array")?;
|
||||
|
||||
if universe.len() != contexts.len() {
|
||||
return Err(format!(
|
||||
"Hyperliquid returned {} instruments but {} contexts",
|
||||
universe.len(),
|
||||
contexts.len()
|
||||
));
|
||||
}
|
||||
|
||||
let mut by_symbol = HashMap::with_capacity(universe.len());
|
||||
|
||||
for (meta, context) in universe.iter().zip(contexts) {
|
||||
let symbol = meta["name"]
|
||||
.as_str()
|
||||
.ok_or("instrument metadata is missing a name")?;
|
||||
let price = number(context, "markPx")?;
|
||||
let previous_day_price = number(context, "prevDayPx")?;
|
||||
let volume_24h = number(context, "dayNtlVlm")?;
|
||||
|
||||
if previous_day_price <= 0.0 {
|
||||
return Err(format!("{symbol} has an invalid previous-day price"));
|
||||
}
|
||||
|
||||
by_symbol.insert(
|
||||
symbol,
|
||||
WatchListItem {
|
||||
symbol: symbol.to_owned(),
|
||||
price,
|
||||
trend: ((price / previous_day_price) - 1.0) * 100.0,
|
||||
volume_24h,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
symbols
|
||||
.iter()
|
||||
.map(|symbol| {
|
||||
by_symbol
|
||||
.remove(*symbol)
|
||||
.ok_or_else(|| format!("{symbol} is not in the Hyperliquid perpetual universe"))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn number(value: &Value, field: &str) -> Result<f64, String> {
|
||||
let raw = value[field]
|
||||
.as_str()
|
||||
.ok_or_else(|| format!("asset context field {field} must be a string"))?;
|
||||
|
||||
raw.parse::<f64>()
|
||||
.map_err(|error| format!("could not parse asset context field {field} ({raw}): {error}"))
|
||||
}
|
||||
|
||||
+12
-8
@@ -75,14 +75,18 @@ impl TerminalServer {
|
||||
(command, Vec::new())
|
||||
};
|
||||
|
||||
self.broadcast(pulse_wire::terminal::TerminalServerMessage::AddLog(
|
||||
pulse_wire::terminal::EventLog {
|
||||
kind: pulse_wire::terminal::LogKind::Err,
|
||||
name: "Command executor".to_string(),
|
||||
message: format!("Command '{}' not found", command),
|
||||
},
|
||||
))
|
||||
.await?;
|
||||
match command {
|
||||
_ => {
|
||||
self.broadcast(pulse_wire::terminal::TerminalServerMessage::AddLog(
|
||||
pulse_wire::terminal::EventLog {
|
||||
kind: pulse_wire::terminal::LogKind::Err,
|
||||
name: "Command executor".to_string(),
|
||||
message: format!("Command '{}' not found", command),
|
||||
},
|
||||
))
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ impl Formatted for InspectTarget {
|
||||
format!("\x1b[96m{}\x1b[0m", format_f64(watch.price)),
|
||||
),
|
||||
Property("Trend", format!("{}", format_f64(watch.trend))),
|
||||
Property("24h volume", format!("${}", format_f64(watch.volume_24h))),
|
||||
]
|
||||
.get_formatted(),
|
||||
|
||||
@@ -102,6 +103,7 @@ impl Formatted for WatchListItem {
|
||||
vec![
|
||||
format!("\x1b[35m{}\x1b[0m", self.symbol),
|
||||
format_f64(self.price),
|
||||
format!("${}", format_f64(self.volume_24h)),
|
||||
format!(
|
||||
"{} {}",
|
||||
if self.trend.is_sign_positive() {
|
||||
@@ -227,7 +229,31 @@ pub fn apply_padding(mut items: Vec<String>) -> Vec<String> {
|
||||
}
|
||||
|
||||
pub fn format_f64(value: f64) -> String {
|
||||
let val = value.to_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();
|
||||
|
||||
Reference in New Issue
Block a user