96 lines
2.8 KiB
Rust
96 lines
2.8 KiB
Rust
use pulse_sdk::prelude::*;
|
|
use rust_decimal::Decimal;
|
|
use serde_json::Value;
|
|
use std::collections::HashMap;
|
|
|
|
use crate::engine::WatchList;
|
|
|
|
fn number(value: &Value, field: &str) -> Result<Decimal, String> {
|
|
let raw = value[field]
|
|
.as_str()
|
|
.ok_or_else(|| format!("asset context field {field} must be a string"))?;
|
|
|
|
raw.parse::<Decimal>()
|
|
.map_err(|error| format!("could not parse asset context field {field} ({raw}): {error}"))
|
|
}
|
|
|
|
pub async fn fetch_watch_list(
|
|
client: &hypersdk::hypercore::HttpClient,
|
|
symbols: &[String],
|
|
) -> Result<WatchList, String> {
|
|
let response = client
|
|
.meta_and_asset_ctxs(None)
|
|
.await
|
|
.map_err(|error| format!("Hyperliquid metaAndAssetCtxs request failed: {error}"))?;
|
|
|
|
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()
|
|
));
|
|
}
|
|
|
|
// Build symbol -> perp asset index
|
|
let mut name_to_index = HashMap::with_capacity(universe.len());
|
|
|
|
for (index, meta) in universe.iter().enumerate() {
|
|
let symbol = meta["name"]
|
|
.as_str()
|
|
.ok_or("instrument metadata is missing a name")?;
|
|
|
|
name_to_index.insert(symbol.to_owned(), index);
|
|
}
|
|
|
|
// Now only process requested symbols
|
|
let mut items = Vec::with_capacity(symbols.len());
|
|
|
|
for symbol in symbols {
|
|
let index = *name_to_index
|
|
.get(symbol)
|
|
.ok_or_else(|| format!("{symbol} is not in the Hyperliquid perpetual universe"))?;
|
|
|
|
let context = &contexts[index];
|
|
|
|
let price = number(context, "markPx")?;
|
|
let previous_day_price = number(context, "prevDayPx")?;
|
|
let volume_24h = number(context, "dayNtlVlm")?;
|
|
|
|
if previous_day_price.is_zero() || previous_day_price.is_sign_negative() {
|
|
return Err(format!("{symbol} has an invalid previous-day price"));
|
|
}
|
|
|
|
items.push(MarketItem {
|
|
symbol: symbol.clone(),
|
|
price: price,
|
|
volume_24h: volume_24h,
|
|
trend: ((price / previous_day_price) - <Decimal as From<i32>>::from(1))
|
|
* <Decimal as From<i32>>::from(100),
|
|
});
|
|
}
|
|
|
|
Ok(WatchList {
|
|
items,
|
|
name_to_index,
|
|
})
|
|
}
|