use rust_decimal::prelude::ToPrimitive;
use super::{Panel, PanelKind};
use crate::source::{SourceId, Symbol};
use crate::state::AppState;
use crate::view::{PanelView, WatchRow, WatchlistView};
#[derive(Debug)]
pub struct WatchlistPanel;
impl Panel for WatchlistPanel {
fn kind(&self) -> PanelKind {
PanelKind::Watchlist
}
fn view(&self, state: &AppState, _focus: (SourceId, &Symbol)) -> PanelView {
let rows = state
.watchlist
.iter()
.map(|key| {
let Some(market) = state.get(key) else {
return WatchRow {
source: key.0,
symbol: key.1.to_string(),
last: 0.0,
bid: 0.0,
ask: 0.0,
volume: 0.0,
change: 0.0,
};
};
let last = market.last.to_f64().unwrap_or(0.0);
let change = if market.open.is_zero() {
0.0
} else {
let open = market.open.to_f64().unwrap_or(0.0);
(last - open) / open * 100.0
};
WatchRow {
source: key.0,
symbol: key.1.to_string(),
last,
bid: market.bid.to_f64().unwrap_or(0.0),
ask: market.ask.to_f64().unwrap_or(0.0),
volume: market.volume.to_f64().unwrap_or(0.0),
change,
}
})
.collect();
PanelView::Watchlist(WatchlistView { rows })
}
}
#[cfg(test)]
mod tests {
use super::*;
use rust_decimal_macros::dec;
use wickra_exchange_core::{OrderSide, Ticker, TradePrint};
fn trade(sym: &Symbol, price: rust_decimal::Decimal) -> wickra_exchange_core::Event {
wickra_exchange_core::Event::Trade(TradePrint {
symbol: sym.clone(),
price,
quantity: dec!(1),
aggressor: OrderSide::Buy,
timestamp: 0,
})
}
fn rows(state: &AppState) -> Vec<WatchRow> {
let sym = Symbol::new("BTC", "USDT");
let PanelView::Watchlist(view) = WatchlistPanel.view(state, (0, &sym)) else {
panic!("the watchlist panel answers with a watchlist")
};
view.rows
}
#[test]
fn a_row_reports_the_quote_the_turnover_and_the_move() {
let sym = Symbol::new("BTC", "USDT");
let mut state = AppState::default();
state.watchlist.push((0, sym.clone()));
state.fold(0, &sym, &trade(&sym, dec!(100)));
state.fold(
0,
&sym,
&wickra_exchange_core::Event::Ticker(Ticker {
symbol: sym.clone(),
last: dec!(110),
bid: dec!(109),
ask: dec!(111),
volume: dec!(5000),
timestamp: 0,
}),
);
let rows = rows(&state);
assert_eq!(rows.len(), 1);
let row = &rows[0];
assert!((row.last - 110.0).abs() < 1e-9);
assert!((row.bid - 109.0).abs() < 1e-9);
assert!((row.ask - 111.0).abs() < 1e-9);
assert!((row.volume - 5000.0).abs() < 1e-9);
assert!((row.change - 10.0).abs() < 1e-9, "change: {}", row.change);
}
#[test]
fn a_subscribed_market_with_no_events_is_a_zeroed_row() {
let sym = Symbol::new("BTC", "USDT");
let mut state = AppState::default();
state.watchlist.push((0, sym));
let rows = rows(&state);
assert_eq!(rows.len(), 1);
let row = &rows[0];
assert!(row.last.abs() < f64::EPSILON);
assert!(row.bid.abs() < f64::EPSILON);
assert!(row.ask.abs() < f64::EPSILON);
assert!(row.volume.abs() < f64::EPSILON);
assert!(row.change.abs() < f64::EPSILON);
}
#[test]
fn trades_without_a_ticker_still_report_a_change() {
let sym = Symbol::new("BTC", "USDT");
let mut state = AppState::default();
state.watchlist.push((0, sym.clone()));
state.fold(0, &sym, &trade(&sym, dec!(200)));
state.fold(0, &sym, &trade(&sym, dec!(150)));
let row = rows(&state).remove(0);
assert!(
row.bid.abs() < f64::EPSILON,
"a quote appeared from nowhere"
);
assert!(
row.ask.abs() < f64::EPSILON,
"a quote appeared from nowhere"
);
assert!((row.change + 25.0).abs() < 1e-9, "change: {}", row.change);
}
}