use std::collections::VecDeque;
use chrono::{DateTime, Utc};
use super::super::trading::TradingSide;
#[derive(Debug, Clone)]
pub struct LiquidationEntry {
pub received_at: DateTime<Utc>,
pub symbol: String,
pub asset_id: u32,
pub side: Option<TradingSide>,
pub liquidated_trader: String,
pub size: f64,
pub mark_price: f64,
pub notional: f64,
pub price_decimals: usize,
pub size_decimals: usize,
}
pub const LIQUIDATION_FEED_CAPACITY: usize = 200;
#[derive(Debug, Clone)]
pub enum LiquidationFeedMsg {
Entry(LiquidationEntry),
BackfillComplete,
}
pub struct LiquidationFeedView {
pub entries: VecDeque<LiquidationEntry>,
pub selected_index: usize,
pub is_backfilling: bool,
}
impl LiquidationFeedView {
pub fn new() -> Self {
Self {
entries: VecDeque::with_capacity(LIQUIDATION_FEED_CAPACITY),
selected_index: 0,
is_backfilling: true,
}
}
pub fn push(&mut self, entry: LiquidationEntry) {
let pos = self
.entries
.iter()
.position(|e| entry.received_at > e.received_at)
.unwrap_or(self.entries.len());
self.entries.insert(pos, entry);
while self.entries.len() > LIQUIDATION_FEED_CAPACITY {
self.entries.pop_back();
}
self.clamp_index();
}
pub fn clamp_index(&mut self) {
if self.entries.is_empty() {
self.selected_index = 0;
} else {
self.selected_index = self.selected_index.min(self.entries.len() - 1);
}
}
pub fn move_down(&mut self) {
if self.selected_index + 1 < self.entries.len() {
self.selected_index += 1;
}
}
pub fn move_up(&mut self) {
if self.selected_index > 0 {
self.selected_index -= 1;
}
}
}
impl Default for LiquidationFeedView {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_entry_at(tag: &str, secs: i64) -> LiquidationEntry {
LiquidationEntry {
received_at: DateTime::from_timestamp(1_700_000_000 + secs, 0).unwrap(),
symbol: tag.to_string(),
asset_id: 0,
side: None,
liquidated_trader: String::new(),
size: 1.0,
mark_price: 100.0,
notional: 100.0,
price_decimals: 2,
size_decimals: 2,
}
}
#[test]
fn push_orders_by_received_at_descending_regardless_of_arrival_order() {
let mut v = LiquidationFeedView::new();
v.push(make_entry_at("older", 0));
v.push(make_entry_at("newer", 10));
v.push(make_entry_at("middle", 5));
let tags: Vec<&str> = v.entries.iter().map(|e| e.symbol.as_str()).collect();
assert_eq!(tags, vec!["newer", "middle", "older"]);
}
#[test]
fn push_drops_oldest_at_capacity() {
let mut v = LiquidationFeedView::new();
for i in 0..(LIQUIDATION_FEED_CAPACITY + 5) {
v.push(make_entry_at(&format!("{:04}", i), i as i64));
}
assert_eq!(v.entries.len(), LIQUIDATION_FEED_CAPACITY);
assert_eq!(
v.entries.front().unwrap().symbol,
format!("{:04}", LIQUIDATION_FEED_CAPACITY + 4)
);
assert_eq!(v.entries.back().unwrap().symbol, format!("{:04}", 5));
}
#[test]
fn push_drops_self_if_older_than_full_buffer() {
let mut v = LiquidationFeedView::new();
for i in 0..LIQUIDATION_FEED_CAPACITY {
v.push(make_entry_at(&format!("{:04}", i), 100 + i as i64));
}
v.push(make_entry_at("ancient", 0));
assert_eq!(v.entries.len(), LIQUIDATION_FEED_CAPACITY);
assert!(v.entries.iter().all(|e| e.symbol != "ancient"));
}
#[test]
fn move_clamped_to_bounds() {
let mut v = LiquidationFeedView::new();
v.push(make_entry_at("a", 0));
v.push(make_entry_at("b", 1));
v.move_down();
v.move_down();
v.move_down();
assert_eq!(v.selected_index, 1);
v.move_up();
v.move_up();
v.move_up();
assert_eq!(v.selected_index, 0);
}
}