Converts Trade, Ohlcv, and Quote variants from data::ProcessedMessage to trading_engine::MarketEvent. Handles HardwareTimestamp→DateTime<Utc>, Decimal→Quantity, and String→Symbol type conversions. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
279 lines
8.7 KiB
Rust
279 lines
8.7 KiB
Rust
//! DBN to MarketEvent converter
|
|
//!
|
|
//! Converts `ProcessedMessage` values from the Databento DBN parser
|
|
//! into the canonical `MarketEvent` type used throughout the trading engine.
|
|
|
|
use chrono::{DateTime, Utc};
|
|
use common::{Quantity, Symbol};
|
|
use data::providers::databento::dbn_parser::ProcessedMessage;
|
|
use trading_engine::timing::HardwareTimestamp;
|
|
use trading_engine::types::events::MarketEvent;
|
|
|
|
/// Convert a `HardwareTimestamp` to `DateTime<Utc>`.
|
|
///
|
|
/// Uses the nanoseconds-since-epoch stored in the timestamp.
|
|
/// Returns `None` if the nanoseconds value cannot represent a valid datetime
|
|
/// (only possible for values outside the i64 range, which is practically
|
|
/// impossible for real timestamps).
|
|
fn hw_ts_to_datetime(ts: &HardwareTimestamp) -> Option<DateTime<Utc>> {
|
|
let nanos_i64 = i64::try_from(ts.as_nanos()).ok()?;
|
|
Some(DateTime::from_timestamp_nanos(nanos_i64))
|
|
}
|
|
|
|
/// Convert a `ProcessedMessage` from the DBN parser into a `MarketEvent`.
|
|
///
|
|
/// Returns `Some(MarketEvent)` for `Trade`, `Ohlcv`, and `Quote` variants.
|
|
/// Returns `None` for `OrderBook` and `Status` variants, which do not have
|
|
/// a direct one-to-one mapping to the `MarketEvent` enum (order book updates
|
|
/// would need aggregation into a full snapshot, and status messages are
|
|
/// system-level events).
|
|
///
|
|
/// # Arguments
|
|
///
|
|
/// * `msg` - A reference to the `ProcessedMessage` to convert.
|
|
///
|
|
/// # Returns
|
|
///
|
|
/// `Some(MarketEvent)` if the message can be converted, `None` otherwise.
|
|
pub fn processed_to_market_event(msg: &ProcessedMessage) -> Option<MarketEvent> {
|
|
match msg {
|
|
ProcessedMessage::Trade {
|
|
symbol,
|
|
timestamp,
|
|
price,
|
|
size,
|
|
side,
|
|
trade_id,
|
|
conditions: _,
|
|
} => {
|
|
let dt = hw_ts_to_datetime(timestamp)?;
|
|
let qty = Quantity::from_decimal(*size).ok()?;
|
|
Some(MarketEvent::Trade {
|
|
symbol: Symbol::new(symbol.clone()),
|
|
price: *price,
|
|
size: qty,
|
|
timestamp: dt,
|
|
side: Some(*side),
|
|
venue: None,
|
|
trade_id: trade_id.clone(),
|
|
})
|
|
}
|
|
ProcessedMessage::Ohlcv {
|
|
symbol,
|
|
timestamp,
|
|
open,
|
|
high,
|
|
low,
|
|
close,
|
|
volume,
|
|
} => {
|
|
let dt = hw_ts_to_datetime(timestamp)?;
|
|
let vol = Quantity::from_decimal(*volume).ok()?;
|
|
Some(MarketEvent::Bar {
|
|
symbol: Symbol::new(symbol.clone()),
|
|
open: *open,
|
|
high: *high,
|
|
low: *low,
|
|
close: *close,
|
|
volume: vol,
|
|
timestamp: dt,
|
|
interval: String::new(),
|
|
venue: None,
|
|
})
|
|
}
|
|
ProcessedMessage::Quote {
|
|
symbol,
|
|
timestamp,
|
|
bid,
|
|
ask,
|
|
bid_size,
|
|
ask_size,
|
|
exchange,
|
|
} => {
|
|
let dt = hw_ts_to_datetime(timestamp)?;
|
|
let bid_price = (*bid)?;
|
|
let ask_price = (*ask)?;
|
|
let bq = Quantity::from_decimal((*bid_size)?).ok()?;
|
|
let aq = Quantity::from_decimal((*ask_size)?).ok()?;
|
|
Some(MarketEvent::Quote {
|
|
symbol: Symbol::new(symbol.clone()),
|
|
bid_price,
|
|
bid_size: bq,
|
|
ask_price,
|
|
ask_size: aq,
|
|
timestamp: dt,
|
|
venue: exchange.clone(),
|
|
})
|
|
}
|
|
ProcessedMessage::OrderBook { .. } | ProcessedMessage::Status { .. } => None,
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use common::{OrderSide, Price};
|
|
use rust_decimal::Decimal;
|
|
use trading_engine::timing::{HardwareTimestamp, TimingSource};
|
|
|
|
/// Helper: build a `HardwareTimestamp` from a `DateTime<Utc>`.
|
|
fn ts_from_dt(dt: DateTime<Utc>) -> HardwareTimestamp {
|
|
#[allow(clippy::cast_sign_loss)]
|
|
let nanos = dt.timestamp_nanos_opt().unwrap_or(0) as u64;
|
|
HardwareTimestamp {
|
|
cycles: 0,
|
|
nanos,
|
|
source: TimingSource::SystemClock,
|
|
validation_passed: true,
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_convert_trade() {
|
|
let now = Utc::now();
|
|
let ts = ts_from_dt(now);
|
|
let price = Price::from_f64(123.45).ok();
|
|
let price = price.unwrap_or(Price::ZERO);
|
|
|
|
let msg = ProcessedMessage::Trade {
|
|
symbol: "AAPL".to_string(),
|
|
timestamp: ts,
|
|
price,
|
|
size: Decimal::new(100, 0),
|
|
side: OrderSide::Buy,
|
|
trade_id: Some("t1".to_string()),
|
|
conditions: vec![],
|
|
};
|
|
|
|
let event = processed_to_market_event(&msg);
|
|
assert!(event.is_some(), "Trade should convert to MarketEvent");
|
|
|
|
let event = event.unwrap_or_else(|| {
|
|
MarketEvent::Control {
|
|
command: String::new(),
|
|
parameters: std::collections::HashMap::new(),
|
|
timestamp: Utc::now(),
|
|
}
|
|
});
|
|
|
|
if let MarketEvent::Trade {
|
|
symbol,
|
|
price: p,
|
|
size,
|
|
side,
|
|
trade_id,
|
|
..
|
|
} = &event
|
|
{
|
|
assert_eq!(symbol.as_str(), "AAPL");
|
|
assert_eq!(*p, price);
|
|
assert_eq!(size.to_f64(), 100.0);
|
|
assert_eq!(*side, Some(OrderSide::Buy));
|
|
assert_eq!(*trade_id, Some("t1".to_string()));
|
|
} else {
|
|
// Should not reach here
|
|
assert!(matches!(event, MarketEvent::Trade { .. }), "Expected MarketEvent::Trade variant");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_convert_ohlcv_to_bar() {
|
|
let now = Utc::now();
|
|
let ts = ts_from_dt(now);
|
|
let open = Price::from_f64(100.0).unwrap_or(Price::ZERO);
|
|
let high = Price::from_f64(110.0).unwrap_or(Price::ZERO);
|
|
let low = Price::from_f64(95.0).unwrap_or(Price::ZERO);
|
|
let close = Price::from_f64(105.0).unwrap_or(Price::ZERO);
|
|
|
|
let msg = ProcessedMessage::Ohlcv {
|
|
symbol: "MSFT".to_string(),
|
|
timestamp: ts,
|
|
open,
|
|
high,
|
|
low,
|
|
close,
|
|
volume: Decimal::new(5000, 0),
|
|
};
|
|
|
|
let event = processed_to_market_event(&msg);
|
|
assert!(event.is_some(), "OHLCV should convert to MarketEvent::Bar");
|
|
|
|
let event = event.unwrap_or_else(|| {
|
|
MarketEvent::Control {
|
|
command: String::new(),
|
|
parameters: std::collections::HashMap::new(),
|
|
timestamp: Utc::now(),
|
|
}
|
|
});
|
|
|
|
if let MarketEvent::Bar {
|
|
symbol,
|
|
open: o,
|
|
high: h,
|
|
low: l,
|
|
close: c,
|
|
volume,
|
|
..
|
|
} = &event
|
|
{
|
|
assert_eq!(symbol.as_str(), "MSFT");
|
|
assert_eq!(*o, open);
|
|
assert_eq!(*h, high);
|
|
assert_eq!(*l, low);
|
|
assert_eq!(*c, close);
|
|
assert_eq!(volume.to_f64(), 5000.0);
|
|
} else {
|
|
assert!(matches!(event, MarketEvent::Bar { .. }), "Expected MarketEvent::Bar variant");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_convert_quote() {
|
|
let now = Utc::now();
|
|
let ts = ts_from_dt(now);
|
|
let bid = Price::from_f64(99.0).unwrap_or(Price::ZERO);
|
|
let ask = Price::from_f64(101.0).unwrap_or(Price::ZERO);
|
|
|
|
let msg = ProcessedMessage::Quote {
|
|
symbol: "TSLA".to_string(),
|
|
timestamp: ts,
|
|
bid: Some(bid),
|
|
ask: Some(ask),
|
|
bid_size: Some(Decimal::new(200, 0)),
|
|
ask_size: Some(Decimal::new(150, 0)),
|
|
exchange: Some("NASDAQ".to_string()),
|
|
};
|
|
|
|
let event = processed_to_market_event(&msg);
|
|
assert!(event.is_some(), "Quote should convert to MarketEvent::Quote");
|
|
|
|
let event = event.unwrap_or_else(|| {
|
|
MarketEvent::Control {
|
|
command: String::new(),
|
|
parameters: std::collections::HashMap::new(),
|
|
timestamp: Utc::now(),
|
|
}
|
|
});
|
|
|
|
if let MarketEvent::Quote {
|
|
symbol,
|
|
bid_price,
|
|
ask_price,
|
|
bid_size,
|
|
ask_size,
|
|
venue,
|
|
..
|
|
} = &event
|
|
{
|
|
assert_eq!(symbol.as_str(), "TSLA");
|
|
assert_eq!(*bid_price, bid);
|
|
assert_eq!(*ask_price, ask);
|
|
assert_eq!(bid_size.to_f64(), 200.0);
|
|
assert_eq!(ask_size.to_f64(), 150.0);
|
|
assert_eq!(*venue, Some("NASDAQ".to_string()));
|
|
} else {
|
|
assert!(matches!(event, MarketEvent::Quote { .. }), "Expected MarketEvent::Quote variant");
|
|
}
|
|
}
|
|
}
|