Skip to main content

finance_query/models/market/industries/
response.rs

1use crate::models::quote::FormattedValue;
2use serde::{Deserialize, Serialize};
3
4// ============================================================================
5// Raw response structs (private) - for parsing Yahoo's nested structure
6// ============================================================================
7
8#[derive(Debug, Clone, Deserialize)]
9struct RawIndustryResponse {
10    data: RawIndustryData,
11}
12
13#[derive(Debug, Clone, Deserialize)]
14#[serde(rename_all = "camelCase")]
15struct RawIndustryData {
16    name: String,
17    symbol: Option<String>,
18    key: String,
19    sector_name: Option<String>,
20    sector_key: Option<String>,
21    overview: Option<RawOverview>,
22    performance: Option<RawPerformance>,
23    #[serde(default)]
24    performance_overview_benchmark: Option<RawBenchmarkPerformance>,
25    #[serde(default)]
26    top_companies: Vec<RawCompany>,
27    #[serde(default)]
28    top_performing_companies: Vec<RawPerformingCompany>,
29    #[serde(default)]
30    top_growth_companies: Vec<RawGrowthCompany>,
31    #[serde(default)]
32    research_reports: Vec<RawResearchReport>,
33}
34
35#[derive(Debug, Clone, Deserialize)]
36#[serde(rename_all = "camelCase")]
37struct RawOverview {
38    companies_count: Option<i64>,
39    market_cap: Option<FormattedValue<f64>>,
40    description: Option<String>,
41    market_weight: Option<FormattedValue<f64>>,
42    employee_count: Option<FormattedValue<i64>>,
43}
44
45#[derive(Debug, Clone, Deserialize)]
46#[serde(rename_all = "camelCase")]
47struct RawPerformance {
48    ytd_change_percent: Option<FormattedValue<f64>>,
49    reg_market_change_percent: Option<FormattedValue<f64>>,
50    three_year_change_percent: Option<FormattedValue<f64>>,
51    one_year_change_percent: Option<FormattedValue<f64>>,
52    five_year_change_percent: Option<FormattedValue<f64>>,
53}
54
55#[derive(Debug, Clone, Deserialize)]
56#[serde(rename_all = "camelCase")]
57struct RawBenchmarkPerformance {
58    name: Option<String>,
59    ytd_change_percent: Option<FormattedValue<f64>>,
60    reg_market_change_percent: Option<FormattedValue<f64>>,
61    three_year_change_percent: Option<FormattedValue<f64>>,
62    one_year_change_percent: Option<FormattedValue<f64>>,
63    five_year_change_percent: Option<FormattedValue<f64>>,
64}
65
66#[derive(Debug, Clone, Deserialize)]
67#[serde(rename_all = "camelCase")]
68struct RawCompany {
69    symbol: String,
70    name: Option<String>,
71    last_price: Option<FormattedValue<f64>>,
72    market_cap: Option<FormattedValue<f64>>,
73    market_weight: Option<FormattedValue<f64>>,
74    #[serde(rename = "regMarketChangePercent")]
75    day_change_percent: Option<FormattedValue<f64>>,
76    ytd_return: Option<FormattedValue<f64>>,
77    rating: Option<String>,
78    target_price: Option<FormattedValue<f64>>,
79}
80
81#[derive(Debug, Clone, Deserialize)]
82#[serde(rename_all = "camelCase")]
83struct RawPerformingCompany {
84    symbol: String,
85    name: Option<String>,
86    last_price: Option<FormattedValue<f64>>,
87    ytd_return: Option<FormattedValue<f64>>,
88    target_price: Option<FormattedValue<f64>>,
89}
90
91#[derive(Debug, Clone, Deserialize)]
92#[serde(rename_all = "camelCase")]
93struct RawGrowthCompany {
94    symbol: String,
95    name: Option<String>,
96    last_price: Option<FormattedValue<f64>>,
97    ytd_return: Option<FormattedValue<f64>>,
98    growth_estimate: Option<FormattedValue<f64>>,
99}
100
101#[derive(Debug, Clone, Deserialize)]
102#[serde(rename_all = "camelCase")]
103struct RawResearchReport {
104    id: Option<String>,
105    #[serde(rename = "reportTitle")]
106    title: Option<String>,
107    provider: Option<String>,
108    report_date: Option<String>,
109    report_type: Option<String>,
110    investment_rating: Option<String>,
111    target_price: Option<f64>,
112    target_price_status: Option<String>,
113}
114
115// ============================================================================
116// Public response structs - clean API surface
117// ============================================================================
118
119/// Industry data from Yahoo Finance
120#[derive(Debug, Clone, Serialize, Deserialize)]
121#[serde(rename_all = "camelCase")]
122#[non_exhaustive]
123pub struct IndustryData {
124    /// Industry name
125    pub name: String,
126    /// Industry key (URL slug)
127    pub key: String,
128    /// Industry index symbol (e.g., "^YH31130020")
129    #[serde(skip_serializing_if = "Option::is_none")]
130    pub symbol: Option<String>,
131    /// Parent sector name
132    #[serde(skip_serializing_if = "Option::is_none")]
133    pub sector_name: Option<String>,
134    /// Parent sector key
135    #[serde(skip_serializing_if = "Option::is_none")]
136    pub sector_key: Option<String>,
137    /// Industry overview statistics
138    #[serde(skip_serializing_if = "Option::is_none")]
139    pub overview: Option<IndustryOverview>,
140    /// Industry performance metrics
141    #[serde(skip_serializing_if = "Option::is_none")]
142    pub performance: Option<IndustryPerformance>,
143    /// Benchmark performance (e.g., S&P 500)
144    #[serde(skip_serializing_if = "Option::is_none")]
145    pub benchmark: Option<BenchmarkPerformance>,
146    /// Top companies by market cap
147    #[serde(default)]
148    pub top_companies: Vec<IndustryCompany>,
149    /// Top performing companies by YTD return
150    #[serde(default)]
151    pub top_performing_companies: Vec<PerformingCompany>,
152    /// Top growth companies by growth estimate
153    #[serde(default)]
154    pub top_growth_companies: Vec<GrowthCompany>,
155    /// Research reports
156    #[serde(default)]
157    pub research_reports: Vec<ResearchReport>,
158}
159
160/// Industry overview statistics
161#[derive(Debug, Clone, Serialize, Deserialize)]
162#[cfg_attr(feature = "dataframe", derive(crate::ToDataFrame))]
163#[serde(rename_all = "camelCase")]
164#[non_exhaustive]
165pub struct IndustryOverview {
166    /// Description of the industry
167    #[serde(skip_serializing_if = "Option::is_none")]
168    pub description: Option<String>,
169    /// Number of companies in the industry
170    #[serde(skip_serializing_if = "Option::is_none")]
171    pub companies_count: Option<i64>,
172    /// Total market cap
173    #[serde(skip_serializing_if = "Option::is_none")]
174    pub market_cap: Option<f64>,
175    /// Industry weight within the sector (0-1)
176    #[serde(skip_serializing_if = "Option::is_none")]
177    pub market_weight: Option<f64>,
178    /// Total employee count
179    #[serde(skip_serializing_if = "Option::is_none")]
180    pub employee_count: Option<i64>,
181}
182
183/// Industry performance metrics
184#[derive(Debug, Clone, Serialize, Deserialize)]
185#[cfg_attr(feature = "dataframe", derive(crate::ToDataFrame))]
186#[serde(rename_all = "camelCase")]
187#[non_exhaustive]
188pub struct IndustryPerformance {
189    /// Daily change percent
190    #[serde(skip_serializing_if = "Option::is_none")]
191    pub day_change_percent: Option<f64>,
192    /// Year-to-date change percent
193    #[serde(skip_serializing_if = "Option::is_none")]
194    pub ytd_change_percent: Option<f64>,
195    /// 1-year change percent
196    #[serde(skip_serializing_if = "Option::is_none")]
197    pub one_year_change_percent: Option<f64>,
198    /// 3-year change percent
199    #[serde(skip_serializing_if = "Option::is_none")]
200    pub three_year_change_percent: Option<f64>,
201    /// 5-year change percent
202    #[serde(skip_serializing_if = "Option::is_none")]
203    pub five_year_change_percent: Option<f64>,
204}
205
206/// Benchmark performance for comparison
207#[derive(Debug, Clone, Serialize, Deserialize)]
208#[cfg_attr(feature = "dataframe", derive(crate::ToDataFrame))]
209#[serde(rename_all = "camelCase")]
210#[non_exhaustive]
211pub struct BenchmarkPerformance {
212    /// Benchmark name (e.g., "S&P 500")
213    #[serde(skip_serializing_if = "Option::is_none")]
214    pub name: Option<String>,
215    /// Daily change percent
216    #[serde(skip_serializing_if = "Option::is_none")]
217    pub day_change_percent: Option<f64>,
218    /// Year-to-date change percent
219    #[serde(skip_serializing_if = "Option::is_none")]
220    pub ytd_change_percent: Option<f64>,
221    /// 1-year change percent
222    #[serde(skip_serializing_if = "Option::is_none")]
223    pub one_year_change_percent: Option<f64>,
224    /// 3-year change percent
225    #[serde(skip_serializing_if = "Option::is_none")]
226    pub three_year_change_percent: Option<f64>,
227    /// 5-year change percent
228    #[serde(skip_serializing_if = "Option::is_none")]
229    pub five_year_change_percent: Option<f64>,
230}
231
232/// Company within an industry
233#[derive(Debug, Clone, Serialize, Deserialize)]
234#[cfg_attr(feature = "dataframe", derive(crate::ToDataFrame))]
235#[serde(rename_all = "camelCase")]
236#[non_exhaustive]
237pub struct IndustryCompany {
238    /// Stock ticker symbol
239    pub symbol: String,
240    /// Company name
241    #[serde(skip_serializing_if = "Option::is_none")]
242    pub name: Option<String>,
243    /// Last traded price
244    #[serde(skip_serializing_if = "Option::is_none")]
245    pub last_price: Option<f64>,
246    /// Market capitalization
247    #[serde(skip_serializing_if = "Option::is_none")]
248    pub market_cap: Option<f64>,
249    /// Weight within the industry (0-1)
250    #[serde(skip_serializing_if = "Option::is_none")]
251    pub market_weight: Option<f64>,
252    /// Daily change percent
253    #[serde(skip_serializing_if = "Option::is_none")]
254    pub day_change_percent: Option<f64>,
255    /// Year-to-date return
256    #[serde(skip_serializing_if = "Option::is_none")]
257    pub ytd_return: Option<f64>,
258    /// Analyst rating
259    #[serde(skip_serializing_if = "Option::is_none")]
260    pub rating: Option<String>,
261    /// Analyst target price
262    #[serde(skip_serializing_if = "Option::is_none")]
263    pub target_price: Option<f64>,
264}
265
266/// Top performing company by YTD return
267#[derive(Debug, Clone, Serialize, Deserialize)]
268#[cfg_attr(feature = "dataframe", derive(crate::ToDataFrame))]
269#[serde(rename_all = "camelCase")]
270#[non_exhaustive]
271pub struct PerformingCompany {
272    /// Stock ticker symbol
273    pub symbol: String,
274    /// Company name
275    #[serde(skip_serializing_if = "Option::is_none")]
276    pub name: Option<String>,
277    /// Last traded price
278    #[serde(skip_serializing_if = "Option::is_none")]
279    pub last_price: Option<f64>,
280    /// Year-to-date return
281    #[serde(skip_serializing_if = "Option::is_none")]
282    pub ytd_return: Option<f64>,
283    /// Analyst target price
284    #[serde(skip_serializing_if = "Option::is_none")]
285    pub target_price: Option<f64>,
286}
287
288/// Top growth company by growth estimate
289#[derive(Debug, Clone, Serialize, Deserialize)]
290#[cfg_attr(feature = "dataframe", derive(crate::ToDataFrame))]
291#[serde(rename_all = "camelCase")]
292#[non_exhaustive]
293pub struct GrowthCompany {
294    /// Stock ticker symbol
295    pub symbol: String,
296    /// Company name
297    #[serde(skip_serializing_if = "Option::is_none")]
298    pub name: Option<String>,
299    /// Last traded price
300    #[serde(skip_serializing_if = "Option::is_none")]
301    pub last_price: Option<f64>,
302    /// Year-to-date return
303    #[serde(skip_serializing_if = "Option::is_none")]
304    pub ytd_return: Option<f64>,
305    /// Growth estimate (as decimal, e.g., 3.0 = 300%)
306    #[serde(skip_serializing_if = "Option::is_none")]
307    pub growth_estimate: Option<f64>,
308}
309
310/// Research report
311#[derive(Debug, Clone, Serialize, Deserialize)]
312#[cfg_attr(feature = "dataframe", derive(crate::ToDataFrame))]
313#[serde(rename_all = "camelCase")]
314#[non_exhaustive]
315pub struct ResearchReport {
316    /// Report ID
317    #[serde(skip_serializing_if = "Option::is_none")]
318    pub id: Option<String>,
319    /// Report title
320    #[serde(skip_serializing_if = "Option::is_none")]
321    pub title: Option<String>,
322    /// Report provider (e.g., "Argus Research")
323    #[serde(skip_serializing_if = "Option::is_none")]
324    pub provider: Option<String>,
325    /// Report date (ISO 8601)
326    #[serde(skip_serializing_if = "Option::is_none")]
327    pub report_date: Option<String>,
328    /// Report type (e.g., "Quantitative Report")
329    #[serde(skip_serializing_if = "Option::is_none")]
330    pub report_type: Option<String>,
331    /// Investment rating (e.g., "Bullish", "Bearish", "Neutral")
332    #[serde(skip_serializing_if = "Option::is_none")]
333    pub investment_rating: Option<String>,
334    /// Target price
335    #[serde(skip_serializing_if = "Option::is_none")]
336    pub target_price: Option<f64>,
337    /// Target price status (e.g., "Increased", "Maintained")
338    #[serde(skip_serializing_if = "Option::is_none")]
339    pub target_price_status: Option<String>,
340}
341
342// ============================================================================
343// Conversion implementations
344// ============================================================================
345
346impl IndustryData {
347    /// Parse from Yahoo Finance JSON response
348    pub(crate) fn from_response(json: serde_json::Value) -> Result<Self, String> {
349        let raw: RawIndustryResponse = serde_json::from_value(json).map_err(|e| e.to_string())?;
350
351        let data = raw.data;
352
353        Ok(IndustryData {
354            name: data.name,
355            key: data.key,
356            symbol: data.symbol,
357            sector_name: data.sector_name,
358            sector_key: data.sector_key,
359            overview: data.overview.map(|o| IndustryOverview {
360                description: o.description,
361                companies_count: o.companies_count,
362                market_cap: o.market_cap.and_then(|v| v.raw),
363                market_weight: o.market_weight.and_then(|v| v.raw),
364                employee_count: o.employee_count.and_then(|v| v.raw),
365            }),
366            performance: data.performance.map(|p| IndustryPerformance {
367                day_change_percent: p.reg_market_change_percent.and_then(|v| v.raw),
368                ytd_change_percent: p.ytd_change_percent.and_then(|v| v.raw),
369                one_year_change_percent: p.one_year_change_percent.and_then(|v| v.raw),
370                three_year_change_percent: p.three_year_change_percent.and_then(|v| v.raw),
371                five_year_change_percent: p.five_year_change_percent.and_then(|v| v.raw),
372            }),
373            benchmark: data
374                .performance_overview_benchmark
375                .map(|b| BenchmarkPerformance {
376                    name: b.name,
377                    day_change_percent: b.reg_market_change_percent.and_then(|v| v.raw),
378                    ytd_change_percent: b.ytd_change_percent.and_then(|v| v.raw),
379                    one_year_change_percent: b.one_year_change_percent.and_then(|v| v.raw),
380                    three_year_change_percent: b.three_year_change_percent.and_then(|v| v.raw),
381                    five_year_change_percent: b.five_year_change_percent.and_then(|v| v.raw),
382                }),
383            top_companies: data
384                .top_companies
385                .into_iter()
386                .map(|c| IndustryCompany {
387                    symbol: c.symbol,
388                    name: c.name,
389                    last_price: c.last_price.and_then(|v| v.raw),
390                    market_cap: c.market_cap.and_then(|v| v.raw),
391                    market_weight: c.market_weight.and_then(|v| v.raw),
392                    day_change_percent: c.day_change_percent.and_then(|v| v.raw),
393                    ytd_return: c.ytd_return.and_then(|v| v.raw),
394                    rating: c.rating,
395                    target_price: c.target_price.and_then(|v| v.raw),
396                })
397                .collect(),
398            top_performing_companies: data
399                .top_performing_companies
400                .into_iter()
401                .map(|c| PerformingCompany {
402                    symbol: c.symbol,
403                    name: c.name,
404                    last_price: c.last_price.and_then(|v| v.raw),
405                    ytd_return: c.ytd_return.and_then(|v| v.raw),
406                    target_price: c.target_price.and_then(|v| v.raw),
407                })
408                .collect(),
409            top_growth_companies: data
410                .top_growth_companies
411                .into_iter()
412                .map(|c| GrowthCompany {
413                    symbol: c.symbol,
414                    name: c.name,
415                    last_price: c.last_price.and_then(|v| v.raw),
416                    ytd_return: c.ytd_return.and_then(|v| v.raw),
417                    growth_estimate: c.growth_estimate.and_then(|v| v.raw),
418                })
419                .collect(),
420            research_reports: data
421                .research_reports
422                .into_iter()
423                .map(|r| ResearchReport {
424                    id: r.id,
425                    title: r.title,
426                    provider: r.provider,
427                    report_date: r.report_date,
428                    report_type: r.report_type,
429                    investment_rating: r.investment_rating,
430                    target_price: r.target_price,
431                    target_price_status: r.target_price_status,
432                })
433                .collect(),
434        })
435    }
436}