finance_query/models/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 Industry {
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 Industry {
347    /// Parse from Yahoo Finance JSON response
348    pub(crate) fn from_response(json: &serde_json::Value) -> Result<Self, String> {
349        let raw: RawIndustryResponse =
350            serde_json::from_value(json.clone()).map_err(|e| e.to_string())?;
351
352        let data = raw.data;
353
354        Ok(Industry {
355            name: data.name,
356            key: data.key,
357            symbol: data.symbol,
358            sector_name: data.sector_name,
359            sector_key: data.sector_key,
360            overview: data.overview.map(|o| IndustryOverview {
361                description: o.description,
362                companies_count: o.companies_count,
363                market_cap: o.market_cap.and_then(|v| v.raw),
364                market_weight: o.market_weight.and_then(|v| v.raw),
365                employee_count: o.employee_count.and_then(|v| v.raw),
366            }),
367            performance: data.performance.map(|p| IndustryPerformance {
368                day_change_percent: p.reg_market_change_percent.and_then(|v| v.raw),
369                ytd_change_percent: p.ytd_change_percent.and_then(|v| v.raw),
370                one_year_change_percent: p.one_year_change_percent.and_then(|v| v.raw),
371                three_year_change_percent: p.three_year_change_percent.and_then(|v| v.raw),
372                five_year_change_percent: p.five_year_change_percent.and_then(|v| v.raw),
373            }),
374            benchmark: data
375                .performance_overview_benchmark
376                .map(|b| BenchmarkPerformance {
377                    name: b.name,
378                    day_change_percent: b.reg_market_change_percent.and_then(|v| v.raw),
379                    ytd_change_percent: b.ytd_change_percent.and_then(|v| v.raw),
380                    one_year_change_percent: b.one_year_change_percent.and_then(|v| v.raw),
381                    three_year_change_percent: b.three_year_change_percent.and_then(|v| v.raw),
382                    five_year_change_percent: b.five_year_change_percent.and_then(|v| v.raw),
383                }),
384            top_companies: data
385                .top_companies
386                .into_iter()
387                .map(|c| IndustryCompany {
388                    symbol: c.symbol,
389                    name: c.name,
390                    last_price: c.last_price.and_then(|v| v.raw),
391                    market_cap: c.market_cap.and_then(|v| v.raw),
392                    market_weight: c.market_weight.and_then(|v| v.raw),
393                    day_change_percent: c.day_change_percent.and_then(|v| v.raw),
394                    ytd_return: c.ytd_return.and_then(|v| v.raw),
395                    rating: c.rating,
396                    target_price: c.target_price.and_then(|v| v.raw),
397                })
398                .collect(),
399            top_performing_companies: data
400                .top_performing_companies
401                .into_iter()
402                .map(|c| PerformingCompany {
403                    symbol: c.symbol,
404                    name: c.name,
405                    last_price: c.last_price.and_then(|v| v.raw),
406                    ytd_return: c.ytd_return.and_then(|v| v.raw),
407                    target_price: c.target_price.and_then(|v| v.raw),
408                })
409                .collect(),
410            top_growth_companies: data
411                .top_growth_companies
412                .into_iter()
413                .map(|c| GrowthCompany {
414                    symbol: c.symbol,
415                    name: c.name,
416                    last_price: c.last_price.and_then(|v| v.raw),
417                    ytd_return: c.ytd_return.and_then(|v| v.raw),
418                    growth_estimate: c.growth_estimate.and_then(|v| v.raw),
419                })
420                .collect(),
421            research_reports: data
422                .research_reports
423                .into_iter()
424                .map(|r| ResearchReport {
425                    id: r.id,
426                    title: r.title,
427                    provider: r.provider,
428                    report_date: r.report_date,
429                    report_type: r.report_type,
430                    investment_rating: r.investment_rating,
431                    target_price: r.target_price,
432                    target_price_status: r.target_price_status,
433                })
434                .collect(),
435        })
436    }
437}