use super::tick::LseMessage;
use crate::{
books::Level,
event::{MarketEvent, MarketIter},
subscription::book::OrderBookL1,
};
use chrono::Utc;
use rust_decimal::Decimal;
use rustrade_instrument::exchange::ExchangeId;
impl<InstrumentKey> From<(ExchangeId, InstrumentKey, LseMessage)>
for MarketIter<InstrumentKey, OrderBookL1>
{
fn from((exchange, instrument, message): (ExchangeId, InstrumentKey, LseMessage)) -> Self {
let LseMessage::Tick(tick) = message else {
return Self(vec![]);
};
Self(vec![Ok(MarketEvent {
time_exchange: tick.time_exchange,
time_received: Utc::now(),
exchange,
instrument,
kind: OrderBookL1 {
last_update_time: tick.time_exchange,
best_bid: quoted(tick.bid),
best_ask: quoted(tick.ask),
},
})])
}
}
fn quoted(price: Option<Decimal>) -> Option<Level> {
price
.filter(|price| !price.is_zero())
.map(|price| Level::new(price, Decimal::ZERO))
}
#[cfg(test)]
#[allow(clippy::unwrap_used)] mod tests {
use super::*;
use rust_decimal_macros::dec;
const TICK: &str = r#"{"type":"tick","symbol":"BTC/USD",
"ts":"2026-01-02T09:37:24.760146+00:00","price":42000.5,"bid":42000.5,
"ask":42001.0,"volume":0.00155}"#;
fn decode(json: &str) -> Vec<MarketEvent<u8, OrderBookL1>> {
let message: LseMessage = serde_json::from_str(json).unwrap();
MarketIter::<u8, OrderBookL1>::from((ExchangeId::LseCrypto, 1_u8, message))
.0
.into_iter()
.map(Result::unwrap)
.collect()
}
#[test]
fn a_tick_decodes_to_a_two_sided_book() {
let events = decode(TICK);
assert_eq!(events.len(), 1);
assert_eq!(events[0].kind.best_bid.unwrap().price, dec!(42000.5));
assert_eq!(events[0].kind.best_ask.unwrap().price, dec!(42001.0));
}
#[test]
fn the_book_instant_equals_the_events_instant() {
let events = decode(TICK);
assert_eq!(events[0].kind.last_update_time, events[0].time_exchange);
}
#[test]
fn both_levels_carry_a_zero_size_because_none_is_published() {
let events = decode(TICK);
assert_eq!(events[0].kind.best_bid.unwrap().amount, Decimal::ZERO);
assert_eq!(events[0].kind.best_ask.unwrap().amount, Decimal::ZERO);
}
#[test]
fn the_tick_volume_is_not_used_as_a_level_size() {
let events = decode(TICK);
assert_ne!(events[0].kind.best_bid.unwrap().amount, dec!(0.00155));
}
fn one_sided(bid: &str, ask: &str) -> String {
format!(
r#"{{"type":"tick","symbol":"BTC/USD","ts":"2026-01-02T09:37:24.760146+00:00",
"price":{bid},"bid":{bid},"ask":{ask},"volume":0.0}}"#
)
}
#[test]
fn a_zero_priced_side_is_absent_rather_than_a_level_at_zero() {
let bid_only = decode(&one_sided("42000.5", "0.0"));
assert_eq!(bid_only[0].kind.best_bid.unwrap().price, dec!(42000.5));
assert_eq!(bid_only[0].kind.best_ask, None);
let ask_only = decode(&one_sided("0.0", "42001.0"));
assert_eq!(ask_only[0].kind.best_bid, None);
assert_eq!(ask_only[0].kind.best_ask.unwrap().price, dec!(42001.0));
let neither = decode(&one_sided("0.0", "0.0"));
assert_eq!(neither[0].kind.best_bid, None);
assert_eq!(neither[0].kind.best_ask, None);
}
#[test]
fn a_null_side_is_absent() {
let events = decode(
r#"{"type":"tick","symbol":"TEST261231C00010500","ts":"2026-01-02T15:00:00+00:00",
"price":1.25,"bid":null,"ask":null,"volume":3,"name":"TEST $10.50 Call Dec 31"}"#,
);
assert_eq!(events[0].kind.best_bid, None);
assert_eq!(events[0].kind.best_ask, None);
}
#[test]
fn a_zero_priced_side_would_have_halved_the_mid_had_it_been_published() {
let events = decode(&one_sided("0.0", "42001.0"));
let book = &events[0].kind;
assert_eq!(
crate::books::mid_price(Decimal::ZERO, dec!(42001.0)),
dec!(21000.5),
"this is what a published zero bid would have priced the instrument at",
);
assert!(book.best_bid.is_none());
assert_eq!(book.mid_price(), None);
assert_eq!(book.volume_weighed_mid_price(), None);
}
#[test]
fn a_control_frame_yields_no_books() {
let events = decode(r#"{"type":"replay_complete","symbol":"BTC/USD","rows":41}"#);
assert!(events.is_empty());
}
}