use crate::{
errors::MarketResult, indicators::EnhancedMarketSeries, publishers::Publisher, MarketError,
};
use chrono::NaiveDateTime;
use serde::{Deserialize, Serialize};
use std::fmt;
use std::str::FromStr;
pub struct MarketClient<T: Publisher> {
pub site: T,
inner: reqwest::Client,
}
impl<T: Publisher> MarketClient<T> {
pub fn new(site: T) -> Self {
Self {
site,
inner: reqwest::Client::builder()
.user_agent("market-data-rust/0.4.0")
.build()
.unwrap_or_default(),
}
}
pub async fn fetch(&self, request: T::Request) -> MarketResult<MarketSeries> {
let url = self.site.create_endpoint(&request)?;
let response = self.inner.get(url).send().await?;
if !response.status().is_success() {
let status = response.status();
let body = response.text().await.unwrap_or_default();
return Err(MarketError::HttpError(format!(
"HTTP Error: status code {}, body: {}",
status, body
)));
}
let body = response.text().await?;
self.site.transform_data(body, &request)
}
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct MarketSeries {
pub symbol: String,
pub interval: Interval,
pub data: Vec<Series>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Series {
pub datetime: NaiveDateTime,
pub open: f32,
pub close: f32,
pub high: f32,
pub low: f32,
pub volume: f64,
}
#[derive(Debug, Serialize, Deserialize, Default, Clone)]
pub enum Interval {
Min1,
Min5,
Min15,
Min30,
Hour1,
Hour2,
Hour4,
#[default]
Daily,
Weekly,
Monthly,
}
impl MarketSeries {
pub fn enhance_data(self) -> EnhancedMarketSeries {
EnhancedMarketSeries {
symbol: self.symbol,
interval: self.interval,
series: self.data,
asks: Vec::new(),
indicators: Default::default(),
}
}
}
impl fmt::Display for MarketSeries {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(
f,
"MarketSeries: Symbol = {}, Interval = {}",
self.symbol, self.interval
)?;
for series in &self.data {
writeln!(f, " {}", series)?;
}
Ok(())
}
}
impl fmt::Display for Series {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"DateTime: {}, Open: {:.2}, Close: {:.2}, High: {:.2}, Low: {:.2}, Volume: {:.2}",
self.datetime, self.open, self.close, self.high, self.low, self.volume
)
}
}
impl fmt::Display for Interval {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let interval_str = match self {
Interval::Min1 => "1 minute",
Interval::Min5 => "5 minutes",
Interval::Min15 => "15 minutes",
Interval::Min30 => "30 minutes",
Interval::Hour1 => "1 hour",
Interval::Hour2 => "2 hours",
Interval::Hour4 => "4 hours",
Interval::Daily => "Daily",
Interval::Weekly => "Weekly",
Interval::Monthly => "Monthly",
};
write!(f, "{}", interval_str)
}
}
impl std::str::FromStr for Interval {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"1min" => Ok(Interval::Min1),
"5min" => Ok(Interval::Min5),
"15min" => Ok(Interval::Min15),
"30min" => Ok(Interval::Min30),
"60min" => Ok(Interval::Hour1),
"1h" => Ok(Interval::Hour1),
"2h" => Ok(Interval::Hour2),
"4h" => Ok(Interval::Hour4),
"1day" => Ok(Interval::Daily),
"1week" => Ok(Interval::Weekly),
"1month" => Ok(Interval::Monthly),
_ => Err("Invalid interval string"),
}
}
}
impl From<String> for Interval {
fn from(s: String) -> Self {
Interval::from_str(&s).unwrap_or(Interval::Daily)
}
}