use serde::{Deserialize, Serialize};
use crate::client::Client;
use crate::market_data::{MdFormattedFloat, MdGroupRank, MdPercentChangeVs};
use crate::types::SymbolVariables;
const QUERY_INDUSTRY_GROUP_RS: &str = include_str!("graphql/industry_group_rs.graphql");
const QUERY_INDUSTRY_OVERVIEW: &str = include_str!("graphql/industry_overview.graphql");
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct IndustryGroupRsResponse {
#[serde(default)]
pub market_data: Vec<IndustryGroupRsItem>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct IndustryGroupRsItem {
pub origin_request: Option<IndustryGroupRsOriginRequest>,
pub industry: Option<IndustryGroupRsIndustry>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct IndustryGroupRsOriginRequest {
pub symbol: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct IndustryGroupRsIndustry {
#[serde(default, rename = "groupRS")]
pub group_rs: Vec<IndustryGroupRsValue>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct IndustryGroupRsValue {
pub value: Option<i64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct IndustryOverviewResponse {
#[serde(default)]
pub market_data: Vec<IndustryOverviewItem>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct IndustryOverviewItem {
pub id: Option<String>,
pub industry: Option<IndustryOverviewIndustry>,
pub ratings: Option<IndustryOverviewRatings>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct IndustryOverviewIndustry {
pub name: Option<String>,
pub ind_code: Option<i64>,
pub news_code: Option<String>,
pub sector: Option<String>,
pub group_market_value_in_billions: Option<MdFormattedFloat>,
pub num_new_highs_in_group: Option<i64>,
pub num_new_lows_in_group: Option<i64>,
pub number_of_stocks_in_group: Option<i64>,
#[serde(default)]
pub group_ranks: Vec<MdGroupRank>,
#[serde(default)]
pub price_percent_change_vs: Vec<MdPercentChangeVs>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct IndustryOverviewRatings {
pub has_ratings_data: Option<bool>,
pub industry: Option<IndustryRankInGroup>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct IndustryRankInGroup {
pub ad_rank_in_industry_group: Option<i64>,
pub comp_rank_in_industry_group: Option<i64>,
pub eps_rank_in_industry_group: Option<i64>,
pub number_of_stocks_in_group: Option<i64>,
pub rs_rank_in_industry_group: Option<i64>,
pub smr_rank_in_industry_group: Option<i64>,
}
impl Client {
pub async fn industry_group_rs(
&self,
symbols: &[&str],
symbol_dialect_type: Option<&str>,
) -> crate::error::Result<IndustryGroupRsResponse> {
self.graphql_operation(
"IndustryGroupRS",
SymbolVariables::new(symbols, symbol_dialect_type),
QUERY_INDUSTRY_GROUP_RS,
)
.await
}
pub async fn industry_overview(
&self,
symbols: &[&str],
symbol_dialect_type: Option<&str>,
) -> crate::error::Result<IndustryOverviewResponse> {
self.graphql_operation(
"IndustryOverview",
SymbolVariables::new(symbols, symbol_dialect_type),
QUERY_INDUSTRY_OVERVIEW,
)
.await
}
}
#[cfg(test)]
mod tests {
use crate::test_support::{mock_test, mock_test_with_fixture};
#[tokio::test]
async fn industry_group_rs_parses_response() {
let (_server, client, mock) = mock_test("IndustryGroupRS").await;
let resp = client
.industry_group_rs(&["AAPL"], None)
.await
.expect("industry_group_rs should succeed");
assert_eq!(resp.market_data.len(), 1);
let item = &resp.market_data[0];
let origin = item.origin_request.as_ref().expect("origin_request");
assert_eq!(origin.symbol.as_deref(), Some("AAPL"));
let industry = item.industry.as_ref().expect("industry");
assert_eq!(industry.group_rs.len(), 1);
assert_eq!(industry.group_rs[0].value, Some(85));
mock.assert();
}
#[cfg(not(coverage))]
#[tokio::test]
#[ignore]
async fn integration_industry_group_rs() {
let client = crate::test_support::live_client().await;
let resp = client
.industry_group_rs(&["AAPL"], None)
.await
.expect("live industry_group_rs should succeed");
assert!(!resp.market_data.is_empty());
}
#[tokio::test]
async fn industry_overview_parses_response() {
let (_server, client, mock) = mock_test("IndustryOverview").await;
let resp = client
.industry_overview(&["13-4698"], Some("DJ_KEY"))
.await
.expect("industry_overview should succeed");
assert_eq!(resp.market_data.len(), 1);
let item = &resp.market_data[0];
assert_eq!(item.id.as_deref(), Some("13-4698"));
let industry = item.industry.as_ref().expect("industry");
assert_eq!(industry.name.as_deref(), Some("Elec-Semicondctor Fablss"));
assert_eq!(industry.ind_code, Some(7010));
assert_eq!(industry.news_code.as_deref(), Some("I/SEMF"));
assert_eq!(industry.sector.as_deref(), Some("CHIPS"));
assert_eq!(
industry
.group_market_value_in_billions
.as_ref()
.and_then(|v| v.formatted_value.as_deref()),
Some("9,056.41B")
);
assert_eq!(industry.num_new_highs_in_group, Some(1));
assert_eq!(industry.num_new_lows_in_group, Some(0));
assert_eq!(industry.number_of_stocks_in_group, Some(45));
assert_eq!(industry.group_ranks.len(), 1);
assert_eq!(industry.group_ranks[0].value, Some(16));
assert_eq!(
industry.group_ranks[0].period_offset.as_deref(),
Some("CURRENT")
);
assert_eq!(industry.price_percent_change_vs.len(), 2);
let today = industry
.price_percent_change_vs
.iter()
.find(|v| v.subject.as_deref() == Some("VS_1D_AGO"))
.expect("VS_1D_AGO entry");
assert_eq!(today.formatted_value.as_deref(), Some("-1.22%"));
let ytd = industry
.price_percent_change_vs
.iter()
.find(|v| v.subject.as_deref() == Some("VS_YTD"))
.expect("VS_YTD entry");
assert_eq!(ytd.formatted_value.as_deref(), Some("27.07%"));
let ratings = item.ratings.as_ref().expect("ratings");
assert_eq!(ratings.has_ratings_data, Some(true));
let rank = ratings.industry.as_ref().expect("industry rank");
assert_eq!(rank.eps_rank_in_industry_group, Some(4));
assert_eq!(rank.rs_rank_in_industry_group, Some(2));
assert_eq!(rank.ad_rank_in_industry_group, Some(10));
assert_eq!(rank.smr_rank_in_industry_group, Some(8));
assert_eq!(rank.comp_rank_in_industry_group, Some(1));
assert_eq!(rank.number_of_stocks_in_group, Some(45));
mock.assert();
}
#[tokio::test]
async fn top_industries_in_sector_parses_response() {
let (_server, client, mock) =
mock_test_with_fixture("TopIndustriesInSector", "MarketDataScreen").await;
let params = vec![crate::screen::ScreenerParameter {
name: "SectorName".to_string(),
value: "CHIPS".to_string(),
}];
let resp = client
.market_data_screen(
"MarketSurge.RelatedInformation.StockTopIndustriesInSector",
params,
)
.await
.expect("market_data_screen should succeed");
let result = resp
.market_data_screen
.as_ref()
.expect("market_data_screen result");
assert_eq!(
result.screen_name.as_deref(),
Some("MarketSurge.RelatedInformation.StockTopIndustriesInSector")
);
assert_eq!(result.response_values.len(), 3);
let row = &result.response_values[0];
assert_eq!(row.len(), 2);
assert_eq!(row[0].value.as_deref(), Some("Elec-Semicondctor Fablss"));
assert_eq!(row[1].value.as_deref(), Some("A+"));
mock.assert();
}
#[cfg(not(coverage))]
#[tokio::test]
#[ignore]
async fn integration_industry_overview() {
let client = crate::test_support::live_client().await;
let resp = client
.industry_overview(&["AAPL"], None)
.await
.expect("live industry_overview should succeed");
assert!(!resp.market_data.is_empty());
}
}