use super::transformer::{AggregateProvenance, deserialize_first_condition};
use crate::{
books::Level,
error::DataError,
event::MarketEvent,
exchange::Connector,
subscriber::shared::elements::excerpt,
subscription::{
Map, SubscriptionKind,
book::{OrderBookL1, OrderBooksL1},
candle::{Candle, Candles},
quote::{Quote, Quotes},
trade::{PublicTrade, PublicTrades},
},
transformer::ExchangeTransformer,
};
use chrono::{DateTime, TimeZone, Utc};
use rust_decimal::Decimal;
use rustrade_instrument::Side;
use rustrade_integration::{
Transformer, protocol::websocket::WsMessage, subscription::SubscriptionId,
};
use serde::{Deserialize, Deserializer, de::DeserializeOwned};
use serde_json::value::RawValue;
use smol_str::{SmolStr, format_smolstr};
use std::marker::PhantomData;
use tokio::sync::mpsc;
const CRYPTO_TRADES: &str = "XT";
#[derive(Debug)]
pub struct MassiveMessages<Message>(pub Vec<Result<Message, DataError>>);
impl<'de, Message> Deserialize<'de> for MassiveMessages<Message>
where
Message: DeserializeOwned,
{
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let elements = Vec::<&'de RawValue>::deserialize(deserializer)?;
Ok(Self(
elements
.into_iter()
.map(|raw| {
serde_json::from_str(raw.get()).map_err(|error| {
DataError::Socket(format!(
"failed to decode a Massive message: {error}: {}",
excerpt(raw.get())
))
})
})
.collect(),
))
}
}
pub trait MassiveMessage: DeserializeOwned {
fn event(&self) -> &str;
fn symbol(&self) -> &str;
fn time_exchange(&self) -> DateTime<Utc>;
fn subscription_id(&self) -> SubscriptionId {
SubscriptionId(format_smolstr!("{}|{}", self.event(), self.symbol()))
}
}
pub trait MassiveKind: SubscriptionKind {
type Message: MassiveMessage;
fn event(message: Self::Message) -> Self::Event;
}
#[derive(Debug, Clone, Deserialize)]
pub struct MassiveTrade {
#[serde(rename = "ev")]
pub event: SmolStr,
#[serde(alias = "sym", alias = "pair")]
pub symbol: SmolStr,
#[serde(rename = "p", with = "rust_decimal::serde::float")]
pub price: Decimal,
#[serde(rename = "s", with = "rust_decimal::serde::float")]
pub size: Decimal,
#[serde(rename = "t")]
pub timestamp: i64,
#[serde(
rename = "c",
default,
deserialize_with = "deserialize_first_condition"
)]
pub condition: Option<i32>,
#[serde(rename = "i", default, deserialize_with = "deserialize_id")]
pub id: Option<SmolStr>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct MassiveQuote {
#[serde(rename = "ev")]
pub event: SmolStr,
#[serde(alias = "sym", alias = "pair", alias = "p")]
pub symbol: SmolStr,
#[serde(
alias = "bp",
alias = "b",
with = "rust_decimal::serde::float",
default
)]
pub bid_price: Decimal,
#[serde(alias = "bs", with = "rust_decimal::serde::float_option", default)]
pub bid_size: Option<Decimal>,
#[serde(
alias = "ap",
alias = "a",
with = "rust_decimal::serde::float",
default
)]
pub ask_price: Decimal,
#[serde(alias = "as", with = "rust_decimal::serde::float_option", default)]
pub ask_size: Option<Decimal>,
#[serde(rename = "t")]
pub timestamp: i64,
}
#[derive(Debug, Clone, Deserialize)]
pub struct MassiveAggregate {
#[serde(rename = "ev")]
pub event: SmolStr,
#[serde(alias = "sym", alias = "pair")]
pub symbol: SmolStr,
#[serde(rename = "o", with = "rust_decimal::serde::float")]
pub open: Decimal,
#[serde(rename = "h", with = "rust_decimal::serde::float")]
pub high: Decimal,
#[serde(rename = "l", with = "rust_decimal::serde::float")]
pub low: Decimal,
#[serde(rename = "c", with = "rust_decimal::serde::float")]
pub close: Decimal,
#[serde(rename = "v", with = "rust_decimal::serde::float")]
pub volume: Decimal,
#[serde(rename = "e")]
pub end_timestamp: i64,
}
impl MassiveAggregate {
pub fn provenance(&self) -> AggregateProvenance {
if self.event.starts_with('C') {
AggregateProvenance::QuoteTape
} else {
AggregateProvenance::TradeTape
}
}
}
impl MassiveMessage for MassiveTrade {
fn event(&self) -> &str {
&self.event
}
fn symbol(&self) -> &str {
&self.symbol
}
fn time_exchange(&self) -> DateTime<Utc> {
millis_to_datetime(self.timestamp)
}
}
impl MassiveMessage for MassiveQuote {
fn event(&self) -> &str {
&self.event
}
fn symbol(&self) -> &str {
&self.symbol
}
fn time_exchange(&self) -> DateTime<Utc> {
millis_to_datetime(self.timestamp)
}
}
impl MassiveMessage for MassiveAggregate {
fn event(&self) -> &str {
&self.event
}
fn symbol(&self) -> &str {
&self.symbol
}
fn time_exchange(&self) -> DateTime<Utc> {
millis_to_datetime(self.end_timestamp)
}
}
impl MassiveKind for PublicTrades {
type Message = MassiveTrade;
fn event(trade: MassiveTrade) -> PublicTrade {
let side = (trade.event == CRYPTO_TRADES)
.then_some(trade.condition)
.flatten()
.and_then(|condition| match condition {
1 => Some(Side::Sell),
2 => Some(Side::Buy),
_ => None,
});
PublicTrade {
id: trade.id.unwrap_or_default(),
price: trade.price,
amount: trade.size,
side,
}
}
}
impl MassiveKind for Quotes {
type Message = MassiveQuote;
fn event(quote: MassiveQuote) -> Quote {
Quote {
bid_price: quote.bid_price,
bid_amount: quote.bid_size.unwrap_or(Decimal::ZERO),
ask_price: quote.ask_price,
ask_amount: quote.ask_size.unwrap_or(Decimal::ZERO),
}
}
}
impl MassiveKind for OrderBooksL1 {
type Message = MassiveQuote;
fn event(quote: MassiveQuote) -> OrderBookL1 {
let side = |price: Decimal, size: Option<Decimal>| {
(!price.is_zero()).then(|| Level::new(price, size.unwrap_or(Decimal::ZERO)))
};
OrderBookL1 {
last_update_time: quote.time_exchange(),
best_bid: side(quote.bid_price, quote.bid_size),
best_ask: side(quote.ask_price, quote.ask_size),
}
}
}
impl MassiveKind for Candles {
type Message = MassiveAggregate;
fn event(aggregate: MassiveAggregate) -> Candle {
let provenance = aggregate.provenance();
Candle {
close_time: aggregate.time_exchange(),
open: aggregate.open,
high: aggregate.high,
low: aggregate.low,
close: aggregate.close,
volume: provenance.volume(aggregate.volume),
trade_count: None,
}
}
}
#[derive(Debug)]
pub struct MassiveTransformer<Exchange, InstrumentKey, Kind> {
instrument_map: Map<InstrumentKey>,
phantom: PhantomData<(Exchange, Kind)>,
}
impl<Exchange, InstrumentKey, Kind> ExchangeTransformer<Exchange, InstrumentKey, Kind>
for MassiveTransformer<Exchange, InstrumentKey, Kind>
where
Exchange: Connector + Send,
InstrumentKey: Clone + Send + Sync,
Kind: MassiveKind + Send,
Kind::Event: Send,
{
async fn init(
instrument_map: Map<InstrumentKey>,
_: &[MarketEvent<InstrumentKey, Kind::Event>],
_: mpsc::UnboundedSender<WsMessage>,
) -> Result<Self, DataError> {
Ok(Self {
instrument_map,
phantom: PhantomData,
})
}
}
impl<Exchange, InstrumentKey, Kind> Transformer
for MassiveTransformer<Exchange, InstrumentKey, Kind>
where
Exchange: Connector,
InstrumentKey: Clone,
Kind: MassiveKind,
{
type Error = DataError;
type Input = MassiveMessages<Kind::Message>;
type Output = MarketEvent<InstrumentKey, Kind::Event>;
type OutputIter = Vec<Result<Self::Output, Self::Error>>;
fn transform(&mut self, MassiveMessages(messages): Self::Input) -> Self::OutputIter {
let time_received = Utc::now();
messages
.into_iter()
.map(|message| {
let message = message?;
let instrument = self
.instrument_map
.find(&message.subscription_id())
.map_err(|unidentified| DataError::Socket(unidentified.to_string()))?
.clone();
Ok(MarketEvent {
time_exchange: message.time_exchange(),
time_received,
exchange: Exchange::ID,
instrument,
kind: Kind::event(message),
})
})
.collect()
}
}
fn deserialize_id<'de, D>(deserializer: D) -> Result<Option<SmolStr>, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(untagged)]
enum Id {
Text(SmolStr),
Number(u64),
}
Ok(Option::<Id>::deserialize(deserializer)?.map(|id| match id {
Id::Text(text) => text,
Id::Number(number) => format_smolstr!("{number}"),
}))
}
fn millis_to_datetime(millis: i64) -> DateTime<Utc> {
Utc.timestamp_millis_opt(millis)
.single()
.unwrap_or_else(|| {
tracing::warn!(
millis,
"out-of-range millisecond timestamp; using UNIX_EPOCH"
);
DateTime::<Utc>::UNIX_EPOCH
})
}
#[cfg(test)]
#[allow(clippy::unwrap_used)] mod tests {
use super::*;
use crate::{
Identifier,
exchange::massive::{
Massive, MassiveServerCrypto, MassiveServerForex, MassiveServerStocks,
channel::MassiveChannel,
market::{MassiveMarket, MassiveServer},
},
subscriber::mapper::{SubscriptionMapper, WebSocketSubMapper},
subscription::{Subscription, candle::CandleInterval},
};
use rust_decimal_macros::dec;
use rustrade_instrument::{
exchange::ExchangeId,
instrument::market_data::{MarketDataInstrument, kind::MarketDataInstrumentKind},
};
type Events<Kind> = Vec<
Result<MarketEvent<MarketDataInstrument, <Kind as SubscriptionKind>::Event>, DataError>,
>;
fn transform<Server, Kind>(pairs: &[(&str, &str)], kind: Kind, frame: &str) -> Events<Kind>
where
Server: MassiveServer,
Kind: MassiveKind,
Subscription<Massive<Server>, MarketDataInstrument, Kind>:
Identifier<MassiveChannel> + Identifier<MassiveMarket>,
{
let subscriptions = pairs
.iter()
.map(|(base, quote)| {
Subscription::new(
Massive::<Server>::default(),
MarketDataInstrument::new(*base, *quote, MarketDataInstrumentKind::Spot),
kind.clone(),
)
})
.collect::<Vec<_>>();
let mut transformer = MassiveTransformer::<Massive<Server>, _, Kind> {
instrument_map: WebSocketSubMapper::map(&subscriptions).instrument_map,
phantom: PhantomData,
};
transformer.transform(serde_json::from_str(frame).unwrap())
}
fn only<Event>(mut events: Vec<Result<Event, DataError>>) -> Event {
assert_eq!(events.len(), 1);
events.remove(0).unwrap()
}
#[test]
fn a_crypto_trade_carries_its_aggressor_side() {
let event = only(transform::<MassiveServerCrypto, _>(
&[("btc", "usd")],
PublicTrades,
r#"[{"ev":"XT","pair":"BTC-USD","p":45230.5,"s":0.5,"t":1704067200000,"c":[2],"i":"trade123"}]"#,
));
assert_eq!(event.instrument.base.name().as_str(), "btc");
assert_eq!(event.exchange, ExchangeId::MassiveCrypto);
assert_eq!(event.time_exchange.timestamp_millis(), 1704067200000);
assert_eq!(event.kind.price, dec!(45230.5));
assert_eq!(event.kind.amount, dec!(0.5));
assert_eq!(event.kind.side, Some(Side::Buy));
assert_eq!(event.kind.id, "trade123");
}
#[test]
fn a_stock_trade_has_no_side_whatever_its_conditions() {
let event = only(transform::<MassiveServerStocks, _>(
&[("aapl", "usd")],
PublicTrades,
r#"[{"ev":"T","sym":"AAPL","p":150.25,"s":100,"t":1704067200000,"c":[2],"i":12345}]"#,
));
assert_eq!(event.exchange, ExchangeId::MassiveStocks);
assert_eq!(event.kind.price, dec!(150.25));
assert_eq!(event.kind.amount, dec!(100));
assert_eq!(event.kind.side, None);
assert_eq!(event.kind.id, "12345");
}
#[test]
fn a_crypto_quote_is_a_quote_and_a_top_of_book() {
let frame = r#"[{"ev":"XQ","pair":"BTC-USD","bp":45220.0,"bs":2.5,"ap":45240.0,"as":3.0,"lp":45230.0,"ls":0.1,"t":1704067200000}]"#;
let quote = only(transform::<MassiveServerCrypto, _>(
&[("btc", "usd")],
Quotes,
frame,
));
assert_eq!(
quote.kind,
Quote {
bid_price: dec!(45220.0),
bid_amount: dec!(2.5),
ask_price: dec!(45240.0),
ask_amount: dec!(3.0),
}
);
let book = only(transform::<MassiveServerCrypto, _>(
&[("btc", "usd")],
OrderBooksL1,
frame,
));
assert_eq!(
book.kind.best_bid,
Some(Level::new(dec!(45220.0), dec!(2.5)))
);
assert_eq!(
book.kind.best_ask,
Some(Level::new(dec!(45240.0), dec!(3.0)))
);
assert_eq!(book.kind.last_update_time, book.time_exchange);
}
#[test]
fn a_forex_quote_has_zero_amounts() {
let frame =
r#"[{"ev":"C","p":"EUR/USD","b":1.085,"a":1.0852,"i":0,"x":48,"t":1704067200000}]"#;
let quote = only(transform::<MassiveServerForex, _>(
&[("eur", "usd")],
Quotes,
frame,
));
assert_eq!(quote.exchange, ExchangeId::MassiveForex);
assert_eq!(quote.kind.bid_price, dec!(1.085));
assert_eq!(quote.kind.ask_price, dec!(1.0852));
assert_eq!(quote.kind.bid_amount, Decimal::ZERO);
assert_eq!(quote.kind.ask_amount, Decimal::ZERO);
let book = only(transform::<MassiveServerForex, _>(
&[("eur", "usd")],
OrderBooksL1,
frame,
));
assert_eq!(
book.kind.best_bid,
Some(Level::new(dec!(1.085), Decimal::ZERO))
);
}
#[test]
fn a_zero_priced_side_is_absent_from_the_top_of_book() {
let book = only(transform::<MassiveServerCrypto, _>(
&[("btc", "usd")],
OrderBooksL1,
r#"[{"ev":"XQ","pair":"BTC-USD","bp":0,"bs":0,"ap":45240.0,"as":3.0,"t":1704067200000}]"#,
));
assert_eq!(book.kind.best_bid, None);
assert!(book.kind.best_ask.is_some());
}
#[test]
fn an_aggregate_is_a_candle_closing_at_its_window_end() {
let start = 1704067200000_i64;
let candle = only(transform::<MassiveServerStocks, _>(
&[("aapl", "usd")],
Candles {
interval: CandleInterval::Min1,
},
&format!(
r#"[{{"ev":"AM","sym":"AAPL","o":150.1,"h":150.5,"l":150.05,"c":150.25,"v":1000,"vw":150.2,"s":{start},"e":{},"z":78.5}}]"#,
start + 60_000
),
));
assert_eq!(candle.time_exchange.timestamp_millis(), start + 60_000);
assert_eq!(candle.kind.close_time, candle.time_exchange);
assert_eq!(candle.kind.open, dec!(150.1));
assert_eq!(candle.kind.high, dec!(150.5));
assert_eq!(candle.kind.low, dec!(150.05));
assert_eq!(candle.kind.close, dec!(150.25));
assert_eq!(candle.kind.volume, Some(dec!(1000)));
assert_eq!(candle.kind.trade_count, None);
}
#[test]
fn a_forex_aggregate_reports_no_volume() {
let candle = only(transform::<MassiveServerForex, _>(
&[("eur", "usd")],
Candles {
interval: CandleInterval::Sec1,
},
r#"[{"ev":"CAS","pair":"EUR/USD","o":1.1,"h":1.2,"l":1.0,"c":1.15,"v":4321,"s":1704067200000,"e":1704067201000}]"#,
));
assert_eq!(candle.kind.volume, None);
assert_eq!(candle.kind.trade_count, None);
}
#[test]
fn a_message_that_fails_to_decode_loses_only_itself() {
let events = transform::<MassiveServerCrypto, _>(
&[("btc", "usd"), ("eth", "usd")],
PublicTrades,
r#"[{"ev":"XT","pair":"BTC-USD","p":1.0,"s":1.0,"t":1,"i":"1"},
{"ev":"XT","pair":"ETH-USD","p":"not a price","s":1.0,"t":1},
{"ev":"XT","pair":"ETH-USD","p":2.0,"s":1.0,"t":1,"i":"2"}]"#,
);
assert_eq!(events.len(), 3);
assert_eq!(events[0].as_ref().unwrap().kind.price, dec!(1.0));
let error = events[1].as_ref().unwrap_err().to_string();
assert!(
error.contains("failed to decode a Massive message"),
"{error}"
);
assert_eq!(events[2].as_ref().unwrap().kind.price, dec!(2.0));
}
#[test]
fn a_message_for_no_subscription_of_the_stream_is_an_error() {
let events = transform::<MassiveServerCrypto, _>(
&[("btc", "usd")],
PublicTrades,
r#"[{"ev":"XT","pair":"ETH-USD","p":1.0,"s":1.0,"t":1}]"#,
);
assert!(events[0].is_err());
}
}