Skip to content

Commit

Permalink
Merge branch 'xemwebe:master' into fix_get_quote_period_interval
Browse files Browse the repository at this point in the history
  • Loading branch information
7jrxt42BxFZo4iAnN4CX authored Jan 16, 2025
2 parents dd09904 + baf969c commit e2d9d20
Show file tree
Hide file tree
Showing 5 changed files with 253 additions and 1 deletion.
63 changes: 63 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ include = ["src/**/*", "LICENSE-*", "README.md"]
reqwest = { version = "0.12", default-features = false, features = [
"json",
"rustls-tls",
"cookies"
] }
rust_decimal = { version = "1.36", optional = true }
serde_json = "1.0"
Expand Down
56 changes: 56 additions & 0 deletions src/async_impl.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate::quotes::YQuoteSummary;
use search_result::YOptionChain;

use super::*;
Expand Down Expand Up @@ -99,6 +100,52 @@ impl YahooConnector {
Ok(resp)
}

// Get symbol metadata
pub async fn get_ticker_info(symbol: &str) -> Result<YQuoteSummary, YahooError> {
let get_cookie_resp = reqwest::get(Y_GET_COOKIE_URL).await.unwrap();
let cookie = get_cookie_resp
.headers()
.get(Y_COOKIE_REQUEST_HEADER)
.unwrap()
.to_str()
.unwrap();
let jar_for_cookie = std::sync::Arc::new(reqwest::cookie::Jar::default());
jar_for_cookie.add_cookie_str(cookie, &reqwest::Url::parse(Y_GET_CRUMB_URL).unwrap());
let intermediate_client_for_cookie = Client::builder()
.user_agent(USER_AGENT)
.cookie_provider(jar_for_cookie)
.build()
.unwrap();

let crumb = intermediate_client_for_cookie
.get(reqwest::Url::parse(Y_GET_CRUMB_URL).unwrap())
.send()
.await
.unwrap()
.text()
.await
.unwrap();

let jar_for_info = std::sync::Arc::new(reqwest::cookie::Jar::default());
let url_for_information = reqwest::Url::parse(
&(format!(YQUOTE_SUMMARY_QUERY!(), symbol = symbol, crumb = crumb)),
);
jar_for_info.add_cookie_str(cookie, &url_for_information.clone().unwrap());

let client_for_info = reqwest::Client::builder()
.user_agent(USER_AGENT)
.cookie_provider(jar_for_info)
.build()
.unwrap();
let resp = client_for_info
.get(url_for_information.unwrap())
.send()
.await
.unwrap();
let result = resp.json().await.unwrap();
Ok(result)
}

/// Send request to yahoo! finance server and transform response to JSON value
async fn send_request(&self, url: &str) -> Result<serde_json::Value, YahooError> {
let resp = self.client.get(url).send().await?;
Expand Down Expand Up @@ -301,4 +348,13 @@ mod tests {
let capital_gains = response.capital_gains().unwrap();
assert!(capital_gains.len() > 0usize);
}

#[test]
fn test_get_ticker_info() {
let result = tokio_test::block_on(YahooConnector::get_ticker_info("AAPL"));

assert!(result.is_ok());
let quote_summary = result.unwrap().quote_summary;
assert!("Cupertino" == quote_summary.result[0].asset_profile.city); // Testing it retrieved info, hard coded but shouldn't change anytime soon
}
}
13 changes: 12 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,12 @@ pub use yahoo_error::YahooError;

const YCHART_URL: &str = "https://query1.finance.yahoo.com/v8/finance/chart";
const YSEARCH_URL: &str = "https://query2.finance.yahoo.com/v1/finance/search";
const Y_GET_COOKIE_URL: &str = "https://fc.yahoo.com";
const Y_GET_CRUMB_URL: &str = "https://query1.finance.yahoo.com/v1/test/getcrumb";

// special yahoo hardcoded keys and headers
const Y_COOKIE_REQUEST_HEADER: &str = "set-cookie";
const USER_AGENT: &str = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36";

// Macros instead of constants,
macro_rules! YCHART_PERIOD_QUERY {
Expand All @@ -209,6 +215,11 @@ macro_rules! YTICKER_QUERY {
"{url}?q={name}"
};
}
macro_rules! YQUOTE_SUMMARY_QUERY {
() => {
"https://query2.finance.yahoo.com/v10/finance/quoteSummary/{symbol}?modules=financialData,quoteType,defaultKeyStatistics,assetProfile,summaryDetail&corsDomain=finance.yahoo.com&formatted=false&symbol={symbol}&crumb={crumb}"
}
}

/// Container for connection parameters to yahoo! finance server
pub struct YahooConnector {
Expand Down Expand Up @@ -247,7 +258,7 @@ impl Default for YahooConnector {

impl YahooConnectorBuilder {
pub fn build(self) -> Result<YahooConnector, YahooError> {
self.build_with_agent("Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36")
self.build_with_agent(USER_AGENT)
}

pub fn build_with_agent(self, user_agent: &str) -> Result<YahooConnector, YahooError> {
Expand Down
121 changes: 121 additions & 0 deletions src/quotes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,127 @@ pub struct CapitalGain {
pub date: u64,
}

#[derive(Deserialize, Debug)]
pub struct YQuoteSummary {
#[serde(rename = "quoteSummary")]
pub quote_summary: ExtendedQuoteSummary,
}

#[derive(Deserialize, Debug)]
pub struct ExtendedQuoteSummary {
pub result: Vec<YSummaryData>,
pub error: Option<serde_json::Value>,
}

impl YQuoteSummary {
pub fn from_json(json: serde_json::Value) -> Result<YQuoteSummary, YahooError> {
Ok(serde_json::from_value(json)?)
}
}

#[derive(Deserialize, Debug)]
pub struct YSummaryData {
#[serde(rename = "assetProfile")]
pub asset_profile: AssetProfile,
#[serde(rename = "summaryDetail")]
pub summary_detail: SummaryDetail,
#[serde(rename = "defaultKeyStatistics")]
pub default_key_statistics: DefaultKeyStatistics,
#[serde(rename = "quoteType")]
pub quote_type: QuoteType,
#[serde(rename = "financialData")]
pub financial_data: FinancialData,
}

#[derive(Deserialize, Debug)]
pub struct AssetProfile {
pub address1: String,
pub city: String,
pub state: String,
pub zip: String,
pub country: String,
pub phone: String,
pub website: String,
pub industry: String,
pub sector: String,
#[serde(rename = "longBusinessSummary")]
pub long_business_summary: String,
#[serde(rename = "fullTimeEmployees")]
pub full_time_employees: u32,
#[serde(rename = "companyOfficers")]
pub company_officers: Vec<CompanyOfficer>,
}

#[derive(Deserialize, Debug)]
pub struct CompanyOfficer {
pub name: String,
pub age: Option<u32>,
pub title: String,
#[serde(rename = "yearBorn")]
pub year_born: Option<u32>,
#[serde(rename = "fiscalYear")]
pub fiscal_year: u32,
#[serde(rename = "totalPay")]
pub total_pay: Option<ValueWrapper>,
}

#[derive(Deserialize, Debug)]
pub struct ValueWrapper {
pub raw: Option<u64>,
pub fmt: Option<String>,
#[serde(rename = "longFmt")]
pub long_fmt: Option<String>,
}

#[derive(Deserialize, Debug)]
pub struct SummaryDetail {
#[serde(rename = "previousClose")]
pub previous_close: f64,
pub open: f64,
#[serde(rename = "dayLow")]
pub day_low: f64,
#[serde(rename = "dayHigh")]
pub day_high: f64,
#[serde(rename = "regularMarketVolume")]
pub regular_market_volume: u64,
#[serde(rename = "marketCap")]
pub market_cap: u64,
pub currency: String,
}

#[derive(Deserialize, Debug)]
pub struct DefaultKeyStatistics {
#[serde(rename = "enterpriseValue")]
pub enterprise_value: u64,
#[serde(rename = "profitMargins")]
pub profit_margins: f64,
#[serde(rename = "sharesOutstanding")]
pub shares_outstanding: u64,
}

#[derive(Deserialize, Debug)]
pub struct QuoteType {
pub exchange: String,
pub symbol: String,
#[serde(rename = "longName")]
pub long_name: String,
#[serde(rename = "timeZoneFullName")]
pub timezone_full_name: String,
}

#[derive(Deserialize, Debug)]
pub struct FinancialData {
#[serde(rename = "currentPrice")]
pub current_price: f64,
#[serde(rename = "totalCash")]
pub total_cash: u64,
pub ebitda: u64,
#[serde(rename = "totalDebt")]
pub total_debt: u64,
#[serde(rename = "totalRevenue")]
pub total_revenue: u64,
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down

0 comments on commit e2d9d20

Please sign in to comment.