#![cfg(feature = "databento")]
use databento::dbn::{
self, BidAskPair, FlagSet, Mbp1Msg, Metadata, RecordHeader, SType, Schema, TradeMsg,
encode::{DbnEncoder, EncodeRecord},
enums::rtype,
};
use rust_decimal::Decimal;
use rust_decimal_macros::dec;
use rustrade_data::exchange::databento::{load_quotes_from_dbn, load_trades_from_dbn};
use rustrade_instrument::exchange::ExchangeId;
use std::fs::File;
use std::path::{Path, PathBuf};
use tempfile::TempDir;
const PRICE_SCALE: i64 = 1_000_000_000;
const TICK: i64 = PRICE_SCALE / 4;
const BASE_PRICE: i64 = 5_400 * PRICE_SCALE;
const START_TS: u64 = 1_718_000_000_000_000_000;
const TS_STEP: u64 = 1_000_000;
const PUBLISHER_ID: u16 = 1;
const INSTRUMENT_ID: u32 = 42;
fn metadata(schema: Schema) -> Metadata {
Metadata::builder()
.dataset("GLBX.MDP3")
.schema(Some(schema))
.start(START_TS)
.stype_in(Some(SType::RawSymbol))
.stype_out(SType::InstrumentId)
.symbols(vec!["ESM4".to_owned()])
.build()
}
fn write_trades(path: &Path, count: u32) {
let file = File::create(path).expect("create trades fixture");
let mut encoder =
DbnEncoder::with_zstd(file, &metadata(Schema::Trades)).expect("trades header");
for i in 0..count {
let ts = START_TS + u64::from(i) * TS_STEP;
let price = BASE_PRICE + i64::from(i % 5) * TICK;
let msg = TradeMsg {
hd: RecordHeader::new::<TradeMsg>(rtype::MBP_0, PUBLISHER_ID, INSTRUMENT_ID, ts),
price,
size: 1 + i % 7,
action: b'T' as std::ffi::c_char,
side: if i % 2 == 0 { b'B' } else { b'A' } as std::ffi::c_char,
flags: FlagSet::empty(),
depth: 0,
ts_recv: ts,
ts_in_delta: 0,
sequence: i,
};
encoder.encode_record(&msg).expect("encode trade");
}
}
fn write_quotes(path: &Path, count: u32) {
let file = File::create(path).expect("create quotes fixture");
let mut encoder = DbnEncoder::with_zstd(file, &metadata(Schema::Mbp1)).expect("quotes header");
for i in 0..count {
let ts = START_TS + u64::from(i) * TS_STEP;
let bid_px = BASE_PRICE + i64::from(i % 5) * TICK;
let ask_px = bid_px + TICK;
let msg = Mbp1Msg {
hd: RecordHeader::new::<Mbp1Msg>(rtype::MBP_1, PUBLISHER_ID, INSTRUMENT_ID, ts),
price: ask_px,
size: 1 + i % 3,
action: b'A' as std::ffi::c_char,
side: b'B' as std::ffi::c_char,
flags: FlagSet::empty(),
depth: 0,
ts_recv: ts,
ts_in_delta: 0,
sequence: i,
levels: [BidAskPair {
bid_px,
ask_px,
bid_sz: 10 + i % 4,
ask_sz: 12 + i % 4,
bid_ct: 2,
ask_ct: 3,
}],
};
encoder.encode_record(&msg).expect("encode quote");
}
}
struct Fixture {
_dir: TempDir,
path: PathBuf,
}
fn trades_fixture(count: u32) -> Fixture {
let dir = TempDir::new().expect("temp dir");
let path = dir.path().join("es_trades_sample.dbn.zst");
write_trades(&path, count);
Fixture { _dir: dir, path }
}
fn quotes_fixture(count: u32) -> Fixture {
let dir = TempDir::new().expect("temp dir");
let path = dir.path().join("es_quotes_sample.dbn.zst");
write_quotes(&path, count);
Fixture { _dir: dir, path }
}
#[test]
fn test_load_trades_from_dbn_fixture() {
let fixture = trades_fixture(250);
let trades: Vec<_> = load_trades_from_dbn(&fixture.path, ExchangeId::DatabentoGlbx, "ESM4")
.expect("Failed to open DBN file")
.collect();
assert!(!trades.is_empty(), "Expected at least one trade record");
let (successes, failures): (Vec<_>, Vec<_>) = trades.into_iter().partition(|r| r.is_ok());
assert!(
failures.is_empty(),
"Expected every synthetic trade to transform, got {} failures",
failures.len()
);
assert_eq!(successes.len(), 250, "Expected all records back");
let first_trade = successes.into_iter().next().unwrap().unwrap();
assert_eq!(first_trade.exchange, ExchangeId::DatabentoGlbx);
assert_eq!(first_trade.instrument, "ESM4");
assert!(
first_trade.kind.price > Decimal::ZERO,
"Price should be positive"
);
assert!(
first_trade.kind.amount > Decimal::ZERO,
"Amount should be positive"
);
assert!(
first_trade.kind.price > dec!(1000) && first_trade.kind.price < dec!(10000),
"ES price {} outside expected range",
first_trade.kind.price
);
}
#[test]
fn test_load_quotes_from_dbn_fixture() {
let fixture = quotes_fixture(250);
let quotes: Vec<_> = load_quotes_from_dbn(&fixture.path, ExchangeId::DatabentoGlbx, "ESM4")
.expect("Failed to open DBN file")
.collect();
assert!(!quotes.is_empty(), "Expected at least one quote record");
let (successes, failures): (Vec<_>, Vec<_>) = quotes.into_iter().partition(|r| r.is_ok());
assert!(
failures.is_empty(),
"Expected every synthetic quote to transform, got {} failures",
failures.len()
);
assert_eq!(successes.len(), 250, "Expected all records back");
let first_quote = successes.into_iter().next().unwrap().unwrap();
assert_eq!(first_quote.exchange, ExchangeId::DatabentoGlbx);
assert_eq!(first_quote.instrument, "ESM4");
let quote = &first_quote.kind;
assert!(
quote.bid_price > Decimal::ZERO,
"Bid price should be positive"
);
assert!(
quote.ask_price > Decimal::ZERO,
"Ask price should be positive"
);
assert!(
quote.ask_price >= quote.bid_price,
"Ask should be >= bid, got bid={} ask={}",
quote.bid_price,
quote.ask_price
);
assert!(
quote.bid_price > dec!(1000) && quote.bid_price < dec!(10000),
"ES bid {} outside expected range",
quote.bid_price
);
}
#[test]
fn test_trade_timestamp_ordering() {
let fixture = trades_fixture(100);
let trades: Vec<_> = load_trades_from_dbn(&fixture.path, ExchangeId::DatabentoGlbx, "ESM4")
.expect("Failed to open DBN file")
.filter_map(|r| r.ok())
.collect();
assert_eq!(trades.len(), 100, "Expected all records back");
for window in trades.windows(2) {
assert!(
window[1].time_exchange >= window[0].time_exchange,
"Timestamps should be monotonically increasing"
);
}
}
#[test]
fn test_quote_spread_is_reasonable() {
let fixture = quotes_fixture(100);
let quotes: Vec<_> = load_quotes_from_dbn(&fixture.path, ExchangeId::DatabentoGlbx, "ESM4")
.expect("Failed to open DBN file")
.filter_map(|r| r.ok())
.collect();
assert_eq!(quotes.len(), 100, "Expected all records back");
for quote in "es {
let spread = quote.kind.ask_price - quote.kind.bid_price;
assert!(
spread >= Decimal::ZERO && spread < dec!(10),
"Spread {} is unreasonable for ES futures",
spread
);
}
}
#[test]
fn synthetic_fixtures_have_the_shape_the_assertions_assume() {
let dir = TempDir::new().expect("temp dir");
let path = dir.path().join("probe.dbn.zst");
write_quotes(&path, 5);
let quotes: Vec<_> = load_quotes_from_dbn(&path, ExchangeId::DatabentoGlbx, "ESM4")
.expect("open")
.filter_map(|r| r.ok())
.collect();
assert_eq!(
quotes[0].kind.ask_price - quotes[0].kind.bid_price,
dec!(0.25)
);
assert_eq!(quotes[0].kind.bid_price, dec!(5400));
assert_eq!(dbn::UNDEF_PRICE, i64::MAX, "undefined-price sentinel moved");
}