//! Real Market Data Helpers for Feature Engineering Tests //! //! Utilities to load real BTC/ETH Parquet data and convert it to formats //! used by feature engineering tests (OHLCV bars, PricePoints). #![allow( clippy::absurd_extreme_comparisons, clippy::assertions_on_constants, clippy::assertions_on_result_states, clippy::clone_on_copy, clippy::decimal_literal_representation, clippy::doc_lazy_continuation, clippy::doc_markdown, clippy::double_comparisons, clippy::else_if_without_else, clippy::empty_drop, clippy::expect_used, clippy::field_reassign_with_default, clippy::format_push_string, clippy::if_then_some_else_none, clippy::indexing_slicing, clippy::integer_division, clippy::len_zero, clippy::let_and_return, clippy::let_underscore_must_use, clippy::manual_let_else, clippy::manual_range_contains, clippy::map_err_ignore, clippy::missing_const_for_fn, clippy::modulo_arithmetic, clippy::needless_range_loop, clippy::new_without_default, clippy::non_ascii_literal, clippy::nonminimal_bool, clippy::octal_escapes, clippy::overly_complex_bool_expr, clippy::redundant_clone, clippy::shadow_reuse, clippy::shadow_unrelated, clippy::similar_names, clippy::single_component_path_imports, clippy::single_match_else, clippy::str_to_string, clippy::string_slice, clippy::tests_outside_test_module, clippy::unnecessary_cast, clippy::unnecessary_get_then_check, clippy::unnecessary_unwrap, clippy::unseparated_literal_suffix, clippy::unwrap_or_default, clippy::unwrap_used, clippy::use_debug, clippy::useless_vec, clippy::wildcard_enum_match_arm, dead_code, deprecated, unreachable_pub, unused_assignments, unused_comparisons, unused_imports, unused_variables )] use anyhow::{Context, Result}; use chrono::{DateTime, Utc}; use data::features::PricePoint; use data::parquet_persistence::{MarketDataEvent, ParquetMarketDataReader}; use std::path::PathBuf; /// Path to real test data directory (relative to workspace root) const REAL_DATA_PATH: &str = "test_data/real/parquet"; const BTC_FILE: &str = "BTC-USD_30day_2024-09.parquet"; const ETH_FILE: &str = "ETH-USD_30day_2024-09.parquet"; /// Real market data loader for feature engineering tests pub(crate) struct RealDataLoader { base_path: String, } impl RealDataLoader { /// Create new loader with workspace-relative path pub(crate) fn new() -> Self { let manifest_dir = env!("CARGO_MANIFEST_DIR"); let base_path = PathBuf::from(manifest_dir) .parent() .expect("Failed to get workspace root") .join(REAL_DATA_PATH); Self { base_path: base_path.to_string_lossy().to_string(), } } /// Check if real data files exist pub(crate) fn files_exist(&self) -> bool { let btc_path = PathBuf::from(&self.base_path).join(BTC_FILE); let eth_path = PathBuf::from(&self.base_path).join(ETH_FILE); btc_path.exists() && eth_path.exists() } /// Load BTC price data as PricePoints pub(crate) async fn load_btc_prices(&self, count: usize) -> Result> { self.load_prices(BTC_FILE, count).await } /// Load ETH price data as PricePoints pub(crate) async fn load_eth_prices(&self, count: usize) -> Result> { self.load_prices(ETH_FILE, count).await } /// Load BTC close prices as f64 vector (for simple technical indicator tests) pub(crate) async fn load_btc_close_prices(&self, count: usize) -> Result> { self.load_close_prices(BTC_FILE, count).await } /// Load ETH close prices as f64 vector (for simple technical indicator tests) pub(crate) async fn load_eth_close_prices(&self, count: usize) -> Result> { self.load_close_prices(ETH_FILE, count).await } /// Load price data from a specific file as PricePoints async fn load_prices(&self, filename: &str, count: usize) -> Result> { let reader = ParquetMarketDataReader::new(self.base_path.clone()); let events = reader .read_file(filename) .await .with_context(|| format!("Failed to load {}", filename))?; // Take only the requested number of events let events_subset = events.into_iter().take(count).collect::>(); // Convert MarketDataEvent to PricePoint let price_points = events_subset .into_iter() .map(|event| self.event_to_price_point(&event)) .collect::>>()?; Ok(price_points) } /// Load close prices as f64 vector for simple indicator calculations async fn load_close_prices(&self, filename: &str, count: usize) -> Result> { let reader = ParquetMarketDataReader::new(self.base_path.clone()); let events = reader .read_file(filename) .await .with_context(|| format!("Failed to load {}", filename))?; // Take only the requested number of events and extract close prices let close_prices = events .into_iter() .take(count) .filter_map(|event| event.price) // Use price as close price .collect::>(); Ok(close_prices) } /// Convert MarketDataEvent to PricePoint fn event_to_price_point(&self, event: &MarketDataEvent) -> Result { let timestamp = self.ns_to_datetime(event.timestamp_ns)?; let close = event.price.context("Price is None")?; // If OHLC data is available, use it; otherwise derive from close price let open = event.open.unwrap_or(close); let high = event.high.unwrap_or(close.max(open)); let low = event.low.unwrap_or(close.min(open)); Ok(PricePoint { timestamp, open, high, low, close, }) } /// Convert nanosecond timestamp to DateTime fn ns_to_datetime(&self, timestamp_ns: u64) -> Result> { let timestamp_ms = (timestamp_ns / 1_000_000) as i64; DateTime::from_timestamp_millis(timestamp_ms).context("Invalid timestamp") } } impl Default for RealDataLoader { fn default() -> Self { Self::new() } }