use crate::client::AkShareClient;
use crate::error::{Error, Result};
use crate::types::IndustryInfo;
use crate::types::value_ext::ValueExt;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct UsStockProfile {
pub symbol: String,
#[serde(default)]
pub sector: Option<String>,
#[serde(default)]
pub industry: Option<String>,
#[serde(default)]
pub full_time_employees: Option<i64>,
#[serde(default)]
pub long_business_summary: Option<String>,
#[serde(default)]
pub website: Option<String>,
#[serde(default)]
pub city: Option<String>,
#[serde(default)]
pub state: Option<String>,
#[serde(default)]
pub country: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct UsKeyStats {
pub symbol: String,
#[serde(default)]
pub shares_outstanding: Option<f64>,
#[serde(default)]
pub float_shares: Option<f64>,
#[serde(default)]
pub market_cap: Option<f64>,
#[serde(default)]
pub trailing_pe: Option<f64>,
#[serde(default)]
pub forward_pe: Option<f64>,
#[serde(default)]
pub price_to_book: Option<f64>,
#[serde(default)]
pub enterprise_value: Option<f64>,
#[serde(default)]
pub trailing_eps: Option<f64>,
#[serde(default)]
pub forward_eps: Option<f64>,
#[serde(default)]
pub book_value: Option<f64>,
#[serde(default)]
pub revenue: Option<f64>,
#[serde(default)]
pub net_income: Option<f64>,
#[serde(default)]
pub gross_margin: Option<f64>,
#[serde(default)]
pub operating_margin: Option<f64>,
#[serde(default)]
pub profit_margin: Option<f64>,
#[serde(default)]
pub roe: Option<f64>,
#[serde(default)]
pub roa: Option<f64>,
#[serde(default)]
pub debt_to_equity: Option<f64>,
#[serde(default)]
pub current_ratio: Option<f64>,
#[serde(default)]
pub beta: Option<f64>,
#[serde(default)]
pub week52_high: Option<f64>,
#[serde(default)]
pub week52_low: Option<f64>,
#[serde(default)]
pub dividend_yield: Option<f64>,
#[serde(default)]
pub payout_ratio: Option<f64>,
}
impl AkShareClient {
pub async fn us_stock_profile(&self, symbol: &str) -> Result<UsStockProfile> {
let url = format!(
"https://query1.finance.yahoo.com/v10/finance/quoteSummary/{}",
symbol.to_uppercase()
);
let response = crate::util::send_and_check(
self.get(&url).query(&[("modules", "assetProfile")]).header(
"User-Agent",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
),
)
.await?;
let payload: serde_json::Value = response.json().await.map_err(Error::from)?;
let profile = payload
.get("quoteSummary")
.and_then(|qs| qs.get("result"))
.and_then(|r| r.as_array())
.and_then(|arr| arr.first())
.and_then(|item| item.get("assetProfile"))
.ok_or_else(|| Error::upstream("Yahoo Finance assetProfile missing"))?;
Ok(UsStockProfile {
symbol: symbol.to_uppercase(),
sector: profile
.get("sector")
.and_then(|v| v.as_str())
.map(std::string::ToString::to_string),
industry: profile
.get("industry")
.and_then(|v| v.as_str())
.map(std::string::ToString::to_string),
full_time_employees: profile
.get("fullTimeEmployees")
.and_then(serde_json::Value::as_i64),
long_business_summary: profile
.get("longBusinessSummary")
.and_then(|v| v.as_str())
.map(std::string::ToString::to_string),
website: profile
.get("website")
.and_then(|v| v.as_str())
.map(std::string::ToString::to_string),
city: profile
.get("city")
.and_then(|v| v.as_str())
.map(std::string::ToString::to_string),
state: profile
.get("state")
.and_then(|v| v.as_str())
.map(std::string::ToString::to_string),
country: profile
.get("country")
.and_then(|v| v.as_str())
.map(std::string::ToString::to_string),
})
}
pub async fn us_stock_industry(&self, symbol: &str) -> IndustryInfo {
if let Ok(profile) = self.us_stock_profile(symbol).await
&& (profile.sector.is_some() || profile.industry.is_some())
{
return IndustryInfo {
sector: profile.sector,
industry: profile.industry,
};
}
let sym = symbol.to_uppercase();
if let Some((sector, industry)) = static_us_sector(&sym) {
return IndustryInfo {
sector: Some(sector.to_string()),
industry: Some(industry.to_string()),
};
}
IndustryInfo {
sector: None,
industry: None,
}
}
pub async fn us_stock_key_stats(&self, symbol: &str) -> Result<UsKeyStats> {
let url = format!(
"https://query1.finance.yahoo.com/v10/finance/quoteSummary/{}",
symbol.to_uppercase()
);
let response = crate::util::send_and_check(
self.get(&url)
.query(&[(
"modules",
"defaultKeyStatistics,summaryDetail,financialData",
)])
.header(
"User-Agent",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
),
)
.await?;
let payload: serde_json::Value = response.json().await.map_err(Error::from)?;
let result = payload
.get("quoteSummary")
.and_then(|qs| qs.get("result"))
.and_then(|r| r.as_array())
.and_then(|arr| arr.first())
.ok_or_else(|| Error::upstream("Yahoo Finance quoteSummary missing"))?;
let stats = result
.get("defaultKeyStatistics")
.unwrap_or(&serde_json::Value::Null);
let detail = result
.get("summaryDetail")
.unwrap_or(&serde_json::Value::Null);
let financial = result
.get("financialData")
.unwrap_or(&serde_json::Value::Null);
let raw = |v: &serde_json::Value| -> Option<f64> { v.f64_field(&["raw"]) };
Ok(UsKeyStats {
symbol: symbol.to_uppercase(),
shares_outstanding: raw(stats
.get("sharesOutstanding")
.unwrap_or(&serde_json::Value::Null)),
float_shares: raw(stats.get("floatShares").unwrap_or(&serde_json::Value::Null)),
market_cap: raw(detail.get("marketCap").unwrap_or(&serde_json::Value::Null)),
trailing_pe: raw(detail.get("trailingPE").unwrap_or(&serde_json::Value::Null)),
forward_pe: raw(detail.get("forwardPE").unwrap_or(&serde_json::Value::Null)),
price_to_book: raw(stats.get("priceToBook").unwrap_or(&serde_json::Value::Null)),
enterprise_value: raw(stats
.get("enterpriseValue")
.unwrap_or(&serde_json::Value::Null)),
trailing_eps: raw(stats.get("trailingEps").unwrap_or(&serde_json::Value::Null)),
forward_eps: raw(stats.get("forwardEps").unwrap_or(&serde_json::Value::Null)),
book_value: raw(stats.get("bookValue").unwrap_or(&serde_json::Value::Null)),
revenue: raw(financial
.get("totalRevenue")
.unwrap_or(&serde_json::Value::Null)),
net_income: raw(financial
.get("netIncomeToCommon")
.unwrap_or(&serde_json::Value::Null)),
gross_margin: raw(financial
.get("grossMargins")
.unwrap_or(&serde_json::Value::Null)),
operating_margin: raw(financial
.get("operatingMargins")
.unwrap_or(&serde_json::Value::Null)),
profit_margin: raw(financial
.get("profitMargins")
.unwrap_or(&serde_json::Value::Null)),
roe: raw(financial
.get("returnOnEquity")
.unwrap_or(&serde_json::Value::Null)),
roa: raw(financial
.get("returnOnAssets")
.unwrap_or(&serde_json::Value::Null)),
debt_to_equity: raw(financial
.get("debtToEquity")
.unwrap_or(&serde_json::Value::Null)),
current_ratio: raw(financial
.get("currentRatio")
.unwrap_or(&serde_json::Value::Null)),
beta: raw(stats.get("beta").unwrap_or(&serde_json::Value::Null)),
week52_high: raw(detail
.get("fiftyTwoWeekHigh")
.unwrap_or(&serde_json::Value::Null)),
week52_low: raw(detail
.get("fiftyTwoWeekLow")
.unwrap_or(&serde_json::Value::Null)),
dividend_yield: raw(detail
.get("dividendYield")
.unwrap_or(&serde_json::Value::Null)),
payout_ratio: raw(detail
.get("payoutRatio")
.unwrap_or(&serde_json::Value::Null)),
})
}
}
fn static_us_sector(symbol: &str) -> Option<(&str, &str)> {
let upper = symbol.to_uppercase();
match upper.as_str() {
"AAPL" => Some(("Technology", "Consumer Electronics")),
"MSFT" | "ORCL" | "SNPS" | "PANW" | "CRWD" | "ZS" | "NET" | "SQ" | "TWLO" => {
Some(("Technology", "Software—Infrastructure"))
}
"CRM" | "ADBE" | "NOW" | "INTU" | "CDNS" | "DDOG" | "SNOW" | "PLTR" | "SHOP" | "UBER"
| "ABNB" | "GRAB" => Some(("Technology", "Software—Application")),
"NVDA" | "AMD" | "INTC" | "TSM" | "AVGO" | "QCOM" | "TXN" | "MU" | "MRVL" => {
Some(("Technology", "Semiconductors"))
}
"AMAT" | "LRCX" | "KLAC" => Some(("Technology", "Semiconductor Equipment & Materials")),
"CSCO" => Some(("Technology", "Communication Equipment")),
"IBM" => Some(("Technology", "Information Technology Services")),
"GOOG" | "GOOGL" | "META" | "SPOT" | "PINS" | "SNAP" | "BIDU" | "BILI" | "ZH" => {
Some(("Communication Services", "Internet Content & Information"))
}
"NFLX" | "DIS" | "CMCSA" | "CHTR" | "IQ" | "TME" => {
Some(("Communication Services", "Entertainment"))
}
"T" | "VZ" | "TMUS" => Some(("Communication Services", "Telecom Services")),
"EA" | "TTWO" | "ATVI" | "NTES" => {
Some(("Communication Services", "Electronic Gaming & Multimedia"))
}
"ROKU" => Some(("Communication Services", "Consumer Electronics")),
"AMZN" | "BABA" | "JD" | "PDD" | "VIPS" | "MELI" | "SE" | "CPNG" => {
Some(("Consumer Cyclical", "Internet Retail"))
}
"TSLA" | "GM" | "F" | "NIO" | "XPEV" | "LI" => {
Some(("Consumer Cyclical", "Auto Manufacturers"))
}
"HD" | "LOW" => Some(("Consumer Cyclical", "Home Improvement Retail")),
"MCD" | "SBUX" => Some(("Consumer Cyclical", "Restaurants")),
"TJX" | "LULU" => Some(("Consumer Cyclical", "Apparel Retail")),
"NKE" => Some(("Consumer Cyclical", "Footwear & Accessories")),
"BKNG" => Some(("Consumer Cyclical", "Travel Services")),
"DKNG" => Some(("Consumer Cyclical", "Gambling")),
"WMT" | "COST" | "TGT" | "MNSO" => Some(("Consumer Defensive", "Discount Stores")),
"STZ" => Some(("Consumer Defensive", "Beverages—Brewers")),
"KO" | "PEP" => Some(("Consumer Defensive", "Beverages—Non-Alcoholic")),
"PM" | "MO" => Some(("Consumer Defensive", "Tobacco")),
"CL" | "PG" | "EL" => Some(("Consumer Defensive", "Household & Personal Products")),
"JNJ" | "PFE" | "ABBV" | "MRK" | "LLY" | "BMY" | "AMGN" | "GILD" => {
Some(("Healthcare", "Drug Manufacturers—General"))
}
"ZTS" => Some(("Healthcare", "Drug Manufacturers—Specialty & Generic")),
"UNH" => Some(("Healthcare", "Healthcare Plans")),
"TMO" => Some(("Healthcare", "Diagnostics & Research")),
"ABT" | "MDT" | "SYK" | "BSX" => Some(("Healthcare", "Medical Devices")),
"ISRG" => Some(("Healthcare", "Medical Instruments & Supplies")),
"REGN" | "VRTX" | "MRNA" | "BIIB" => Some(("Healthcare", "Biotechnology")),
"HCA" => Some(("Healthcare", "Medical Care Facilities")),
"BRK-B" | "BRK.A" | "BRK_A" | "AIG" => Some(("Financials", "Insurance—Diversified")),
"JPM" | "BAC" | "WFC" | "C" => Some(("Financials", "Banks—Diversified")),
"V" | "MA" | "AXP" => Some(("Financials", "Credit Services")),
"GS" | "MS" | "SCHW" | "FUTU" | "TIGR" => Some(("Financials", "Capital Markets")),
"BLK" => Some(("Financials", "Asset Management")),
"CB" | "PGR" | "TRV" => Some(("Financials", "Insurance—Property & Casualty")),
"MET" | "AFL" => Some(("Financials", "Insurance—Life")),
"CME" | "ICE" | "MCO" | "SPGI" => Some(("Financials", "Financial Data & Stock Exchanges")),
"XOM" | "CVX" => Some(("Energy", "Oil & Gas Integrated")),
"COP" | "EOG" | "OXY" | "DVN" | "FANG" => Some(("Energy", "Oil & Gas E&P")),
"SLB" | "HAL" => Some(("Energy", "Oil & Gas Equipment & Services")),
"MPC" | "PSX" | "VLO" => Some(("Energy", "Oil & Gas Refining & Marketing")),
"CAT" | "DE" => Some(("Industrials", "Farm & Heavy Construction Machinery")),
"BA" | "RTX" | "LMT" | "GD" | "NOC" | "TDG" => Some(("Industrials", "Aerospace & Defense")),
"HON" | "GE" | "MMM" | "ETN" | "EMR" | "ITW" | "ROK" | "PH" => {
Some(("Industrials", "Specialty Industrial Machinery"))
}
"UPS" | "FDX" => Some(("Industrials", "Integrated Freight & Logistics")),
"WM" => Some(("Industrials", "Waste Management")),
"PLD" => Some(("Real Estate", "REIT—Industrial")),
"AMT" | "CCI" => Some(("Real Estate", "REIT—Specialty")),
"EQIX" | "DLR" => Some(("Real Estate", "REIT—Data Center")),
"SPG" | "O" => Some(("Real Estate", "REIT—Retail")),
"WELL" => Some(("Real Estate", "REIT—Healthcare Facilities")),
"NEE" => Some(("Utilities", "Utilities—Renewable")),
"DUK" | "SO" | "D" | "AEP" | "EXC" | "XEL" => {
Some(("Utilities", "Utilities—Regulated Electric"))
}
"SRE" => Some(("Utilities", "Utilities—Diversified")),
"LIN" | "APD" | "SHW" | "ECL" | "DOW" | "DD" | "PPG" => {
Some(("Basic Materials", "Specialty Chemicals"))
}
"FCX" => Some(("Basic Materials", "Copper")),
"NEM" => Some(("Basic Materials", "Gold")),
"NUE" => Some(("Basic Materials", "Steel")),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_static_sector_known() {
assert_eq!(
static_us_sector("AAPL"),
Some(("Technology", "Consumer Electronics"))
);
assert_eq!(
static_us_sector("MSFT"),
Some(("Technology", "Software—Infrastructure"))
);
assert_eq!(
static_us_sector("NVDA"),
Some(("Technology", "Semiconductors"))
);
assert_eq!(
static_us_sector("JPM"),
Some(("Financials", "Banks—Diversified"))
);
assert_eq!(
static_us_sector("XOM"),
Some(("Energy", "Oil & Gas Integrated"))
);
assert_eq!(
static_us_sector("BABA"),
Some(("Consumer Cyclical", "Internet Retail"))
);
}
#[test]
fn test_static_sector_unknown() {
assert_eq!(static_us_sector("UNKNOWN_SYMBOL"), None);
}
#[test]
fn test_static_sector_case_insensitive() {
assert_eq!(
static_us_sector("aapl"),
Some(("Technology", "Consumer Electronics"))
);
assert_eq!(
static_us_sector("Nvda"),
Some(("Technology", "Semiconductors"))
);
}
}