use std::pin::Pin;
use std::sync::Arc;
use std::task::{Context, Poll};
use std::time::Duration;
use futures::stream::Stream;
use super::handle::SourceStream;
use super::pricing::PriceUpdate;
use super::source::{ReconnectConfig, StreamSource};
use super::yahoo::YahooStreamSource;
use crate::error::FinanceError;
pub type StreamResult<T> = std::result::Result<T, StreamError>;
#[derive(Debug, Clone)]
pub enum StreamError {
ConnectionFailed(String),
WebSocketError(String),
DecodeError(String),
}
impl std::fmt::Display for StreamError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
StreamError::ConnectionFailed(e) => write!(f, "Connection failed: {}", e),
StreamError::WebSocketError(e) => write!(f, "WebSocket error: {}", e),
StreamError::DecodeError(e) => write!(f, "Decode error: {}", e),
}
}
}
impl std::error::Error for StreamError {}
impl From<StreamError> for FinanceError {
fn from(e: StreamError) -> Self {
FinanceError::ResponseStructureError {
field: "streaming".to_string(),
context: e.to_string(),
}
}
}
const RECONNECT_BACKOFF_SECS: u64 = 3;
const CHANNEL_CAPACITY: usize = 1024;
pub struct PriceStream {
inner: SourceStream<PriceUpdate>,
}
impl PriceStream {
pub async fn subscribe<S, I>(symbols: I) -> StreamResult<Self>
where
S: Into<String>,
I: IntoIterator<Item = S>,
{
Self::subscribe_with_source(
Arc::new(YahooStreamSource),
symbols,
ReconnectConfig::new(Duration::from_secs(RECONNECT_BACKOFF_SECS)),
)
.await
}
pub(crate) async fn subscribe_with_source<S, I>(
source: Arc<dyn StreamSource<PriceUpdate>>,
symbols: I,
reconnect: ReconnectConfig,
) -> StreamResult<Self>
where
S: Into<String>,
I: IntoIterator<Item = S>,
{
let initial_symbols: Vec<String> = symbols.into_iter().map(Into::into).collect();
Ok(PriceStream {
inner: SourceStream::start(source, initial_symbols, reconnect, CHANNEL_CAPACITY),
})
}
pub fn resubscribe(&self) -> Self {
PriceStream {
inner: self.inner.resubscribe(),
}
}
pub async fn add_symbols<S, I>(&self, symbols: I)
where
S: Into<String>,
I: IntoIterator<Item = S>,
{
self.inner.add(symbols).await;
}
pub async fn remove_symbols<S, I>(&self, symbols: I)
where
S: Into<String>,
I: IntoIterator<Item = S>,
{
self.inner.remove(symbols).await;
}
pub async fn close(&self) {
self.inner.close().await;
}
}
impl Stream for PriceStream {
type Item = PriceUpdate;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
Pin::new(&mut self.inner).poll_next(cx)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum PriceSource {
#[default]
Yahoo,
#[cfg(feature = "polygon")]
Polygon(crate::streaming::AssetClass),
}
pub struct PriceStreamBuilder {
symbols: Vec<String>,
retry_delay: Duration,
max_reconnect_attempts: Option<u32>,
source: PriceSource,
}
impl PriceStreamBuilder {
pub fn new() -> Self {
Self {
symbols: Vec::new(),
retry_delay: Duration::from_secs(RECONNECT_BACKOFF_SECS),
max_reconnect_attempts: None,
source: PriceSource::Yahoo,
}
}
pub fn source(mut self, source: PriceSource) -> Self {
self.source = source;
self
}
pub fn symbols<S, I>(mut self, symbols: I) -> Self
where
S: Into<String>,
I: IntoIterator<Item = S>,
{
self.symbols.extend(symbols.into_iter().map(Into::into));
self
}
pub fn retry(mut self, delay: Duration) -> Self {
self.retry_delay = delay;
self
}
pub fn max_reconnect_attempts(mut self, max: u32) -> Self {
self.max_reconnect_attempts = Some(max);
self
}
pub async fn build(self) -> StreamResult<PriceStream> {
let source: Arc<dyn StreamSource<PriceUpdate>> = match self.source {
PriceSource::Yahoo => Arc::new(YahooStreamSource),
#[cfg(feature = "polygon")]
PriceSource::Polygon(class) => Arc::new(super::polygon::PolygonPriceSource::new(class)),
};
let reconnect =
ReconnectConfig::new(self.retry_delay).max_attempts(self.max_reconnect_attempts);
PriceStream::subscribe_with_source(source, self.symbols, reconnect).await
}
}
impl Default for PriceStreamBuilder {
fn default() -> Self {
Self::new()
}
}