use std::{borrow::Cow, sync::Arc};
use bytes::Bytes;
use nautilus_core::{Params, UUID4, UnixNanos};
use nautilus_model::{
data::{BookOrder, CustomData, Data, DataType},
enums::{BookType, OrderSide, RecordFlag},
identifiers::{ClientId, InstrumentId, Venue},
orderbook::OrderBook,
};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use crate::messages::data::{
BarsResponse, BookDeltasResponse, BookDepthResponse, BookResponse, CustomDataResponse,
DataResponse, FundingRatesResponse, InstrumentResponse, InstrumentsResponse,
OptionChainReferencePriceResponse, Payload, QuotesResponse, TradesResponse,
};
impl Serialize for DataResponse {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
WireResponse::from_native(self)
.map_err(serde::ser::Error::custom)?
.serialize(serializer)
}
}
impl<'de> Deserialize<'de> for DataResponse {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
WireResponse::deserialize(deserializer)?
.into_native()
.map_err(serde::de::Error::custom)
}
}
#[derive(Serialize, Deserialize)]
#[allow(
clippy::large_enum_variant,
reason = "model features vary enum sizes; transient serialization views avoid an extra allocation"
)]
enum WireResponse<'a> {
Data(WireCustomResponse<'a>),
Book(WireBookResponse<'a>),
Instrument(Cow<'a, InstrumentResponse>),
Instruments(Cow<'a, InstrumentsResponse>),
BookDeltas(Cow<'a, BookDeltasResponse>),
BookDepth(Cow<'a, BookDepthResponse>),
Quotes(Cow<'a, QuotesResponse>),
Trades(Cow<'a, TradesResponse>),
FundingRates(Cow<'a, FundingRatesResponse>),
OptionChainReferencePrice(Cow<'a, OptionChainReferencePriceResponse>),
Bars(Cow<'a, BarsResponse>),
}
impl<'a> WireResponse<'a> {
fn from_native(value: &'a DataResponse) -> anyhow::Result<Self> {
Ok(match value {
DataResponse::Data(value) => Self::Data(WireCustomResponse::from_native(value)?),
DataResponse::Book(value) => Self::Book(WireBookResponse::from_native(value)),
DataResponse::Instrument(value) => Self::Instrument(Cow::Borrowed(value)),
DataResponse::Instruments(value) => Self::Instruments(Cow::Borrowed(value)),
DataResponse::BookDeltas(value) => Self::BookDeltas(Cow::Borrowed(value)),
DataResponse::BookDepth(value) => Self::BookDepth(Cow::Borrowed(value)),
DataResponse::Quotes(value) => Self::Quotes(Cow::Borrowed(value)),
DataResponse::Trades(value) => Self::Trades(Cow::Borrowed(value)),
DataResponse::FundingRates(value) => Self::FundingRates(Cow::Borrowed(value)),
DataResponse::OptionChainReferencePrice(value) => {
Self::OptionChainReferencePrice(Cow::Borrowed(value))
}
DataResponse::Bars(value) => Self::Bars(Cow::Borrowed(value)),
})
}
fn into_native(self) -> anyhow::Result<DataResponse> {
Ok(match self {
Self::Data(value) => DataResponse::Data(value.into_native()?),
Self::Book(value) => DataResponse::Book(value.into_native()?),
Self::Instrument(value) => DataResponse::Instrument(Box::new(value.into_owned())),
Self::Instruments(value) => DataResponse::Instruments(value.into_owned()),
Self::BookDeltas(value) => DataResponse::BookDeltas(value.into_owned()),
Self::BookDepth(value) => DataResponse::BookDepth(value.into_owned()),
Self::Quotes(value) => DataResponse::Quotes(value.into_owned()),
Self::Trades(value) => DataResponse::Trades(value.into_owned()),
Self::FundingRates(value) => DataResponse::FundingRates(value.into_owned()),
Self::OptionChainReferencePrice(value) => {
DataResponse::OptionChainReferencePrice(value.into_owned())
}
Self::Bars(value) => DataResponse::Bars(value.into_owned()),
})
}
}
#[derive(Serialize, Deserialize)]
struct WireCustomResponse<'a> {
correlation_id: UUID4,
client_id: ClientId,
venue: Option<Venue>,
data_type: Cow<'a, DataType>,
data: CustomPayload<'a>,
start: Option<UnixNanos>,
end: Option<UnixNanos>,
ts_init: UnixNanos,
params: Cow<'a, Option<Params>>,
}
impl<'a> WireCustomResponse<'a> {
fn from_native(value: &'a CustomDataResponse) -> anyhow::Result<Self> {
Ok(Self {
correlation_id: value.correlation_id,
client_id: value.client_id,
venue: value.venue,
data_type: Cow::Borrowed(&value.data_type),
data: CustomPayload::from_native(value.data.as_ref())?,
start: value.start,
end: value.end,
ts_init: value.ts_init,
params: Cow::Borrowed(&value.params),
})
}
fn into_native(self) -> anyhow::Result<CustomDataResponse> {
Ok(CustomDataResponse {
correlation_id: self.correlation_id,
client_id: self.client_id,
venue: self.venue,
data_type: self.data_type.into_owned(),
data: self.data.into_native()?,
start: self.start,
end: self.end,
ts_init: self.ts_init,
params: self.params.into_owned(),
})
}
}
#[derive(Serialize, Deserialize)]
#[expect(
clippy::large_enum_variant,
reason = "transient serialization views avoid an extra allocation for scalar payloads"
)]
pub(super) enum CustomPayload<'a> {
Scalar(CustomItem<'a>),
Batch(Vec<CustomItem<'a>>),
Bytes(Cow<'a, [u8]>),
Buffer(Cow<'a, Bytes>),
Text(Cow<'a, str>),
}
impl<'a> CustomPayload<'a> {
pub(super) fn from_native(value: &'a dyn std::any::Any) -> anyhow::Result<Self> {
if let Some(value) = value.downcast_ref::<CustomData>() {
return Ok(Self::Scalar(CustomItem::from_native(value)));
}
if let Some(value) = value.downcast_ref::<Vec<CustomData>>() {
return Ok(Self::Batch(
value.iter().map(CustomItem::from_native).collect(),
));
}
if let Some(value) = value.downcast_ref::<Vec<u8>>() {
return Ok(Self::Bytes(Cow::Borrowed(value)));
}
if let Some(value) = value.downcast_ref::<Bytes>() {
return Ok(Self::Buffer(Cow::Borrowed(value)));
}
if let Some(value) = value.downcast_ref::<String>() {
return Ok(Self::Text(Cow::Borrowed(value)));
}
anyhow::bail!(
"custom response payload requires CustomData, Vec<CustomData>, Vec<u8>, Bytes, or String"
)
}
pub(super) fn into_native(self) -> anyhow::Result<Payload> {
Ok(match self {
Self::Scalar(value) => Arc::new(value.into_native()?),
Self::Batch(values) => Arc::new(
values
.into_iter()
.map(CustomItem::into_native)
.collect::<anyhow::Result<Vec<_>>>()?,
),
Self::Bytes(value) => Arc::new(value.into_owned()),
Self::Buffer(value) => Arc::new(value.into_owned()),
Self::Text(value) => Arc::new(value.into_owned()),
})
}
}
#[derive(Serialize, Deserialize)]
pub(super) struct CustomItem<'a> {
pub(super) data_type: Cow<'a, DataType>,
pub(super) value: CustomValue<'a>,
}
impl<'a> CustomItem<'a> {
pub(super) fn from_native(value: &'a CustomData) -> Self {
Self {
data_type: Cow::Borrowed(&value.data_type),
value: CustomValue::Borrowed(value),
}
}
fn into_native(self) -> anyhow::Result<CustomData> {
let mut value = match self.value {
CustomValue::Borrowed(value) => value.clone(),
CustomValue::Owned(Data::Custom(value)) => value,
CustomValue::Owned(_) => {
anyhow::bail!("custom response contains a non-custom payload")
}
};
value.data_type = self.data_type.into_owned();
Ok(value)
}
}
#[derive(Serialize)]
#[serde(untagged)]
#[allow(
clippy::large_enum_variant,
reason = "model features vary enum sizes; owned decoding avoids an extra allocation"
)]
pub(super) enum CustomValue<'a> {
Borrowed(&'a CustomData),
Owned(Data),
}
impl<'de> Deserialize<'de> for CustomValue<'_> {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
Data::deserialize(deserializer).map(Self::Owned)
}
}
#[derive(Serialize, Deserialize)]
struct WireBookResponse<'a> {
correlation_id: UUID4,
client_id: ClientId,
instrument_id: InstrumentId,
data: BookState,
start: Option<UnixNanos>,
end: Option<UnixNanos>,
ts_init: UnixNanos,
params: Cow<'a, Option<Params>>,
}
impl<'a> WireBookResponse<'a> {
fn from_native(value: &'a BookResponse) -> Self {
let book = &value.data;
Self {
correlation_id: value.correlation_id,
client_id: value.client_id,
instrument_id: value.instrument_id,
data: BookState {
instrument_id: book.instrument_id,
book_type: book.book_type,
sequence: book.sequence,
ts_last: book.ts_last,
update_count: book.update_count,
orders: book
.bids(None)
.chain(book.asks(None))
.flat_map(|level| level.iter().copied())
.collect(),
bid_batch_state: book.batch_state_code(OrderSide::Buy),
ask_batch_state: book.batch_state_code(OrderSide::Sell),
},
start: value.start,
end: value.end,
ts_init: value.ts_init,
params: Cow::Borrowed(&value.params),
}
}
fn into_native(self) -> anyhow::Result<BookResponse> {
let mut book = OrderBook::new(self.data.instrument_id, self.data.book_type);
for order in self.data.orders {
anyhow::ensure!(order.side.is_some(), "book snapshot order has no side");
book.add(
order,
RecordFlag::F_SNAPSHOT as u8 | RecordFlag::F_LAST as u8,
self.data.sequence,
self.data.ts_last,
);
}
book.set_batch_state_code(OrderSide::Buy, self.data.bid_batch_state)?;
book.set_batch_state_code(OrderSide::Sell, self.data.ask_batch_state)?;
book.sequence = self.data.sequence;
book.ts_last = self.data.ts_last;
book.update_count = self.data.update_count;
Ok(BookResponse {
correlation_id: self.correlation_id,
client_id: self.client_id,
instrument_id: self.instrument_id,
data: book,
start: self.start,
end: self.end,
ts_init: self.ts_init,
params: self.params.into_owned(),
})
}
}
#[derive(Serialize, Deserialize)]
struct BookState {
instrument_id: InstrumentId,
book_type: BookType,
sequence: u64,
ts_last: UnixNanos,
update_count: u64,
orders: Vec<BookOrder>,
#[serde(default)]
bid_batch_state: u8,
#[serde(default)]
ask_batch_state: u8,
}