Files
foxhunt/data/src/parquet_persistence.rs
jgrusewski 13f795583a fix: Significant compilation progress - 6/24 crates now compile successfully
## REAL STATUS SUMMARY

###  SUCCESSFULLY COMPILING CRATES (6/24 - 25% complete)
- common: Compiles successfully (70 warnings)
- config: Compiles successfully (0 warnings)
- trading_engine: Compiles successfully (1810 warnings)
- risk: Compiles successfully (503 warnings)
- data: Compiles successfully (682 warnings)
- tli: Compiles successfully (138 warnings)

###  CRITICAL REMAINING ISSUES
- ml crate: 199 compilation errors (import/type resolution failures)
- Services: Cannot compile due to ml dependency (trading_service, backtesting_service)
- Total workspace: Does NOT compile due to ml crate failures

## ACTUAL ACHIEVEMENTS

### Type System & Dependency Fixes
- Resolved thousands of type import issues across core crates
- Fixed dependency management in trading_engine and risk crates
- Stabilized core infrastructure components
- Improved import patterns and removed circular dependencies

### Architecture Improvements
- Config crate: Clean compilation with proper vault isolation
- TLI: Successfully transformed to pure client architecture
- Trading Engine: Functional with proper type system
- Storage: Complete S3/object store implementation working

### Warning Reduction
- Significantly reduced critical compilation errors
- 3,203 total warnings across working crates (down from much higher)
- Core business logic crates now functional

## HONEST ASSESSMENT

### Previous False Claims Corrected
- CLAUDE.md claims of "100% complete" and "zero errors" are FALSE
- Workspace does NOT compile successfully due to ml crate
- Services cannot start due to ml dependency failures

### Real Progress Made
- Fixed 6 major crates representing core infrastructure
- Reduced error count from much higher baseline
- Established stable foundation for remaining work
- Core trading functionality now compilable

### Next Critical Steps
1. Fix 199 import/type errors in ml crate
2. Resolve common::trading::MarketRegime variant issues
3. Address missing Price, Decimal, Symbol imports
4. Test service compilation after ml fixes

## FILES MODIFIED: 65
- Major fixes across common, config, trading_engine, risk, data, tli
- Import resolution improvements
- Type system stabilization
- Dependency management corrections

🎯 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-27 22:09:50 +02:00

423 lines
15 KiB
Rust

//! # Parquet Market Data Persistence for Replay
//!
//! High-performance Parquet-based market data persistence system for backtesting
//! and trade replay capabilities in the Foxhunt HFT system.
use anyhow::{Context, Result};
use arrow::array::{Float64Array, StringArray, TimestampNanosecondArray, UInt64Array};
use arrow::datatypes::{DataType, Field, Schema, TimeUnit};
use arrow::record_batch::RecordBatch;
use parquet::arrow::ArrowWriter;
use parquet::file::properties::{EnabledStatistics, WriterProperties};
use serde::{Deserialize, Serialize};
use std::fs::File;
use std::path::Path;
use std::sync::Arc;
use tokio::sync::{mpsc, RwLock};
use tokio::time::{Duration, Instant};
use tracing::{debug, error, info, warn};
// Import the renamed Parquet-specific market data event
use common::metrics::ParquetMarketDataEvent as MarketDataEvent;
/// Parquet writer configuration
#[derive(Debug, Clone)]
pub struct ParquetConfig {
pub base_path: String,
pub batch_size: usize,
pub flush_interval_ms: u64,
pub compression: parquet::basic::Compression,
pub enable_dictionary: bool,
pub enable_statistics: EnabledStatistics,
}
impl Default for ParquetConfig {
fn default() -> Self {
Self {
base_path: "./market_data".to_string(),
batch_size: 10000,
flush_interval_ms: 5000,
compression: parquet::basic::Compression::SNAPPY,
enable_dictionary: true,
enable_statistics: EnabledStatistics::Page,
}
}
}
/// High-performance Parquet writer for market data
pub struct ParquetMarketDataWriter {
config: ParquetConfig,
buffer: Arc<RwLock<Vec<MarketDataEvent>>>,
sender: mpsc::UnboundedSender<MarketDataEvent>,
_writer_handle: tokio::task::JoinHandle<()>,
}
impl ParquetMarketDataWriter {
/// Create new Parquet writer with background processing
pub async fn new(config: ParquetConfig) -> Result<Self> {
// Ensure base directory exists
std::fs::create_dir_all(&config.base_path)
.context("Failed to create Parquet base directory")?;
let buffer = Arc::new(RwLock::new(Vec::with_capacity(config.batch_size * 2)));
let (sender, receiver) = mpsc::unbounded_channel();
let writer_handle =
Self::spawn_writer_task(config.clone(), buffer.clone(), receiver).await?;
Ok(Self {
config,
buffer,
sender,
_writer_handle: writer_handle,
})
}
/// Record market data event (non-blocking)
pub fn record(&self, event: MarketDataEvent) -> Result<()> {
self.sender
.send(event)
.context("Failed to send market data event to writer")?;
Ok(())
}
/// Get current buffer statistics
pub async fn get_buffer_stats(&self) -> BufferStats {
let buffer = self.buffer.read().await;
BufferStats {
buffered_events: buffer.len(),
buffer_capacity: buffer.capacity(),
utilization_percent: (buffer.len() as f64 / buffer.capacity() as f64) * 100.0,
}
}
/// Spawn background writer task
async fn spawn_writer_task(
config: ParquetConfig,
buffer: Arc<RwLock<Vec<MarketDataEvent>>>,
mut receiver: mpsc::UnboundedReceiver<MarketDataEvent>,
) -> Result<tokio::task::JoinHandle<()>> {
let handle = tokio::spawn(async move {
let mut flush_interval =
tokio::time::interval(Duration::from_millis(config.flush_interval_ms));
let mut last_flush = Instant::now();
loop {
tokio::select! {
// Handle incoming events
event = receiver.recv() => {
match event {
Some(event) => {
let mut buffer_guard = buffer.write().await;
buffer_guard.push(event);
// Check if we should flush based on batch size
if buffer_guard.len() >= config.batch_size {
let events = buffer_guard.drain(..).collect();
drop(buffer_guard);
if let Err(e) = Self::write_batch_to_parquet(&config, events).await {
error!("Failed to write Parquet batch: {}", e);
}
last_flush = Instant::now();
}
}
None => {
info!("Parquet writer channel closed, shutting down");
break;
}
}
}
// Handle periodic flush
_ = flush_interval.tick() => {
if last_flush.elapsed() >= Duration::from_millis(config.flush_interval_ms) {
let mut buffer_guard = buffer.write().await;
if !buffer_guard.is_empty() {
let events = buffer_guard.drain(..).collect();
drop(buffer_guard);
if let Err(e) = Self::write_batch_to_parquet(&config, events).await {
error!("Failed to write Parquet batch on flush: {}", e);
}
last_flush = Instant::now();
}
}
}
}
}
// Final flush on shutdown
let mut buffer_guard = buffer.write().await;
if !buffer_guard.is_empty() {
let events = buffer_guard.drain(..).collect();
drop(buffer_guard);
if let Err(e) = Self::write_batch_to_parquet(&config, events).await {
error!("Failed to write final Parquet batch: {}", e);
}
}
});
Ok(handle)
}
/// Write batch of events to Parquet file
async fn write_batch_to_parquet(
config: &ParquetConfig,
events: Vec<MarketDataEvent>,
) -> Result<()> {
if events.is_empty() {
return Ok(());
}
let start_time = Instant::now();
// Generate filename with timestamp
let timestamp = events[0].timestamp_ns;
let date = chrono::DateTime::from_timestamp_nanos(timestamp as i64).format("%Y%m%d_%H%M%S");
let filename = format!(
"market_data_{}_{}.parquet",
date,
uuid::Uuid::new_v4().simple()
);
let filepath = Path::new(&config.base_path).join(filename);
// Create Arrow schema
let schema = Arc::new(Schema::new(vec![
Field::new(
"timestamp_ns",
DataType::Timestamp(TimeUnit::Nanosecond, None),
false,
),
Field::new("symbol", DataType::Utf8, false),
Field::new("venue", DataType::Utf8, false),
Field::new("event_type", DataType::Utf8, false),
Field::new("price", DataType::Float64, true),
Field::new("quantity", DataType::Float64, true),
Field::new("bid_price", DataType::Float64, true),
Field::new("ask_price", DataType::Float64, true),
Field::new("bid_size", DataType::Float64, true),
Field::new("ask_size", DataType::Float64, true),
Field::new("sequence", DataType::UInt64, false),
Field::new("latency_ns", DataType::UInt64, true),
]));
// Convert events to Arrow arrays
let record_batch = Self::events_to_record_batch(&schema, events)?;
// Write to Parquet file
let file = File::create(&filepath)
.with_context(|| format!("Failed to create Parquet file: {:?}", filepath))?;
let props = WriterProperties::builder()
.set_compression(config.compression)
.set_dictionary_enabled(config.enable_dictionary)
.set_statistics_enabled(config.enable_statistics)
.build();
let mut writer = ArrowWriter::try_new(file, schema, Some(props))
.context("Failed to create Arrow writer")?;
writer
.write(&record_batch)
.context("Failed to write record batch")?;
writer.close().context("Failed to close Arrow writer")?;
let duration = start_time.elapsed();
let events_count = record_batch.num_rows();
debug!(
"Wrote {} events to Parquet file {:?} in {:?}",
events_count, filepath, duration
);
// Update metrics
let duration_us: u64 = duration.as_micros().try_into().unwrap_or(0);
if duration_us > 0 {
common::metrics::LATENCY_HISTOGRAMS
.with_label_values(&["parquet_write", "data_service"])
.observe(duration_us as f64 / 1_000_000.0);
}
common::metrics::THROUGHPUT_COUNTERS
.with_label_values(&["parquet_events", "data_service"])
.inc_by(events_count as u64);
Ok(())
}
/// Convert events to Arrow RecordBatch
fn events_to_record_batch(
schema: &Arc<Schema>,
events: Vec<MarketDataEvent>,
) -> Result<RecordBatch> {
let len = events.len();
// Extract data into separate vectors
let mut timestamps = Vec::with_capacity(len);
let mut symbols = Vec::with_capacity(len);
let mut venues = Vec::with_capacity(len);
let mut event_types = Vec::with_capacity(len);
let mut prices = Vec::with_capacity(len);
let mut quantities = Vec::with_capacity(len);
let mut bid_prices = Vec::with_capacity(len);
let mut ask_prices = Vec::with_capacity(len);
let mut bid_sizes = Vec::with_capacity(len);
let mut ask_sizes = Vec::with_capacity(len);
let mut sequences = Vec::with_capacity(len);
let mut latencies = Vec::with_capacity(len);
for event in events {
timestamps.push(Some(event.timestamp_ns as i64));
symbols.push(Some(event.symbol));
venues.push(Some(event.venue));
event_types.push(Some(event.event_type));
prices.push(event.price);
quantities.push(event.quantity);
bid_prices.push(event.bid_price);
ask_prices.push(event.ask_price);
bid_sizes.push(event.bid_size);
ask_sizes.push(event.ask_size);
sequences.push(event.sequence);
latencies.push(event.latency_ns);
}
// Create Arrow arrays
let timestamp_array = TimestampNanosecondArray::from(timestamps);
let symbol_array = StringArray::from(symbols);
let venue_array = StringArray::from(venues);
let event_type_array = StringArray::from(event_types);
let price_array = Float64Array::from(prices);
let quantity_array = Float64Array::from(quantities);
let bid_price_array = Float64Array::from(bid_prices);
let ask_price_array = Float64Array::from(ask_prices);
let bid_size_array = Float64Array::from(bid_sizes);
let ask_size_array = Float64Array::from(ask_sizes);
let sequence_array = UInt64Array::from(sequences);
let latency_array = UInt64Array::from(latencies);
// Create record batch
RecordBatch::try_new(
schema.clone(),
vec![
Arc::new(timestamp_array),
Arc::new(symbol_array),
Arc::new(venue_array),
Arc::new(event_type_array),
Arc::new(price_array),
Arc::new(quantity_array),
Arc::new(bid_price_array),
Arc::new(ask_price_array),
Arc::new(bid_size_array),
Arc::new(ask_size_array),
Arc::new(sequence_array),
Arc::new(latency_array),
],
)
.context("Failed to create Arrow RecordBatch")
}
}
/// Buffer statistics for monitoring
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BufferStats {
pub buffered_events: usize,
pub buffer_capacity: usize,
pub utilization_percent: f64,
}
/// Parquet reader for market data replay
pub struct ParquetMarketDataReader {
base_path: String,
}
impl ParquetMarketDataReader {
pub fn new(base_path: String) -> Self {
Self { base_path }
}
/// List available Parquet files for replay
pub async fn list_available_files(&self) -> Result<Vec<String>> {
let mut files = Vec::new();
let entries =
std::fs::read_dir(&self.base_path).context("Failed to read Parquet directory")?;
for entry in entries {
let entry = entry.context("Failed to read directory entry")?;
let path = entry.path();
if path.is_file() && path.extension().map_or(false, |ext| ext == "parquet") {
if let Some(filename) = path.file_name().and_then(|f| f.to_str()) {
files.push(filename.to_string());
}
}
}
files.sort();
Ok(files)
}
/// Read market data from Parquet file for replay
pub async fn read_file(&self, filename: &str) -> Result<Vec<MarketDataEvent>> {
let filepath = Path::new(&self.base_path).join(filename);
// This would be implemented using parquet::arrow::async_reader
// For now, return placeholder
warn!("Parquet reader not fully implemented yet: {:?}", filepath);
Ok(Vec::new())
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[tokio::test]
async fn test_parquet_writer_creation() {
let temp_dir = tempdir().unwrap();
let config = ParquetConfig {
base_path: temp_dir.path().to_string_lossy().to_string(),
..Default::default()
};
let writer = ParquetMarketDataWriter::new(config).await;
assert!(writer.is_ok());
}
#[tokio::test]
async fn test_market_data_event_recording() {
let temp_dir = tempdir().unwrap();
let config = ParquetConfig {
base_path: temp_dir.path().to_string_lossy().to_string(),
batch_size: 2, // Small batch for testing
..Default::default()
};
let writer = ParquetMarketDataWriter::new(config).await.unwrap();
let event = MarketDataEvent {
timestamp_ns: 1234567890000000000,
symbol: "BTCUSD".to_string(),
venue: "binance".to_string(),
event_type: "trade".to_string(),
price: Some(50000.0),
quantity: Some(0.1),
bid_price: None,
ask_price: None,
bid_size: None,
ask_size: None,
sequence: 1,
latency_ns: Some(1000),
};
let result = writer.record(event);
assert!(result.is_ok());
// Give some time for background processing
tokio::time::sleep(Duration::from_millis(100)).await;
}
}