equity_scanner/
client.rs

1use crate::equity::Equity;
2use crate::utils::formatter::year_url;
3use crate::utils::structs::HistoricalData;
4use crate::utils::yahoo::ApiResponse;
5use reqwest::Client;
6use serde_json::Value;
7use std::error::Error;
8use std::time::{Duration, Instant};
9/// Client used for holding data for a ticker. All in one package for requesting and analysing data
10#[derive(Debug)]
11pub struct YahooFinanceClient {
12    client: Client,
13    crumb: Option<String>,
14    last_refresh: Option<Instant>,
15}
16#[derive(Debug)]
17pub struct InvalidRequest;
18
19impl YahooFinanceClient {
20    pub async fn new() -> Result<Self, Box<dyn Error>> {
21        let client = Client::builder()
22            .cookie_store(true)
23            .https_only(true)
24            .user_agent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36")
25            .build()?;
26
27        Ok(YahooFinanceClient {
28            client,
29            crumb: None,
30            last_refresh: None,
31        })
32    }
33
34    async fn refresh_crumb(&mut self) -> Result<(), Box<dyn Error>> {
35        self.client.get("https://fc.yahoo.com").send().await?;
36
37        let crumb = self
38            .client
39            .get("https://query1.finance.yahoo.com/v1/test/getcrumb")
40            .send()
41            .await?
42            .text()
43            .await?;
44
45        self.crumb = Some(crumb);
46        self.last_refresh = Some(Instant::now());
47        Ok(())
48    }
49
50    /// Ensures the crumb is valid before procceding
51    async fn ensure_crumb_valid(&mut self) -> Result<(), Box<dyn Error>> {
52        let crumb_ttl = Duration::from_secs(15 * 60); // 15 minutes
53        match (self.crumb.as_ref(), self.last_refresh) {
54            (Some(_), Some(t)) if t.elapsed() < crumb_ttl => Ok(()),
55            _ => self.refresh_crumb().await,
56        }
57    }
58    /// Fetches the historical data from every day since inception of the ticker price data.
59    pub async fn fetch_historical(&mut self, ticker: &str) -> Result<Equity, Box<dyn Error>> {
60        let request = self.crumbed_request(&year_url(ticker)).await?;
61        let historical_data = HistoricalData::new(&request);
62        Ok(Equity::new(ticker.to_owned(), historical_data))
63    }
64    /// Uses current data and formats it into HistoricalData
65    // pub async fn from_historical(&mut self, data: &str) -> Result<Equity, Box<dyn Error>> {
66    //     let data = &serde_json::from_str(data)?;
67    //     let historical_data = HistoricalData::new(data);
68    //     Ok(Equity {
69    //         historical_data, ticker: ticker.to_owned()
70    //     })
71    // }
72    /// Fetches a summary of what the company behind the ticker does.
73    pub async fn fetch_quote_summary(
74        &mut self,
75        symbol: &str,
76    ) -> Result<ApiResponse, Box<dyn Error>> {
77        self.ensure_crumb_valid().await?;
78
79        let crumb = self.crumb.as_ref().ok_or("Crumb not found")?;
80        let url = format!(
81            "https://query1.finance.yahoo.com/v10/finance/quoteSummary/{}?modules=assetProfile%2CfinancialData&crumb={}",
82            symbol, crumb
83        );
84
85        let response = self.crumbed_request(&url).await?;
86        print!("{:#?}", response);
87        Ok(serde_json::from_value::<ApiResponse>(response)?)
88    }
89
90    async fn crumbed_request(&mut self, url: &str) -> Result<Value, Box<dyn Error>> {
91        self.ensure_crumb_valid().await?;
92        let crumb = self.crumb.as_ref().ok_or("Crumb not found")?;
93        let full_url = format!("{}&crumb={}", url, crumb);
94
95        let response = self
96            .client
97            .get(&full_url)
98            .header("Accept", "application/json")
99            .send()
100            .await?;
101
102        let text = response.text().await?;
103        Ok(serde_json::from_str(&text)?)
104    }
105}