use super::Trade;
use crate::shared::OrderBookId;
use std::collections::VecDeque;
#[derive(Debug, Clone)]
pub struct TradeHistory {
pub orderbook_id: OrderBookId,
trades: VecDeque<Trade>,
max_size: usize,
}
impl TradeHistory {
pub fn new(orderbook_id: OrderBookId, max_size: usize) -> Self {
Self {
orderbook_id,
trades: VecDeque::with_capacity(max_size),
max_size,
}
}
pub fn push(&mut self, trade: Trade) {
if self.max_size == 0 {
return;
}
if trade.sequence == 0 {
self.trades.push_front(trade);
if self.trades.len() > self.max_size {
self.trades.pop_back();
}
return;
}
let position = self
.trades
.iter()
.position(|existing| existing.sequence < trade.sequence)
.unwrap_or(self.trades.len());
if self.trades.len() >= self.max_size && position == self.trades.len() {
return;
}
self.trades.insert(position, trade);
if self.trades.len() > self.max_size {
self.trades.pop_back();
}
}
pub fn replace(&mut self, trades: Vec<Trade>) {
self.trades.clear();
for trade in trades.into_iter().take(self.max_size) {
self.trades.push_back(trade);
}
}
pub fn trades(&self) -> &VecDeque<Trade> {
&self.trades
}
pub fn latest(&self) -> Option<&Trade> {
self.trades.front()
}
pub fn clear(&mut self) {
self.trades.clear();
}
pub fn len(&self) -> usize {
self.trades.len()
}
pub fn is_empty(&self) -> bool {
self.trades.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::shared::{OrderBookId, Side};
use chrono::Utc;
use rust_decimal::Decimal;
fn make_trade(id: &str, price: f64, size: f64, sequence: u64) -> Trade {
Trade {
orderbook_id: OrderBookId::from("ob1"),
trade_id: id.to_string(),
cursor_id: id.parse().ok(),
timestamp: Utc::now(),
price: Decimal::try_from(price).unwrap(),
size: Decimal::try_from(size).unwrap(),
side: Side::Bid,
sequence,
}
}
#[test]
fn test_push_adds_trades() {
let mut th = TradeHistory::new(OrderBookId::from("ob1"), 10);
th.push(make_trade("t1", 50.0, 5.0, 0));
th.push(make_trade("t2", 51.0, 3.0, 0));
assert_eq!(th.len(), 2);
assert_eq!(th.latest().unwrap().trade_id, "t2");
}
#[test]
fn test_rolling_buffer_evicts_oldest() {
let mut th = TradeHistory::new(OrderBookId::from("ob1"), 3);
th.push(make_trade("t1", 50.0, 1.0, 0));
th.push(make_trade("t2", 51.0, 2.0, 0));
th.push(make_trade("t3", 52.0, 3.0, 0));
assert_eq!(th.len(), 3);
th.push(make_trade("t4", 53.0, 4.0, 0));
assert_eq!(th.len(), 3);
let ids: Vec<_> = th.trades().iter().map(|t| t.trade_id.as_str()).collect();
assert_eq!(ids, ["t4", "t3", "t2"]);
}
#[test]
fn test_push_reorders_by_sequence() {
let mut th = TradeHistory::new(OrderBookId::from("ob1"), 10);
th.push(make_trade("t3", 52.0, 3.0, 3));
th.push(make_trade("t1", 50.0, 1.0, 1));
th.push(make_trade("t2", 51.0, 2.0, 2));
let ids: Vec<_> = th.trades().iter().map(|t| t.trade_id.as_str()).collect();
assert_eq!(ids, ["t3", "t2", "t1"]);
}
#[test]
fn test_push_drops_older_sequence_when_full() {
let mut th = TradeHistory::new(OrderBookId::from("ob1"), 3);
th.push(make_trade("t5", 54.0, 5.0, 5));
th.push(make_trade("t4", 53.0, 4.0, 4));
th.push(make_trade("t3", 52.0, 3.0, 3));
th.push(make_trade("t2", 51.0, 2.0, 2));
let ids: Vec<_> = th.trades().iter().map(|t| t.trade_id.as_str()).collect();
assert_eq!(ids, ["t5", "t4", "t3"]);
}
#[test]
fn test_replace_clears_and_fills() {
let mut th = TradeHistory::new(OrderBookId::from("ob1"), 10);
th.push(make_trade("t1", 50.0, 1.0, 0));
th.replace(vec![
make_trade("a", 49.0, 1.0, 0),
make_trade("b", 50.0, 2.0, 0),
]);
assert_eq!(th.len(), 2);
assert_eq!(th.latest().unwrap().trade_id, "a");
}
}