use polars::prelude::*;
use time::OffsetDateTime;
use yahoo::YahooError;
use yahoo_finance_api as yahoo;
use RustQuant_error::RustQuantError;
pub struct YahooFinanceData {
pub ticker: Option<String>,
pub start: Option<OffsetDateTime>,
pub end: Option<OffsetDateTime>,
pub price_history: Option<DataFrame>,
pub returns: Option<DataFrame>,
pub options_chain: Option<DataFrame>,
pub latest_quote: Option<DataFrame>,
}
pub enum ReturnsType {
Arithmetic,
Logarithmic,
Absolute,
}
pub trait YahooFinanceReader {
fn get_price_history(&mut self) -> Result<(), RustQuantError>;
fn get_latest_quote(&mut self) -> Result<(), RustQuantError>;
}
impl Default for YahooFinanceData {
fn default() -> Self {
Self {
ticker: None,
start: Some(OffsetDateTime::UNIX_EPOCH),
end: Some(OffsetDateTime::now_utc()),
price_history: None,
returns: None,
options_chain: None,
latest_quote: None,
}
}
}
impl YahooFinanceData {
pub fn new(ticker: String) -> Self {
Self {
ticker: Some(ticker),
..Default::default()
}
}
pub fn set_start_date(&mut self, start: OffsetDateTime) {
self.start = Some(start);
}
pub fn set_end_date(&mut self, end: OffsetDateTime) {
self.end = Some(end);
}
pub fn set_date_range(&mut self, start: OffsetDateTime, end: OffsetDateTime) {
self.start = Some(start);
self.end = Some(end);
}
pub fn compute_returns(&mut self, returns_type: ReturnsType) -> Result<(), RustQuantError> {
if self.price_history.is_none() {
self.get_price_history()?
}
let price_columns = || col("*").exclude(["date", "volume"]);
match returns_type {
ReturnsType::Arithmetic => {
self.returns = Some(
self.price_history
.clone()
.ok_or(YahooError::EmptyDataSet)?
.lazy()
.select(vec![
col("date"),
col("volume"),
(price_columns() / price_columns().shift(lit(1)) - lit(1.))
.name()
.suffix("_arithmetic"),
])
.collect()?,
);
}
ReturnsType::Absolute => {
self.returns = Some(
self.price_history
.clone()
.ok_or(YahooError::EmptyDataSet)?
.lazy()
.select(vec![
col("date"),
col("volume"),
(price_columns() - price_columns().shift(lit(1)))
.name()
.suffix("_absolute"),
])
.collect()?,
);
}
ReturnsType::Logarithmic => {
fn logarithm(col: &Column) -> Series {
col.f64().unwrap().apply(|x| Some(x?.ln())).into_series()
}
let mut prices = self.price_history.clone().ok_or(YahooError::EmptyDataSet)?;
prices.apply("open", logarithm)?;
prices.apply("high", logarithm)?;
prices.apply("low", logarithm)?;
prices.apply("close", logarithm)?;
prices.apply("adjusted", logarithm)?;
self.returns = Some(
prices
.lazy()
.select(vec![
col("date"),
col("volume"),
(price_columns() - price_columns().shift(lit(1)))
.name()
.suffix("_logarithmic"),
])
.collect()?,
);
}
}
Ok(())
}
}
impl YahooFinanceReader for YahooFinanceData {
fn get_price_history(&mut self) -> Result<(), RustQuantError> {
let provider = yahoo::YahooConnector::new()?;
let response = tokio_test::block_on(provider.get_quote_history(
self.ticker.as_ref().ok_or(RustQuantError::MissingInput(
"No ticker provided.".to_string(),
))?,
self.start.unwrap_or(OffsetDateTime::UNIX_EPOCH),
self.end.unwrap_or(OffsetDateTime::now_utc()),
))?;
let quotes = response.quotes()?;
let date = quotes
.iter()
.map(|q| (q.timestamp / (24 * 60 * 60)) as i32)
.collect::<Vec<_>>();
let open = quotes.iter().map(|q| q.open).collect::<Vec<_>>();
let high = quotes.iter().map(|q| q.high).collect::<Vec<_>>();
let low = quotes.iter().map(|q| q.low).collect::<Vec<_>>();
let close = quotes.iter().map(|q| q.close).collect::<Vec<_>>();
let volume = quotes.iter().map(|q| q.volume as f64).collect::<Vec<_>>();
let adjclose = quotes.iter().map(|q| q.adjclose).collect::<Vec<_>>();
let df = df!(
"date" => Series::new("date".into(), date).cast(&DataType::Date)?,
"open" => open,
"high" => high,
"low" => low,
"close" => close,
"volume" => volume,
"adjusted" => adjclose
);
self.price_history = Some(df?);
Ok(())
}
fn get_latest_quote(&mut self) -> Result<(), RustQuantError> {
let provider = yahoo::YahooConnector::new()?;
let response = tokio_test::block_on(
provider.get_latest_quotes(
self.ticker
.as_ref()
.ok_or(RustQuantError::MissingInput(
"No ticker provided.".to_string(),
))?
.as_str(),
"1d",
),
)
.unwrap();
let quote = response.last_quote().unwrap();
let timestamp = vec![quote.timestamp];
let open = vec![quote.open];
let high = vec![quote.high];
let low = vec![quote.low];
let close = vec![quote.close];
let volume = vec![quote.volume as f64];
let adjclose = vec![quote.adjclose];
let df = df!(
"timestamp" => timestamp,
"open" => open,
"high" => high,
"low" => low,
"close" => close,
"volume" => volume,
"adjusted" => adjclose
);
self.latest_quote = Some(df?);
Ok(())
}
}
#[cfg(test)]
mod test_yahoo {
use super::*;
#[test]
fn test_get_price_history() {
let mut yfd = YahooFinanceData::new("AAPL".to_string());
yfd.set_start_date(time::macros::datetime!(2019 - 01 - 01 0:00 UTC));
yfd.set_end_date(time::macros::datetime!(2020 - 01 - 01 0:00 UTC));
let _ = yfd.get_price_history();
println!("Apple's quotes: {:?}", yfd.price_history)
}
#[test]
fn test_compute_returns() {
let mut yfd = YahooFinanceData::new("AAPL".to_string());
yfd.set_start_date(time::macros::datetime!(2019 - 01 - 01 0:00 UTC));
yfd.set_end_date(time::macros::datetime!(2020 - 01 - 01 0:00 UTC));
let _ = yfd.compute_returns(ReturnsType::Logarithmic);
println!("Apple's returns: {:?}", yfd.returns)
}
#[test]
fn test_get_latest_quote() {
let mut yfd = YahooFinanceData::new("AAPL".to_string());
let _ = yfd.get_latest_quote();
println!("Apple's latest quote: {:?}", yfd.latest_quote)
}
}