use std::path::PathBuf;
use std::sync::Arc;
use digdigdig3::connector_manager::ExchangeHub;
use digdigdig3::core::types::{StreamEvent, SubscriptionRequest, Symbol};
use digdigdig3::core::utils::SymbolNormalizer;
use futures_util::StreamExt;
use tokio::sync::{mpsc, oneshot};
use crate::persistence::TradeWriter;
use crate::subscription::{Entry, Event, Stream};
use crate::{
PersistenceConfig, Result, StationBuilder, StationError, SubscriptionHandle, SubscriptionSet,
};
pub struct Station {
hub: Arc<ExchangeHub>,
storage_root: PathBuf,
persistence: PersistenceConfig,
}
impl std::fmt::Debug for Station {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Station")
.field("storage_root", &self.storage_root)
.field("persistence", &self.persistence)
.finish()
}
}
impl Station {
pub fn builder() -> StationBuilder {
StationBuilder::new()
}
pub fn storage_root(&self) -> &std::path::Path {
&self.storage_root
}
pub(crate) async fn from_builder(b: StationBuilder) -> Result<Self> {
let _ = digdigdig3::core::install_default_crypto_provider();
if b.persistence.enabled {
std::fs::create_dir_all(&b.storage_root).map_err(StationError::Io)?;
}
Ok(Self {
hub: Arc::new(ExchangeHub::new()),
storage_root: b.storage_root,
persistence: b.persistence,
})
}
pub async fn subscribe(&self, set: SubscriptionSet) -> Result<SubscriptionHandle> {
if set.is_empty() {
return Err(StationError::Subscribe("empty SubscriptionSet".into()));
}
let (tx, rx) = mpsc::unbounded_channel::<Event>();
let (shutdown_tx, mut shutdown_rx) = oneshot::channel::<()>();
for entry in set.entries {
self.start_entry(entry, tx.clone()).await?;
}
drop(tx);
tokio::spawn(async move {
let _ = shutdown_rx.try_recv();
});
Ok(SubscriptionHandle {
rx,
_shutdown: shutdown_tx,
})
}
async fn start_entry(&self, entry: Entry, tx: mpsc::UnboundedSender<Event>) -> Result<()> {
self.hub
.connect_websocket(entry.exchange, entry.account_type, false)
.await
.map_err(|e| StationError::Core(format!("connect_websocket: {e}")))?;
let ws = self
.hub
.ws(entry.exchange, entry.account_type)
.ok_or_else(|| StationError::Core("ws handle missing post-connect".into()))?;
let canonical = parse_symbol(&entry.symbol);
let raw_for_exchange = SymbolNormalizer::to_exchange(
entry.exchange,
&canonical,
entry.account_type,
)
.map_err(|e| StationError::Subscribe(format!("symbol normalize: {e}")))?;
let sym = Symbol::with_raw(&canonical.base, &canonical.quote, raw_for_exchange.clone());
let want_trade = entry.streams.iter().any(|s| matches!(s, Stream::Trade));
if !want_trade {
return Ok(());
}
ws.subscribe(SubscriptionRequest::trade_for(sym.clone(), entry.account_type))
.await
.map_err(|e| StationError::Subscribe(format!("ws.subscribe trade: {e}")))?;
let mut writer: Option<TradeWriter> = None;
if self.persistence.enabled && self.persistence.trades {
match TradeWriter::new(
&self.storage_root,
&format!("{:?}", entry.exchange).to_lowercase(),
entry.account_type.as_key_str(),
&raw_for_exchange,
) {
Ok(w) => writer = Some(w),
Err(e) => {
tracing::warn!(?e, exchange=?entry.exchange, "trade writer open failed; continuing without persistence");
}
}
}
let exchange = entry.exchange;
let symbol_label = entry.symbol.clone();
let mut stream = ws.event_stream();
tokio::spawn(async move {
let mut writer = writer; while let Some(item) = stream.next().await {
let ev = match item {
Ok(ev) => ev,
Err(e) => {
tracing::warn!(?e, "ws event_stream yielded err");
continue;
}
};
if let StreamEvent::Trade(t) = ev {
if let Some(w) = writer.as_mut() {
if let Err(e) = w.append(t.timestamp, t.price, t.quantity, t.side, &t.id) {
tracing::warn!(?e, "trade writer append failed");
}
}
let out = Event::Trade {
exchange,
symbol: symbol_label.clone(),
price: t.price,
quantity: t.quantity,
side: format!("{:?}", t.side),
timestamp: t.timestamp,
};
if tx.send(out).is_err() {
break;
}
}
}
});
Ok(())
}
}
fn parse_symbol(s: &str) -> Symbol {
if let Some((b, q)) = s.split_once(['-', '/', '_']) {
return Symbol::new(b, q);
}
let upper = s.to_uppercase();
for q in ["USDT", "USDC", "USD", "BTC", "ETH", "BUSD", "EUR", "JPY"] {
if let Some(base) = upper.strip_suffix(q) {
if !base.is_empty() {
return Symbol::new(base, q);
}
}
}
Symbol::new(&upper, "")
}