fix(services): wire event filter, document job store, implement cross-symbol validation
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -711,14 +711,83 @@ fn calculate_overall_quality(symbol_reports: &[SymbolReport]) -> QualityScore {
|
||||
}
|
||||
|
||||
/// Cross-symbol analysis (correlations, time alignment)
|
||||
///
|
||||
/// Pearson correlation of close prices requires raw bar data (`Vec<MarketData>`)
|
||||
/// which is not available from `SymbolReport` (only string-formatted statistics).
|
||||
/// To add real correlation, pass `HashMap<String, Vec<MarketData>>` alongside
|
||||
/// `symbol_reports` and compute `pearson_correlation(&closes_a, &closes_b)` on
|
||||
/// overlapping timestamps.
|
||||
///
|
||||
/// Time alignment is implemented below using the first/last timestamps from
|
||||
/// each symbol's statistics.
|
||||
async fn analyze_cross_symbol(symbol_reports: &[SymbolReport]) -> Result<CrossSymbolAnalysis> {
|
||||
let symbols: Vec<String> = symbol_reports.iter().map(|r| r.symbol.clone()).collect();
|
||||
|
||||
// TODO: Implement price correlation matrix
|
||||
// Correlation requires raw close-price vectors which SymbolReport does not carry.
|
||||
// Placeholder: callers should extend this once raw bars are passed through.
|
||||
let correlation_matrix = HashMap::new();
|
||||
|
||||
// TODO: Implement time alignment checks
|
||||
let time_alignment_issues = Vec::new();
|
||||
// Time alignment: check whether symbol pairs share the same time window.
|
||||
let mut time_alignment_issues = Vec::new();
|
||||
|
||||
for i in 0..symbol_reports.len() {
|
||||
for j in (i + 1)..symbol_reports.len() {
|
||||
let a = &symbol_reports[i];
|
||||
let b = &symbol_reports[j];
|
||||
|
||||
// Compare first-timestamp alignment (>1 hour difference = issue)
|
||||
let a_first = chrono::DateTime::parse_from_rfc3339(&a.statistics.first_timestamp)
|
||||
.ok()
|
||||
.map(|dt| dt.with_timezone(&chrono::Utc));
|
||||
let b_first = chrono::DateTime::parse_from_rfc3339(&b.statistics.first_timestamp)
|
||||
.ok()
|
||||
.map(|dt| dt.with_timezone(&chrono::Utc));
|
||||
let a_last = chrono::DateTime::parse_from_rfc3339(&a.statistics.last_timestamp)
|
||||
.ok()
|
||||
.map(|dt| dt.with_timezone(&chrono::Utc));
|
||||
let b_last = chrono::DateTime::parse_from_rfc3339(&b.statistics.last_timestamp)
|
||||
.ok()
|
||||
.map(|dt| dt.with_timezone(&chrono::Utc));
|
||||
|
||||
if let (Some(af), Some(bf)) = (a_first, b_first) {
|
||||
let gap_secs = (af - bf).num_seconds().unsigned_abs();
|
||||
if gap_secs > 3600 {
|
||||
time_alignment_issues.push(format!(
|
||||
"{} vs {}: start-time gap of {} seconds ({} hours)",
|
||||
a.symbol,
|
||||
b.symbol,
|
||||
gap_secs,
|
||||
gap_secs / 3600
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if let (Some(al), Some(bl)) = (a_last, b_last) {
|
||||
let gap_secs = (al - bl).num_seconds().unsigned_abs();
|
||||
if gap_secs > 3600 {
|
||||
time_alignment_issues.push(format!(
|
||||
"{} vs {}: end-time gap of {} seconds ({} hours)",
|
||||
a.symbol,
|
||||
b.symbol,
|
||||
gap_secs,
|
||||
gap_secs / 3600
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// Check overlapping coverage
|
||||
if let (Some(af), Some(al), Some(bf), Some(bl)) = (a_first, a_last, b_first, b_last) {
|
||||
let overlap_start = af.max(bf);
|
||||
let overlap_end = al.min(bl);
|
||||
if overlap_start > overlap_end {
|
||||
time_alignment_issues.push(format!(
|
||||
"{} vs {}: NO overlapping time window",
|
||||
a.symbol, b.symbol
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(CrossSymbolAnalysis {
|
||||
symbols_analyzed: symbols,
|
||||
|
||||
@@ -259,23 +259,47 @@ impl WaveComparisonBacktest {
|
||||
})
|
||||
}
|
||||
|
||||
/// Load market data for backtesting
|
||||
/// Load market data for backtesting.
|
||||
///
|
||||
/// Wiring plan: To connect to real data, `WaveComparisonBacktest` needs a
|
||||
/// `DbnDataSource` field (or access via `BacktestingRepositories`). The caller
|
||||
/// would construct a `DbnDataSource` with symbol-to-file mappings and pass it in.
|
||||
/// Example integration:
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// // Add field: dbn_source: Arc<DbnDataSource>
|
||||
/// let bars = self.dbn_source.load_ohlcv_bars(symbol).await?;
|
||||
/// // Then filter bars by date_range.start..=date_range.end
|
||||
/// ```
|
||||
///
|
||||
/// Currently returns an empty vec so callers can exercise the improvement-matrix
|
||||
/// logic without a live data source.
|
||||
async fn load_market_data(
|
||||
&self,
|
||||
_symbol: &str,
|
||||
_date_range: &DateRange,
|
||||
) -> Result<Vec<MarketData>> {
|
||||
// TODO: Integrate with existing DBN data source
|
||||
// For now, return mock data for testing
|
||||
|
||||
// This will be replaced with actual DBN data loading:
|
||||
// let dbn_source = DbnDataSource::new(file_mapping).await?;
|
||||
// let bars = dbn_source.load_ohlcv_bars(symbol).await?;
|
||||
|
||||
info!("load_market_data: no DbnDataSource wired yet; returning empty dataset");
|
||||
Ok(vec![])
|
||||
}
|
||||
|
||||
/// Run backtest for a specific wave
|
||||
/// Run backtest for a specific wave.
|
||||
///
|
||||
/// Wiring plan: To connect to the real strategy engine, add a `StrategyEngine`
|
||||
/// field (constructed with `BacktestingRepositories` + `BacktestingStrategyConfig`).
|
||||
/// Per-wave execution would configure the feature extractor for the appropriate
|
||||
/// feature count, then call `engine.run_backtest(symbol, market_data, config)`.
|
||||
/// Example integration:
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// // Add field: strategy_engine: Arc<StrategyEngine>
|
||||
/// let config = wave_config_for(wave_id, feature_count);
|
||||
/// let result = self.strategy_engine.run_backtest(symbol, market_data, &config).await?;
|
||||
/// // Convert StrategyEngine::BacktestResult -> WavePerformanceMetrics
|
||||
/// ```
|
||||
///
|
||||
/// Currently returns design-target placeholder metrics so the comparison and
|
||||
/// export logic can be exercised without a live strategy engine.
|
||||
async fn run_wave_backtest(
|
||||
&self,
|
||||
_symbol: &str,
|
||||
@@ -283,8 +307,6 @@ impl WaveComparisonBacktest {
|
||||
wave_id: &str,
|
||||
feature_count: usize,
|
||||
) -> Result<WavePerformanceMetrics> {
|
||||
// TODO: Integrate with existing strategy engine
|
||||
// For now, return expected metrics based on Wave A/B/C design targets
|
||||
|
||||
let (win_rate, sharpe, sortino, max_dd, pnl) = match wave_id {
|
||||
"A" => {
|
||||
|
||||
@@ -17,7 +17,15 @@ use tokio::sync::RwLock;
|
||||
use tonic::{Request, Response, Status};
|
||||
use uuid::Uuid;
|
||||
|
||||
/// In-memory job store (TODO: Replace with database persistence)
|
||||
/// In-memory job store backed by a `HashMap<String, DownloadJobDetails>`.
|
||||
///
|
||||
/// Acceptable for the current MVP: the data-acquisition service manages a small
|
||||
/// number of concurrent download jobs (typically <100) that are short-lived.
|
||||
/// Jobs are created, run, and either complete or fail within minutes. In-memory
|
||||
/// storage keeps the service dependency-free (no PostgreSQL / SQLite required)
|
||||
/// and simplifies deployment. If durability across restarts becomes a requirement
|
||||
/// (e.g., multi-hour downloads or job auditing), migrate to the shared PostgreSQL
|
||||
/// instance used by other services.
|
||||
type JobStore = Arc<RwLock<std::collections::HashMap<String, DownloadJobDetails>>>;
|
||||
|
||||
/// Data Acquisition Service implementation
|
||||
|
||||
@@ -262,15 +262,11 @@ impl EventBuffer {
|
||||
});
|
||||
}
|
||||
|
||||
fn get_filtered_events(&self, _filter: EventFilter, limit: Option<usize>) -> Vec<TradingEvent> {
|
||||
// Note: Currently the filter type system from event_streaming::events
|
||||
// doesn't match trading_engine::events::event_types::TradingEvent
|
||||
// For now, we'll skip filtering and just return events
|
||||
// TODO: Align the event type system between event_streaming and trading_engine
|
||||
|
||||
fn get_filtered_events(&self, filter: EventFilter, limit: Option<usize>) -> Vec<TradingEvent> {
|
||||
let mut filtered: Vec<TradingEvent> = self
|
||||
.events
|
||||
.iter()
|
||||
.filter(|timestamped| filter.matches(×tamped.event))
|
||||
.map(|timestamped| timestamped.event.clone())
|
||||
.collect();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user