use crate::transport::ReqwestTransport;
use eventsource_client::{Client, ClientBuilder, SSE};
use futures::{Stream, StreamExt, TryStreamExt};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct OdbSseConfig {
pub host: String,
pub port: u16,
pub symbols: Vec<String>,
pub thresholds: Vec<u32>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OdbBar {
pub symbol: String,
pub threshold: u32,
pub open_time_us: i64,
pub close_time_us: i64,
pub open: f64,
pub high: f64,
pub low: f64,
pub close: f64,
pub volume: Option<f64>,
pub vwap: Option<f64>,
pub buy_volume: Option<f64>,
pub sell_volume: Option<f64>,
pub individual_trade_count: Option<u32>,
pub agg_record_count: Option<u32>,
pub first_ref_id: Option<i64>,
pub last_ref_id: Option<i64>,
pub duration_us: Option<i64>,
pub ofi: Option<f64>,
pub vwap_close_deviation: Option<f64>,
pub price_impact: Option<f64>,
pub kyle_lambda_proxy: Option<f64>,
pub trade_intensity: Option<f64>,
pub volume_per_trade: Option<f64>,
pub aggression_ratio: Option<f64>,
pub aggregation_density: Option<f64>,
pub turnover_imbalance: Option<f64>,
pub is_orphan: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub is_forming: Option<bool>,
#[serde(flatten)]
pub extra: HashMap<String, serde_json::Value>,
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum OdbSseEvent {
Connected,
Bar(Box<OdbBar>),
FormingBar(Box<OdbBar>),
Checkpoint {
checkpoints: serde_json::Value,
},
Heartbeat,
DeserializationError {
error: String,
raw_data: String,
},
Disconnected(String),
}
#[derive(Clone)]
pub struct OdbSseClient {
config: OdbSseConfig,
}
impl OdbSseClient {
pub fn new(config: OdbSseConfig) -> Self {
Self { config }
}
#[doc(hidden)]
pub fn build_url(&self) -> String {
let mut url = format!(
"http://{}:{}/events/bars",
self.config.host, self.config.port
);
let mut params = Vec::new();
if !self.config.symbols.is_empty() {
params.push(format!("symbols={}", self.config.symbols.join(",")));
}
if !self.config.thresholds.is_empty() {
let ts: Vec<String> = self
.config
.thresholds
.iter()
.map(ToString::to_string)
.collect();
params.push(format!("thresholds={}", ts.join(",")));
}
if !params.is_empty() {
url.push('?');
url.push_str(¶ms.join("&"));
}
url
}
pub fn connect(&self) -> impl Stream<Item = OdbSseEvent> + Send + '_ {
let url = self.build_url();
futures::stream::once(async move {
let init_result = ReqwestTransport::new()
.map_err(|e| format!("failed to build HTTP client: {e}"))
.and_then(|transport| {
ClientBuilder::for_url(&url)
.map_err(|e| format!("invalid URL: {e}"))
.map(|builder| builder.build_with_transport(transport))
});
let client = match init_result {
Ok(c) => c,
Err(msg) => {
return futures::stream::once(async move { OdbSseEvent::Disconnected(msg) })
.left_stream();
}
};
let stream = client.stream();
stream
.map_ok(|sse| match sse {
SSE::Connected(_) => {
tracing::info!(target: "odb_client", "SSE connected");
Some(OdbSseEvent::Connected)
}
SSE::Comment(comment) => {
if comment.trim() == "heartbeat" {
Some(OdbSseEvent::Heartbeat)
} else {
tracing::debug!(
target: "odb_client",
comment = %comment,
"ignoring unknown SSE comment"
);
None
}
}
SSE::Event(evt) => {
match evt.event_type.as_str() {
"bar" => match serde_json::from_str::<OdbBar>(&evt.data) {
Ok(bar) => {
tracing::info!(
target: "odb_client::bar",
symbol = %bar.symbol,
threshold = bar.threshold,
close_time_us = bar.close_time_us,
open = bar.open,
high = bar.high,
low = bar.low,
close = bar.close,
buy_volume = bar.buy_volume,
sell_volume = bar.sell_volume,
source = "sse",
"bar_received"
);
Some(OdbSseEvent::Bar(Box::new(bar)))
}
Err(e) => {
tracing::warn!(
target: "odb_client",
error = %e,
data = %evt.data,
"failed to deserialize bar event"
);
Some(OdbSseEvent::DeserializationError {
error: e.to_string(),
raw_data: evt.data,
})
}
},
"forming" => match serde_json::from_str::<OdbBar>(&evt.data) {
Ok(bar) => {
tracing::debug!(
target: "odb_client::forming",
symbol = %bar.symbol,
threshold = bar.threshold,
close = bar.close,
"forming_bar_received"
);
Some(OdbSseEvent::FormingBar(Box::new(bar)))
}
Err(e) => {
tracing::debug!(
target: "odb_client",
error = %e,
"failed to deserialize forming bar"
);
Some(OdbSseEvent::DeserializationError {
error: e.to_string(),
raw_data: evt.data,
})
}
},
"checkpoint" => {
match serde_json::from_str::<serde_json::Value>(&evt.data) {
Ok(data) => {
let checkpoints =
data.get("checkpoints").cloned().unwrap_or(data);
tracing::debug!(
target: "odb_client::checkpoint",
"checkpoint_received"
);
Some(OdbSseEvent::Checkpoint { checkpoints })
}
Err(e) => {
tracing::debug!(
target: "odb_client",
error = %e,
"failed to deserialize checkpoint"
);
Some(OdbSseEvent::DeserializationError {
error: e.to_string(),
raw_data: evt.data,
})
}
}
}
_ => {
tracing::debug!(
target: "odb_client",
event_type = %evt.event_type,
"ignoring unknown SSE event type"
);
None
}
}
}
})
.filter_map(|result| async move {
match result {
Ok(Some(event)) => Some(event),
Ok(None) => None, Err(e) => Some(OdbSseEvent::Disconnected(format!("{e}"))),
}
})
.chain(futures::stream::once(async {
OdbSseEvent::Disconnected("stream ended".into())
}))
.right_stream()
})
.flatten()
}
}