Move 17 library crates into crates/, CLI binary into bin/fxt, consolidate 10 test crates into testing/, split config crate from deployment config files. Root directory reduced from 38+ to ~17 directories. All Cargo.toml paths and build.rs proto refs updated. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
772 lines
24 KiB
Rust
772 lines
24 KiB
Rust
//! Market data replay engine for historical backtesting
|
|
//!
|
|
//! Provides tick-by-tick historical market data replay with configurable speed,
|
|
//! filtering, and synchronization capabilities for strategy testing.
|
|
|
|
use std::{
|
|
sync::Arc,
|
|
time::{Duration as StdDuration, Instant},
|
|
};
|
|
|
|
use anyhow::{Context, Result};
|
|
use chrono::{DateTime, TimeDelta, Utc};
|
|
use common::{Price, Quantity, Symbol, Timestamp};
|
|
use rust_decimal::Decimal;
|
|
use trading_engine::types::events::MarketEvent;
|
|
// Channel imports removed as not used
|
|
use dashmap::DashMap;
|
|
use serde::{Deserialize, Serialize};
|
|
use tokio::{
|
|
fs::File,
|
|
io::{AsyncBufReadExt, BufReader},
|
|
sync::{mpsc, RwLock},
|
|
time::sleep,
|
|
};
|
|
use tracing::{error, info, warn};
|
|
|
|
/// Configuration for market data replay
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct ReplayConfig {
|
|
/// Speed multiplier for replay (1.0 = real-time, 0.0 = maximum speed)
|
|
pub speed_multiplier: f64,
|
|
/// Start time for replay
|
|
pub start_time: DateTime<Utc>,
|
|
/// End time for replay
|
|
pub end_time: DateTime<Utc>,
|
|
/// Symbols to include in replay
|
|
pub symbols: Vec<Symbol>,
|
|
/// Data sources to replay from
|
|
pub data_sources: Vec<DataSource>,
|
|
/// Buffer size for event queue
|
|
pub buffer_size: usize,
|
|
/// Enable tick-by-tick replay (vs aggregated bars)
|
|
pub tick_by_tick: bool,
|
|
/// Filter configuration
|
|
pub filters: ReplayFilters,
|
|
}
|
|
|
|
impl Default for ReplayConfig {
|
|
fn default() -> Self {
|
|
Self {
|
|
speed_multiplier: 1.0,
|
|
start_time: Utc::now() - TimeDelta::days(1),
|
|
end_time: Utc::now(),
|
|
symbols: Vec::new(),
|
|
data_sources: vec![DataSource::default()],
|
|
buffer_size: 10000,
|
|
tick_by_tick: true,
|
|
filters: ReplayFilters::default(),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Data source configuration
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct DataSource {
|
|
/// Source type (file, database, etc.)
|
|
pub source_type: SourceType,
|
|
/// Path or connection string
|
|
pub path: String,
|
|
/// Data format
|
|
pub format: DataFormat,
|
|
/// Priority for conflicting data
|
|
pub priority: u8,
|
|
}
|
|
|
|
impl Default for DataSource {
|
|
fn default() -> Self {
|
|
Self {
|
|
source_type: SourceType::CsvFile,
|
|
path: "data/market_data.csv".to_string(),
|
|
format: DataFormat::OhlcvTicks,
|
|
priority: 1,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Supported data source types
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub enum SourceType {
|
|
CsvFile,
|
|
ParquetFile,
|
|
Database,
|
|
BinaryFile,
|
|
}
|
|
|
|
/// Data format specifications
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub enum DataFormat {
|
|
OhlcvTicks,
|
|
L1BookUpdates,
|
|
L2BookUpdates,
|
|
TradeUpdates,
|
|
Custom(String),
|
|
}
|
|
|
|
/// Replay filtering configuration
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct ReplayFilters {
|
|
/// Minimum price change to include
|
|
pub min_price_change: Option<Decimal>,
|
|
/// Minimum volume to include
|
|
pub min_volume: Option<Quantity>,
|
|
/// Include only market hours
|
|
pub market_hours_only: bool,
|
|
/// Custom filter expressions
|
|
pub custom_filters: Vec<String>,
|
|
}
|
|
|
|
impl Default for ReplayFilters {
|
|
fn default() -> Self {
|
|
Self {
|
|
min_price_change: None,
|
|
min_volume: None,
|
|
market_hours_only: false,
|
|
custom_filters: Vec::new(),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Historical market event with metadata
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct ReplayEvent {
|
|
/// The market event
|
|
pub event: MarketEvent,
|
|
/// Original timestamp from data source
|
|
pub original_timestamp: Timestamp,
|
|
/// Replay timestamp (adjusted for speed)
|
|
pub replay_timestamp: Timestamp,
|
|
/// Data source identifier
|
|
pub source_id: String,
|
|
/// Event sequence number
|
|
pub sequence: u64,
|
|
}
|
|
|
|
/// Market data replay engine
|
|
pub struct MarketReplay {
|
|
/// Replay configuration
|
|
config: ReplayConfig,
|
|
/// Event output channel
|
|
_event_sender: mpsc::UnboundedSender<ReplayEvent>,
|
|
/// Event receiver for consumers
|
|
event_receiver: Arc<RwLock<Option<mpsc::UnboundedReceiver<ReplayEvent>>>>,
|
|
/// Current replay state
|
|
state: Arc<RwLock<ReplayState>>,
|
|
/// Symbol-specific order books
|
|
order_books: Arc<DashMap<Symbol, std::collections::HashMap<String, String>>>,
|
|
/// Performance metrics
|
|
metrics: Arc<ReplayMetrics>,
|
|
/// Event sequence counter
|
|
sequence_counter: Arc<std::sync::atomic::AtomicU64>,
|
|
}
|
|
|
|
/// Current state of replay engine
|
|
#[derive(Debug, Clone)]
|
|
pub struct ReplayState {
|
|
/// Current replay time
|
|
pub current_time: DateTime<Utc>,
|
|
/// Is replay active
|
|
pub is_active: bool,
|
|
/// Is replay paused
|
|
pub is_paused: bool,
|
|
/// Events processed count
|
|
pub events_processed: u64,
|
|
/// Replay start time (wall clock)
|
|
pub replay_start: Option<Instant>,
|
|
/// Last event timestamp
|
|
pub last_event_time: Option<DateTime<Utc>>,
|
|
}
|
|
|
|
impl Default for ReplayState {
|
|
fn default() -> Self {
|
|
Self {
|
|
current_time: Utc::now(),
|
|
is_active: false,
|
|
is_paused: false,
|
|
events_processed: 0,
|
|
replay_start: None,
|
|
last_event_time: None,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Performance metrics for replay engine
|
|
#[derive(Debug, Default)]
|
|
pub struct ReplayMetrics {
|
|
/// Events per second
|
|
pub events_per_second: Arc<std::sync::atomic::AtomicU64>,
|
|
/// Total events processed
|
|
pub total_events: Arc<std::sync::atomic::AtomicU64>,
|
|
/// Latency distribution
|
|
pub latency_histogram: Arc<RwLock<Vec<StdDuration>>>,
|
|
/// Memory usage
|
|
pub memory_usage: Arc<std::sync::atomic::AtomicUsize>,
|
|
/// Error count
|
|
pub error_count: Arc<std::sync::atomic::AtomicU64>,
|
|
}
|
|
|
|
impl MarketReplay {
|
|
/// Create new market replay engine
|
|
///
|
|
/// # Arguments
|
|
/// * `config` - Configuration for market data replay
|
|
///
|
|
/// # Returns
|
|
/// * `Self` - New market replay engine instance
|
|
pub fn new(config: ReplayConfig) -> Self {
|
|
let (_event_sender, event_receiver) = mpsc::unbounded_channel();
|
|
|
|
// Initialize state with config.start_time instead of Utc::now()
|
|
// to ensure consistent timestamps before start_replay() is called
|
|
let initial_state = ReplayState {
|
|
current_time: config.start_time,
|
|
is_active: false,
|
|
is_paused: false,
|
|
events_processed: 0,
|
|
replay_start: None,
|
|
last_event_time: None,
|
|
};
|
|
|
|
Self {
|
|
config,
|
|
_event_sender,
|
|
event_receiver: Arc::new(RwLock::new(Some(event_receiver))),
|
|
state: Arc::new(RwLock::new(initial_state)),
|
|
order_books: Arc::new(DashMap::new()),
|
|
metrics: Arc::new(ReplayMetrics::default()),
|
|
sequence_counter: Arc::new(std::sync::atomic::AtomicU64::new(0)),
|
|
}
|
|
}
|
|
|
|
/// Take the event receiver (can only be called once)
|
|
///
|
|
/// # Returns
|
|
/// * `Option<mpsc::UnboundedReceiver<ReplayEvent>>` - Event receiver for consuming replay events
|
|
///
|
|
/// # Note
|
|
///
|
|
/// This method can only be called once as it moves the receiver out of the engine
|
|
pub async fn take_receiver(&self) -> Option<mpsc::UnboundedReceiver<ReplayEvent>> {
|
|
self.event_receiver.write().await.take()
|
|
}
|
|
|
|
/// Start the replay process
|
|
///
|
|
/// # Returns
|
|
/// * `Result<()>` - Success or error from replay process
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// Returns error if data loading or replay fails
|
|
pub async fn start_replay(&self) -> Result<()> {
|
|
info!("Starting market data replay");
|
|
|
|
// Update state
|
|
{
|
|
let mut state = self.state.write().await;
|
|
state.is_active = true;
|
|
state.is_paused = false;
|
|
state.current_time = self.config.start_time;
|
|
state.replay_start = Some(Instant::now());
|
|
}
|
|
|
|
// Load and sort all data sources
|
|
let mut all_events = self.load_all_events().await?;
|
|
all_events.sort_by_key(|event| event.original_timestamp);
|
|
|
|
info!("Loaded {} events for replay", all_events.len());
|
|
|
|
// Start replay loop
|
|
self.replay_events(all_events).await?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Load events from all configured data sources
|
|
///
|
|
/// # Returns
|
|
/// * `Result<Vec<ReplayEvent>>` - All loaded and filtered events sorted by timestamp
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// Returns error if any data source fails to load
|
|
async fn load_all_events(&self) -> Result<Vec<ReplayEvent>> {
|
|
let mut all_events = Vec::new();
|
|
|
|
for (source_idx, source) in self.config.data_sources.iter().enumerate() {
|
|
match source.source_type {
|
|
SourceType::CsvFile => {
|
|
let events = self.load_csv_events(&source, source_idx).await?;
|
|
all_events.extend(events);
|
|
},
|
|
SourceType::ParquetFile => {
|
|
warn!("Parquet files not yet implemented");
|
|
},
|
|
SourceType::Database => {
|
|
warn!("Database sources not yet implemented");
|
|
},
|
|
SourceType::BinaryFile => {
|
|
warn!("Binary files not yet implemented");
|
|
},
|
|
}
|
|
}
|
|
|
|
// Apply filters
|
|
let filtered_events = self.apply_filters(all_events).await?;
|
|
|
|
Ok(filtered_events)
|
|
}
|
|
|
|
/// Load events from CSV file
|
|
///
|
|
/// # Arguments
|
|
/// * `source` - Data source configuration specifying the CSV file
|
|
///
|
|
/// * `source_idx` - Index of the data source for identification
|
|
///
|
|
/// # Returns
|
|
/// * `Result<Vec<ReplayEvent>>` - Events loaded from the CSV file
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// Returns error if file cannot be opened or parsed
|
|
async fn load_csv_events(
|
|
&self,
|
|
source: &DataSource,
|
|
source_idx: usize,
|
|
) -> Result<Vec<ReplayEvent>> {
|
|
let file = File::open(&source.path)
|
|
.await
|
|
.with_context(|| format!("Failed to open CSV file: {}", source.path))?;
|
|
|
|
let mut reader = BufReader::new(file);
|
|
let mut line = String::new();
|
|
let mut events = Vec::new();
|
|
let mut line_number = 0;
|
|
|
|
// Skip header
|
|
reader.read_line(&mut line).await?;
|
|
line.clear();
|
|
|
|
while reader.read_line(&mut line).await? > 0 {
|
|
line_number += 1;
|
|
|
|
if let Ok(event) = self.parse_csv_line(&line, source, source_idx).await {
|
|
events.push(event);
|
|
} else {
|
|
warn!("Failed to parse line {}: {}", line_number, line.trim());
|
|
}
|
|
|
|
line.clear();
|
|
}
|
|
|
|
Ok(events)
|
|
}
|
|
|
|
/// Parse a single CSV line into a replay event
|
|
///
|
|
/// # Arguments
|
|
/// * `line` - CSV line to parse
|
|
///
|
|
/// * `source` - Data source configuration for format specification
|
|
/// * `source_idx` - Index of the data source for identification
|
|
///
|
|
/// # Returns
|
|
/// * `Result<ReplayEvent>` - Parsed replay event
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// Returns error if line format is invalid or cannot be parsed
|
|
async fn parse_csv_line(
|
|
&self,
|
|
line: &str,
|
|
source: &DataSource,
|
|
source_idx: usize,
|
|
) -> Result<ReplayEvent> {
|
|
let fields: Vec<&str> = line.trim().split(',').collect();
|
|
|
|
match source.format {
|
|
DataFormat::OhlcvTicks => {
|
|
if fields.len() < 7 {
|
|
return Err(anyhow::anyhow!(
|
|
"Invalid OHLCV format: need at least 7 fields"
|
|
));
|
|
}
|
|
|
|
let timestamp: i64 = fields[0].parse()?;
|
|
let symbol = Symbol::new(fields[1].to_string());
|
|
let _open: Decimal = fields[2].parse()?;
|
|
let _high: Decimal = fields[3].parse()?;
|
|
let _low: Decimal = fields[4].parse()?;
|
|
let close: Decimal = fields[5].parse()?;
|
|
let volume: Decimal = fields[6].parse()?;
|
|
|
|
let market_event = MarketEvent::Trade {
|
|
symbol: symbol.clone(),
|
|
price: Price::from_f64(close.try_into().unwrap_or(0.0))?,
|
|
size: Quantity::from_f64(volume.try_into().unwrap_or(0.0))?,
|
|
timestamp: DateTime::from_timestamp(
|
|
timestamp / 1000,
|
|
((timestamp % 1000) * 1_000_000) as u32,
|
|
)
|
|
.unwrap_or_default(),
|
|
side: None,
|
|
venue: None,
|
|
trade_id: None,
|
|
};
|
|
|
|
let sequence = self
|
|
.sequence_counter
|
|
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
|
|
|
Ok(ReplayEvent {
|
|
event: market_event,
|
|
original_timestamp: DateTime::from_timestamp(
|
|
timestamp / 1000,
|
|
((timestamp % 1000) * 1_000_000) as u32,
|
|
)
|
|
.unwrap_or_default(),
|
|
replay_timestamp: Utc::now(),
|
|
source_id: format!("source_{}", source_idx),
|
|
sequence,
|
|
})
|
|
},
|
|
_ => Err(anyhow::anyhow!(
|
|
"Unsupported data format: {:?}",
|
|
source.format
|
|
)),
|
|
}
|
|
}
|
|
|
|
/// Apply configured filters to events
|
|
///
|
|
/// # Arguments
|
|
/// * `events` - Vector of events to filter
|
|
///
|
|
/// # Returns
|
|
/// * `Result<Vec<ReplayEvent>>` - Filtered events that meet filter criteria
|
|
async fn apply_filters(&self, events: Vec<ReplayEvent>) -> Result<Vec<ReplayEvent>> {
|
|
let mut filtered = Vec::new();
|
|
let total_events = events.len(); // Store length before moving events
|
|
|
|
for event in events {
|
|
if self.should_include_event(&event).await {
|
|
filtered.push(event);
|
|
}
|
|
}
|
|
|
|
info!(
|
|
"Filtered {} events from {} total",
|
|
filtered.len(),
|
|
total_events
|
|
);
|
|
Ok(filtered)
|
|
}
|
|
|
|
/// Check if event should be included based on filters
|
|
///
|
|
/// # Arguments
|
|
/// * `event` - Event to check against filter criteria
|
|
///
|
|
/// # Returns
|
|
/// * `bool` - True if event should be included, false otherwise
|
|
async fn should_include_event(&self, event: &ReplayEvent) -> bool {
|
|
// Time range filter
|
|
let event_time = DateTime::from_timestamp(
|
|
event.original_timestamp.timestamp(),
|
|
event.original_timestamp.timestamp_subsec_nanos(),
|
|
)
|
|
.unwrap_or_default();
|
|
|
|
if event_time < self.config.start_time || event_time > self.config.end_time {
|
|
return false;
|
|
}
|
|
|
|
// Symbol filter
|
|
if !self.config.symbols.is_empty() {
|
|
let event_symbol = match &event.event {
|
|
MarketEvent::Trade { symbol, .. } => symbol,
|
|
MarketEvent::Quote { symbol, .. } => symbol,
|
|
MarketEvent::OrderBookUpdate { symbol, .. } => symbol,
|
|
MarketEvent::Bar { symbol, .. } => symbol,
|
|
MarketEvent::OrderBook { symbol, .. } => symbol,
|
|
MarketEvent::Sentiment { .. } => return true, // Include sentiment events for now
|
|
MarketEvent::Control { .. } => return true, // Include control events for now
|
|
};
|
|
|
|
if !self.config.symbols.contains(event_symbol) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Volume filter
|
|
if let Some(min_volume) = &self.config.filters.min_volume {
|
|
let event_volume = match &event.event {
|
|
MarketEvent::Trade { size, .. } => Some(size),
|
|
_ => None,
|
|
};
|
|
|
|
if let Some(volume) = event_volume {
|
|
if volume < min_volume {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
true
|
|
}
|
|
|
|
/// Replay events with timing control
|
|
///
|
|
/// # Arguments
|
|
/// * `events` - Vector of events to replay in chronological order
|
|
///
|
|
/// # Returns
|
|
/// * `Result<()>` - Success or error from replay process
|
|
///
|
|
/// # Note
|
|
///
|
|
/// Respects speed multiplier for timing and handles pause/resume functionality
|
|
async fn replay_events(&self, events: Vec<ReplayEvent>) -> Result<()> {
|
|
let mut last_event_time: Option<DateTime<Utc>> = None;
|
|
let _replay_start = Instant::now();
|
|
|
|
for event in events {
|
|
// Check if replay should continue
|
|
{
|
|
let state = self.state.read().await;
|
|
if !state.is_active {
|
|
break;
|
|
}
|
|
|
|
// Handle pause
|
|
while state.is_paused {
|
|
sleep(StdDuration::from_millis(100)).await;
|
|
}
|
|
}
|
|
|
|
// Calculate timing
|
|
let event_time = DateTime::from_timestamp(
|
|
event.original_timestamp.timestamp(),
|
|
event.original_timestamp.timestamp_subsec_nanos(),
|
|
)
|
|
.unwrap_or_default();
|
|
|
|
if let Some(last_time) = last_event_time {
|
|
let time_diff = event_time.signed_duration_since(last_time);
|
|
if time_diff > TimeDelta::zero() && self.config.speed_multiplier > 0.0 {
|
|
let sleep_duration = StdDuration::from_millis(
|
|
((time_diff.num_milliseconds() as f64) / self.config.speed_multiplier)
|
|
as u64,
|
|
);
|
|
sleep(sleep_duration).await;
|
|
}
|
|
}
|
|
|
|
// Update order book if applicable
|
|
self.update_order_book(&event).await;
|
|
|
|
// Send event
|
|
if let Err(e) = self._event_sender.send(event.clone()) {
|
|
error!("Failed to send replay event: {}", e);
|
|
break;
|
|
}
|
|
|
|
// Update metrics and state
|
|
self.update_metrics(&event).await;
|
|
self.update_state(event_time).await;
|
|
|
|
last_event_time = Some(event_time);
|
|
}
|
|
|
|
// Mark replay as complete
|
|
{
|
|
let mut state = self.state.write().await;
|
|
state.is_active = false;
|
|
}
|
|
|
|
info!("Market data replay completed");
|
|
Ok(())
|
|
}
|
|
|
|
/// Update order book with new event
|
|
///
|
|
/// # Arguments
|
|
/// * `event` - Replay event that may contain order book updates
|
|
///
|
|
/// # Note
|
|
///
|
|
/// Currently handles basic order book tracking for trade and order book events
|
|
async fn update_order_book(&self, event: &ReplayEvent) {
|
|
match &event.event {
|
|
MarketEvent::OrderBookUpdate { symbol, .. } => {
|
|
// Update order book logic would go here
|
|
// For now, just ensure the symbol exists
|
|
self.order_books
|
|
.entry(symbol.clone())
|
|
.or_insert_with(std::collections::HashMap::new);
|
|
},
|
|
MarketEvent::Trade {
|
|
symbol,
|
|
price: _price,
|
|
..
|
|
} => {
|
|
// Update last trade price in order book
|
|
if let Some(mut _book) = self.order_books.get_mut(symbol) {
|
|
// Update last trade price logic
|
|
}
|
|
},
|
|
_ => {},
|
|
}
|
|
}
|
|
|
|
/// Update performance metrics
|
|
///
|
|
/// # Arguments
|
|
/// * `_event` - Replay event (currently unused but reserved for future metrics)
|
|
///
|
|
/// # Note
|
|
///
|
|
/// Updates event count and calculates events per second
|
|
async fn update_metrics(&self, _event: &ReplayEvent) {
|
|
self.metrics
|
|
.total_events
|
|
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
|
|
|
// Update events per second calculation
|
|
// Implementation would track timing for EPS calculation
|
|
}
|
|
|
|
/// Update replay state
|
|
///
|
|
/// # Arguments
|
|
/// * `event_time` - Timestamp of the current event being processed
|
|
///
|
|
/// # Note
|
|
///
|
|
/// Updates current time, event count, and last event timestamp
|
|
async fn update_state(&self, event_time: DateTime<Utc>) {
|
|
let mut state = self.state.write().await;
|
|
state.current_time = event_time;
|
|
state.events_processed += 1;
|
|
state.last_event_time = Some(event_time);
|
|
}
|
|
|
|
/// Pause the replay
|
|
///
|
|
/// # Note
|
|
///
|
|
/// Sets the replay state to paused, causing event processing to halt until resumed
|
|
pub async fn pause(&self) {
|
|
let mut state = self.state.write().await;
|
|
state.is_paused = true;
|
|
info!("Market replay paused");
|
|
}
|
|
|
|
/// Resume the replay
|
|
///
|
|
/// # Note
|
|
///
|
|
/// Clears the paused state, allowing event processing to continue
|
|
pub async fn resume(&self) {
|
|
let mut state = self.state.write().await;
|
|
state.is_paused = false;
|
|
info!("Market replay resumed");
|
|
}
|
|
|
|
/// Stop the replay
|
|
///
|
|
/// # Note
|
|
///
|
|
/// Completely stops the replay process and clears both active and paused states
|
|
pub async fn stop(&self) {
|
|
let mut state = self.state.write().await;
|
|
state.is_active = false;
|
|
state.is_paused = false;
|
|
info!("Market replay stopped");
|
|
}
|
|
|
|
/// Get current replay state
|
|
///
|
|
/// # Returns
|
|
/// * `ReplayState` - Current state including timing, event counts, and status flags
|
|
pub async fn get_state(&self) -> ReplayState {
|
|
self.state.read().await.clone()
|
|
}
|
|
|
|
/// Get current order book for symbol
|
|
///
|
|
/// # Arguments
|
|
/// * `symbol` - Symbol to retrieve order book for
|
|
///
|
|
/// # Returns
|
|
/// * `Option<HashMap<String, String>>` - Order book data if available for the symbol
|
|
pub async fn get_order_book(
|
|
&self,
|
|
symbol: &Symbol,
|
|
) -> Option<std::collections::HashMap<String, String>> {
|
|
self.order_books.get(symbol).map(|book| book.clone())
|
|
}
|
|
|
|
/// Get performance metrics
|
|
///
|
|
/// # Returns
|
|
/// * `ReplayMetrics` - Current performance metrics including event rates and error counts
|
|
pub async fn get_metrics(&self) -> ReplayMetrics {
|
|
ReplayMetrics {
|
|
events_per_second: Arc::clone(&self.metrics.events_per_second),
|
|
total_events: Arc::clone(&self.metrics.total_events),
|
|
latency_histogram: Arc::clone(&self.metrics.latency_histogram),
|
|
memory_usage: Arc::clone(&self.metrics.memory_usage),
|
|
error_count: Arc::clone(&self.metrics.error_count),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use std::io::Write;
|
|
use tempfile::NamedTempFile;
|
|
|
|
#[tokio::test]
|
|
async fn test_replay_engine_creation() {
|
|
let config = ReplayConfig::default();
|
|
let replay = MarketReplay::new(config);
|
|
|
|
let state = replay.get_state().await;
|
|
assert!(!state.is_active);
|
|
assert!(!state.is_paused);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_csv_loading() {
|
|
// Create test CSV file
|
|
let mut temp_file = NamedTempFile::new().unwrap();
|
|
writeln!(temp_file, "timestamp,symbol,open,high,low,close,volume").unwrap();
|
|
writeln!(
|
|
temp_file,
|
|
"1609459200000,BTCUSD,29000.0,29100.0,28900.0,29050.0,1.5"
|
|
)
|
|
.unwrap();
|
|
|
|
let config = ReplayConfig {
|
|
data_sources: vec![DataSource {
|
|
source_type: SourceType::CsvFile,
|
|
path: temp_file.path().to_string_lossy().to_string(),
|
|
format: DataFormat::OhlcvTicks,
|
|
priority: 1,
|
|
}],
|
|
start_time: DateTime::from_timestamp(1609459200, 0).unwrap_or_default(),
|
|
end_time: DateTime::from_timestamp(1609459300, 0).unwrap_or_default(),
|
|
..Default::default()
|
|
};
|
|
|
|
let replay = MarketReplay::new(config);
|
|
let events = replay.load_all_events().await.unwrap();
|
|
|
|
assert_eq!(events.len(), 1);
|
|
assert_eq!(events[0].sequence, 0);
|
|
}
|
|
}
|