#![allow(clippy::clone_on_copy)]
use opendeviationbar_core::Tick;
use std::collections::VecDeque;
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use tokio::time::sleep;
use tokio_stream::Stream;
#[derive(Debug, Clone)]
pub struct ReplayBuffer {
inner: Arc<Mutex<ReplayBufferInner>>,
}
#[derive(Debug)]
struct ReplayBufferInner {
capacity: Duration,
trades: VecDeque<Tick>,
start_time: Option<Instant>,
}
impl ReplayBuffer {
pub fn new(capacity: Duration) -> Self {
Self {
inner: Arc::new(Mutex::new(ReplayBufferInner {
capacity,
trades: VecDeque::new(),
start_time: None,
})),
}
}
pub fn push(&self, trade: Tick) {
let mut inner = self
.inner
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
if inner.start_time.is_none() {
inner.start_time = Some(Instant::now());
}
let cutoff_timestamp = trade.timestamp - (inner.capacity.as_micros() as i64);
while let Some(front_trade) = inner.trades.front() {
if front_trade.timestamp < cutoff_timestamp {
inner.trades.pop_front();
} else {
break;
}
}
inner.trades.push_back(trade);
}
pub fn len(&self) -> usize {
self.inner
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.trades
.len()
}
pub fn is_empty(&self) -> bool {
self.inner
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.trades
.is_empty()
}
pub fn time_span(&self) -> Option<Duration> {
let inner = self
.inner
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
if let (Some(first), Some(last)) = (inner.trades.front(), inner.trades.back()) {
let span_microseconds = last.timestamp - first.timestamp;
if span_microseconds > 0 {
Some(Duration::from_micros(span_microseconds as u64))
} else {
None
}
} else {
None
}
}
pub fn get_trades_from(&self, minutes_ago: u32) -> Vec<Tick> {
let inner = self
.inner
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
if inner.trades.is_empty() {
return Vec::new();
}
let latest_timestamp = inner.trades.back().unwrap().timestamp;
let cutoff_timestamp = latest_timestamp - (minutes_ago as i64 * 60 * 1000);
let mut start_idx = 0;
for (idx, trade) in inner.trades.iter().enumerate() {
if trade.timestamp >= cutoff_timestamp {
start_idx = idx;
break;
}
}
let mut result = Vec::with_capacity(inner.trades.len().saturating_sub(start_idx));
result.extend(inner.trades.iter().skip(start_idx).cloned());
result
}
pub fn replay_from(&self, minutes_ago: u32, speed_multiplier: f32) -> ReplayStream {
let trades = self.get_trades_from(minutes_ago);
ReplayStream::new(trades, speed_multiplier)
}
pub fn stats(&self) -> ReplayBufferStats {
let inner = self
.inner
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
let (first_timestamp, last_timestamp) =
if let (Some(first), Some(last)) = (inner.trades.front(), inner.trades.back()) {
(Some(first.timestamp), Some(last.timestamp))
} else {
(None, None)
};
ReplayBufferStats {
capacity: inner.capacity,
trade_count: inner.trades.len(),
first_timestamp,
last_timestamp,
memory_usage_bytes: inner.trades.len() * std::mem::size_of::<Tick>(),
}
}
}
#[derive(Debug, Clone)]
pub struct ReplayBufferStats {
pub capacity: Duration,
pub trade_count: usize,
pub first_timestamp: Option<i64>,
pub last_timestamp: Option<i64>,
pub memory_usage_bytes: usize,
}
pub struct ReplayStream {
trades: Vec<Tick>,
current_index: usize,
speed_multiplier: f32,
base_timestamp: Option<i64>,
start_time: Option<Instant>,
}
impl ReplayStream {
pub fn new(trades: Vec<Tick>, speed_multiplier: f32) -> Self {
let base_timestamp = trades.first().map(|t| t.timestamp);
Self {
trades,
current_index: 0,
speed_multiplier: speed_multiplier.max(0.1), base_timestamp,
start_time: None,
}
}
pub fn set_speed(&mut self, speed_multiplier: f32) {
self.speed_multiplier = speed_multiplier.max(0.1);
}
pub fn speed(&self) -> f32 {
self.speed_multiplier
}
pub fn remaining(&self) -> usize {
self.trades.len().saturating_sub(self.current_index)
}
pub fn total(&self) -> usize {
self.trades.len()
}
pub fn progress(&self) -> f32 {
if self.trades.is_empty() {
1.0
} else {
self.current_index as f32 / self.trades.len() as f32
}
}
}
impl Stream for ReplayStream {
type Item = Tick;
fn poll_next(
mut self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Option<Self::Item>> {
if self.current_index >= self.trades.len() {
return std::task::Poll::Ready(None);
}
if self.start_time.is_none() {
let current_trade = self.trades[self.current_index].clone();
self.start_time = Some(Instant::now());
self.current_index += 1;
return std::task::Poll::Ready(Some(current_trade));
}
let current_trade = &self.trades[self.current_index];
if let (Some(base_timestamp), Some(start_time)) = (self.base_timestamp, self.start_time) {
let time_diff_microseconds = current_trade.timestamp - base_timestamp;
let real_time_diff = Duration::from_micros(time_diff_microseconds as u64);
let scaled_time_diff = Duration::from_micros(
(real_time_diff.as_micros() as f64 / self.speed_multiplier as f64) as u64,
);
let target_time = start_time + scaled_time_diff;
let now = Instant::now();
if now >= target_time {
let trade = current_trade.clone();
self.current_index += 1;
std::task::Poll::Ready(Some(trade))
} else {
let waker = cx.waker().clone();
let sleep_duration = target_time - now;
tokio::spawn(async move {
sleep(sleep_duration).await;
waker.wake();
});
std::task::Poll::Pending
}
} else {
let trade = current_trade.clone();
self.current_index += 1;
std::task::Poll::Ready(Some(trade))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use futures::StreamExt;
use opendeviationbar_core::FixedPoint;
fn create_test_trade(id: i64, timestamp: i64, price: f64) -> Tick {
Tick {
ref_id: id,
price: FixedPoint::from_str(&price.to_string()).unwrap(),
volume: FixedPoint::from_str("1.0").unwrap(),
first_sub_id: id,
last_sub_id: id,
timestamp,
is_buyer_maker: false,
is_best_match: None,
best_bid: None,
best_ask: None,
}
}
#[test]
fn test_replay_buffer_capacity() {
let buffer = ReplayBuffer::new(Duration::from_secs(60));
let base_time = 1_704_067_200_000_000_i64;
for i in 0..120 {
let trade = create_test_trade(i, base_time + (i * 1_000_000), 50000.0); buffer.push(trade);
}
let stats = buffer.stats();
assert!(
stats.trade_count <= 120,
"Expected <= 120 trades, got {}",
stats.trade_count
);
let trades = buffer.get_trades_from(1); assert!(!trades.is_empty());
}
#[test]
fn test_replay_buffer_time_span() {
let buffer = ReplayBuffer::new(Duration::from_secs(300));
let base_time = 1_704_067_200_000_000_i64;
buffer.push(create_test_trade(1, base_time, 50000.0));
buffer.push(create_test_trade(2, base_time + 30_000_000, 50100.0)); buffer.push(create_test_trade(3, base_time + 60_000_000, 50200.0));
let span = buffer.time_span().unwrap();
assert_eq!(span.as_secs(), 60);
}
#[tokio::test]
async fn test_replay_stream() {
let base_time = 1_704_067_200_000_000_i64; let trades = vec![
create_test_trade(1, base_time, 50000.0),
create_test_trade(2, base_time + 1_000_000, 50100.0), create_test_trade(3, base_time + 2_000_000, 50200.0), ];
let mut stream = ReplayStream::new(trades, 10.0); assert_eq!(stream.total(), 3);
assert_eq!(stream.remaining(), 3);
assert_eq!(stream.progress(), 0.0);
let first = StreamExt::next(&mut stream).await;
assert!(first.is_some());
assert_eq!(first.unwrap().ref_id, 1);
}
#[test]
fn test_get_trades_from_empty_buffer() {
let buffer = ReplayBuffer::new(Duration::from_secs(60));
let trades = buffer.get_trades_from(1);
assert_eq!(trades.len(), 0); }
#[test]
fn test_push_len_is_empty() {
let buffer = ReplayBuffer::new(Duration::from_secs(300));
assert!(buffer.is_empty());
assert_eq!(buffer.len(), 0);
let base = 1_704_067_200_000_000_i64;
buffer.push(create_test_trade(1, base, 50000.0));
assert!(!buffer.is_empty());
assert_eq!(buffer.len(), 1);
buffer.push(create_test_trade(2, base + 1_000_000, 50100.0));
assert_eq!(buffer.len(), 2);
}
#[test]
fn test_push_eviction_by_capacity() {
let buffer = ReplayBuffer::new(Duration::from_secs(10));
let base = 1_704_067_200_000_000_i64;
for i in 0..20 {
buffer.push(create_test_trade(i, base + (i * 1_000_000), 50000.0));
}
let len = buffer.len();
assert!(len <= 12, "Expected <=12 trades after eviction, got {len}");
assert!(len >= 10, "Expected >=10 trades retained, got {len}");
}
#[test]
fn test_replay_stream_set_speed() {
let trades = vec![
create_test_trade(1, 1_704_067_200_000_000, 50000.0),
create_test_trade(2, 1_704_067_201_000_000, 50100.0),
];
let mut stream = ReplayStream::new(trades, 1.0);
assert!((stream.speed() - 1.0).abs() < f32::EPSILON);
stream.set_speed(5.0);
assert!((stream.speed() - 5.0).abs() < f32::EPSILON);
stream.set_speed(0.01);
assert!((stream.speed() - 0.1).abs() < f32::EPSILON);
}
#[test]
fn test_replay_stream_remaining_total_progress() {
let base = 1_704_067_200_000_000_i64;
let trades = vec![
create_test_trade(1, base, 50000.0),
create_test_trade(2, base + 1_000_000, 50100.0),
create_test_trade(3, base + 2_000_000, 50200.0),
create_test_trade(4, base + 3_000_000, 50300.0),
];
let stream = ReplayStream::new(trades, 10.0);
assert_eq!(stream.total(), 4);
assert_eq!(stream.remaining(), 4);
assert!((stream.progress() - 0.0).abs() < f32::EPSILON);
}
#[test]
fn test_replay_stream_empty_progress() {
let stream = ReplayStream::new(vec![], 1.0);
assert_eq!(stream.total(), 0);
assert_eq!(stream.remaining(), 0);
assert!((stream.progress() - 1.0).abs() < f32::EPSILON);
}
#[test]
fn test_buffer_stats() {
let buffer = ReplayBuffer::new(Duration::from_secs(60));
let base = 1_704_067_200_000_000_i64;
let stats = buffer.stats();
assert_eq!(stats.trade_count, 0);
assert!(stats.first_timestamp.is_none());
assert!(stats.last_timestamp.is_none());
buffer.push(create_test_trade(1, base, 50000.0));
buffer.push(create_test_trade(2, base + 5_000_000, 50100.0));
let stats = buffer.stats();
assert_eq!(stats.trade_count, 2);
assert_eq!(stats.first_timestamp, Some(base));
assert_eq!(stats.last_timestamp, Some(base + 5_000_000));
assert!(stats.memory_usage_bytes > 0);
}
}