use async_stream::try_stream;
use futures::{Stream, StreamExt};
use smol_str::SmolStr;
use tracing::warn;
use rustrade_instrument::corporate_action::{CorporateAction, CorporateActionKind};
use rustrade_integration::corporate_action::{CorporateActionFilter, StockSplitSource};
use super::error::MassiveError;
use super::reference::{SplitQuery, StockSplit};
use super::rest::MassiveRestClient;
impl StockSplitSource for MassiveRestClient {
type Error = MassiveError;
fn fetch_splits(
&self,
filter: &CorporateActionFilter,
) -> impl Stream<Item = Result<CorporateAction<SmolStr>, Self::Error>> + Send {
let queries = build_split_queries(filter);
try_stream! {
for query in &queries {
let raw = self.fetch_splits_raw(query);
futures::pin_mut!(raw);
while let Some(split) = raw.next().await {
if let Some(action) = map_split(split?) {
yield action;
}
}
}
}
}
}
fn build_split_queries(filter: &CorporateActionFilter) -> Vec<SplitQuery> {
let with_dates = |mut query: SplitQuery| {
if let Some(start) = filter.start {
query = query.execution_date_gte(start);
}
if let Some(end) = filter.end {
query = query.execution_date_lte(end);
}
query
};
if filter.symbols.is_empty() {
vec![with_dates(SplitQuery::new())]
} else {
filter
.symbols
.iter()
.map(|symbol| with_dates(SplitQuery::new().ticker(symbol.as_str())))
.collect()
}
}
fn map_split(split: StockSplit) -> Option<CorporateAction<SmolStr>> {
match CorporateActionKind::stock_split(split.split_to, split.split_from) {
Some(kind) => Some(CorporateAction::new(
SmolStr::from(split.ticker),
kind,
Some(split.execution_date),
)),
None => {
warn!(
ticker = %split.ticker,
split_to = %split.split_to,
split_from = %split.split_from,
"Massive stock split has a degenerate ratio; skipping",
);
None
}
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)] mod tests {
use super::*;
use chrono::NaiveDate;
use rust_decimal::Decimal;
use rustrade_instrument::corporate_action::SplitRatio;
#[test]
fn build_split_queries_fans_out_per_symbol() {
let filter = CorporateActionFilter::new(
vec![SmolStr::new("AAPL"), SmolStr::new("NVDA")],
NaiveDate::from_ymd_opt(2020, 1, 1),
NaiveDate::from_ymd_opt(2024, 12, 31),
);
let queries = build_split_queries(&filter);
assert_eq!(queries.len(), 2);
assert_eq!(queries[0].ticker.as_deref(), Some("AAPL"));
assert_eq!(queries[1].ticker.as_deref(), Some("NVDA"));
assert_eq!(queries[0].execution_date_gte, filter.start);
assert_eq!(queries[0].execution_date_lte, filter.end);
}
#[test]
fn build_split_queries_empty_symbols_is_one_unfiltered_query() {
let filter = CorporateActionFilter::new(vec![], NaiveDate::from_ymd_opt(2023, 1, 1), None);
let queries = build_split_queries(&filter);
assert_eq!(queries.len(), 1);
assert_eq!(queries[0].ticker, None);
assert_eq!(queries[0].execution_date_gte, filter.start);
assert_eq!(queries[0].execution_date_lte, None);
}
#[test]
fn map_split_computes_forward_ratio() {
let split = StockSplit {
ticker: "AAPL".to_string(),
execution_date: NaiveDate::from_ymd_opt(2020, 8, 31).unwrap(),
split_to: Decimal::from(4),
split_from: Decimal::from(1),
};
let action = map_split(split).unwrap();
assert_eq!(action.instrument, SmolStr::new("AAPL"));
assert_eq!(
action.kind,
CorporateActionKind::StockSplit {
ratio: SplitRatio::new(Decimal::from(4)).unwrap()
}
);
assert_eq!(action.effective_date, NaiveDate::from_ymd_opt(2020, 8, 31));
}
#[test]
fn map_split_drops_degenerate_ratio() {
let split = StockSplit {
ticker: "BAD".to_string(),
execution_date: NaiveDate::from_ymd_opt(2023, 1, 1).unwrap(),
split_to: Decimal::from(1),
split_from: Decimal::ZERO,
};
assert!(map_split(split).is_none());
}
#[tokio::test]
async fn fetch_splits_via_trait_object_is_stream_of_descriptors() {
struct OneSplit;
impl StockSplitSource for OneSplit {
type Error = std::convert::Infallible;
fn fetch_splits(
&self,
_filter: &CorporateActionFilter,
) -> impl Stream<Item = Result<CorporateAction<SmolStr>, Self::Error>> + Send
{
futures::stream::iter([Ok(CorporateAction::new(
SmolStr::new("TSLA"),
CorporateActionKind::StockSplit {
ratio: SplitRatio::new(Decimal::from(3)).unwrap(),
},
NaiveDate::from_ymd_opt(2022, 8, 25),
))])
}
}
let actions: Vec<_> = OneSplit
.fetch_splits(&CorporateActionFilter::default())
.collect()
.await;
assert_eq!(actions.len(), 1);
assert_eq!(
actions[0].as_ref().unwrap().instrument,
SmolStr::new("TSLA")
);
}
}