finance_query/models/sectors/
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 RawSectorResponse {
10    data: RawSectorData,
11}
12
13#[derive(Debug, Clone, Deserialize)]
14#[serde(rename_all = "camelCase")]
15struct RawSectorData {
16    name: String,
17    symbol: Option<String>,
18    key: String,
19    overview: Option<RawOverview>,
20    performance: Option<RawPerformance>,
21    #[serde(default)]
22    performance_overview_benchmark: Option<RawBenchmarkPerformance>,
23    #[serde(default)]
24    top_companies: Vec<RawCompany>,
25    #[serde(default, rename = "topETFs")]
26    top_etfs: Vec<RawETF>,
27    #[serde(default)]
28    top_mutual_funds: Vec<RawMutualFund>,
29    #[serde(default)]
30    industries: Vec<RawIndustry>,
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    industries_count: Option<i64>,
42    market_weight: Option<FormattedValue<f64>>,
43    employee_count: Option<FormattedValue<i64>>,
44}
45
46#[derive(Debug, Clone, Deserialize)]
47#[serde(rename_all = "camelCase")]
48struct RawPerformance {
49    ytd_change_percent: Option<FormattedValue<f64>>,
50    reg_market_change_percent: Option<FormattedValue<f64>>,
51    three_year_change_percent: Option<FormattedValue<f64>>,
52    one_year_change_percent: Option<FormattedValue<f64>>,
53    five_year_change_percent: Option<FormattedValue<f64>>,
54}
55
56#[derive(Debug, Clone, Deserialize)]
57#[serde(rename_all = "camelCase")]
58struct RawBenchmarkPerformance {
59    name: Option<String>,
60    ytd_change_percent: Option<FormattedValue<f64>>,
61    reg_market_change_percent: Option<FormattedValue<f64>>,
62    three_year_change_percent: Option<FormattedValue<f64>>,
63    one_year_change_percent: Option<FormattedValue<f64>>,
64    five_year_change_percent: Option<FormattedValue<f64>>,
65}
66
67#[derive(Debug, Clone, Deserialize)]
68#[serde(rename_all = "camelCase")]
69struct RawCompany {
70    symbol: String,
71    name: Option<String>,
72    market_cap: Option<FormattedValue<f64>>,
73    market_weight: Option<FormattedValue<f64>>,
74    last_price: Option<FormattedValue<f64>>,
75    target_price: Option<FormattedValue<f64>>,
76    reg_market_change_percent: Option<FormattedValue<f64>>,
77    ytd_return: Option<FormattedValue<f64>>,
78    rating: Option<String>,
79}
80
81#[derive(Debug, Clone, Deserialize)]
82#[serde(rename_all = "camelCase")]
83struct RawETF {
84    symbol: String,
85    name: Option<String>,
86    net_assets: Option<FormattedValue<f64>>,
87    expense_ratio: Option<FormattedValue<f64>>,
88    last_price: Option<FormattedValue<f64>>,
89    ytd_return: Option<FormattedValue<f64>>,
90}
91
92#[derive(Debug, Clone, Deserialize)]
93#[serde(rename_all = "camelCase")]
94struct RawMutualFund {
95    symbol: String,
96    name: Option<String>,
97    net_assets: Option<FormattedValue<f64>>,
98    expense_ratio: Option<FormattedValue<f64>>,
99    last_price: Option<FormattedValue<f64>>,
100    ytd_return: Option<FormattedValue<f64>>,
101}
102
103#[derive(Debug, Clone, Deserialize)]
104#[serde(rename_all = "camelCase")]
105struct RawIndustry {
106    symbol: Option<String>,
107    key: Option<String>,
108    name: String,
109    market_weight: Option<FormattedValue<f64>>,
110    reg_market_change_percent: Option<FormattedValue<f64>>,
111    ytd_return: Option<FormattedValue<f64>>,
112}
113
114#[derive(Debug, Clone, Deserialize)]
115#[serde(rename_all = "camelCase")]
116struct RawResearchReport {
117    id: String,
118    head_html: Option<String>,
119    provider: Option<String>,
120    report_date: Option<String>,
121    report_title: Option<String>,
122    report_type: Option<String>,
123    target_price: Option<f64>,
124    target_price_status: Option<String>,
125    investment_rating: Option<String>,
126}
127
128// ============================================================================
129// Public response structs - clean, user-friendly types
130// ============================================================================
131
132/// Complete sector data with all available information
133#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
134#[serde(rename_all = "camelCase")]
135#[non_exhaustive]
136pub struct Sector {
137    /// Sector name (e.g., "Technology")
138    pub name: String,
139
140    /// Yahoo Finance sector symbol (e.g., "^YH311")
141    #[serde(skip_serializing_if = "Option::is_none")]
142    pub symbol: Option<String>,
143
144    /// Sector key for API calls (e.g., "technology")
145    pub key: String,
146
147    /// Sector overview with market statistics
148    #[serde(skip_serializing_if = "Option::is_none")]
149    pub overview: Option<SectorOverview>,
150
151    /// Sector performance metrics
152    #[serde(skip_serializing_if = "Option::is_none")]
153    pub performance: Option<SectorPerformance>,
154
155    /// Benchmark (S&P 500) comparison performance
156    #[serde(skip_serializing_if = "Option::is_none")]
157    pub benchmark: Option<SectorPerformance>,
158
159    /// Benchmark name (usually "S&P 500")
160    #[serde(skip_serializing_if = "Option::is_none")]
161    pub benchmark_name: Option<String>,
162
163    /// Top companies in the sector
164    #[serde(skip_serializing_if = "Vec::is_empty")]
165    pub top_companies: Vec<SectorCompany>,
166
167    /// Top ETFs tracking this sector
168    #[serde(skip_serializing_if = "Vec::is_empty")]
169    pub top_etfs: Vec<SectorETF>,
170
171    /// Top mutual funds in this sector
172    #[serde(skip_serializing_if = "Vec::is_empty")]
173    pub top_mutual_funds: Vec<SectorMutualFund>,
174
175    /// Industries within this sector
176    #[serde(skip_serializing_if = "Vec::is_empty")]
177    pub industries: Vec<SectorIndustry>,
178
179    /// Recent research reports
180    #[serde(skip_serializing_if = "Vec::is_empty")]
181    pub research_reports: Vec<ResearchReport>,
182}
183
184/// Sector overview statistics
185#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
186#[cfg_attr(feature = "dataframe", derive(crate::ToDataFrame))]
187#[serde(rename_all = "camelCase")]
188#[non_exhaustive]
189pub struct SectorOverview {
190    /// Number of companies in the sector
191    #[serde(skip_serializing_if = "Option::is_none")]
192    pub companies_count: Option<i64>,
193
194    /// Total market capitalization
195    #[serde(skip_serializing_if = "Option::is_none")]
196    pub market_cap: Option<FormattedValue<f64>>,
197
198    /// Sector description
199    #[serde(skip_serializing_if = "Option::is_none")]
200    pub description: Option<String>,
201
202    /// Number of industries in the sector
203    #[serde(skip_serializing_if = "Option::is_none")]
204    pub industries_count: Option<i64>,
205
206    /// Market weight (percentage of total market)
207    #[serde(skip_serializing_if = "Option::is_none")]
208    pub market_weight: Option<FormattedValue<f64>>,
209
210    /// Total employee count across sector
211    #[serde(skip_serializing_if = "Option::is_none")]
212    pub employee_count: Option<FormattedValue<i64>>,
213}
214
215/// Sector performance metrics
216#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
217#[cfg_attr(feature = "dataframe", derive(crate::ToDataFrame))]
218#[serde(rename_all = "camelCase")]
219#[non_exhaustive]
220pub struct SectorPerformance {
221    /// Year-to-date change percentage
222    #[serde(skip_serializing_if = "Option::is_none")]
223    pub ytd_change_percent: Option<FormattedValue<f64>>,
224
225    /// Regular market change percentage (today)
226    #[serde(skip_serializing_if = "Option::is_none")]
227    pub day_change_percent: Option<FormattedValue<f64>>,
228
229    /// One year change percentage
230    #[serde(skip_serializing_if = "Option::is_none")]
231    pub one_year_change_percent: Option<FormattedValue<f64>>,
232
233    /// Three year change percentage
234    #[serde(skip_serializing_if = "Option::is_none")]
235    pub three_year_change_percent: Option<FormattedValue<f64>>,
236
237    /// Five year change percentage
238    #[serde(skip_serializing_if = "Option::is_none")]
239    pub five_year_change_percent: Option<FormattedValue<f64>>,
240}
241
242/// A company in the sector's top companies list
243#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
244#[cfg_attr(feature = "dataframe", derive(crate::ToDataFrame))]
245#[serde(rename_all = "camelCase")]
246#[non_exhaustive]
247pub struct SectorCompany {
248    /// Stock symbol
249    pub symbol: String,
250
251    /// Company name
252    #[serde(skip_serializing_if = "Option::is_none")]
253    pub name: Option<String>,
254
255    /// Market capitalization
256    #[serde(skip_serializing_if = "Option::is_none")]
257    pub market_cap: Option<FormattedValue<f64>>,
258
259    /// Weight in sector (percentage)
260    #[serde(skip_serializing_if = "Option::is_none")]
261    pub market_weight: Option<FormattedValue<f64>>,
262
263    /// Last traded price
264    #[serde(skip_serializing_if = "Option::is_none")]
265    pub last_price: Option<FormattedValue<f64>>,
266
267    /// Analyst target price
268    #[serde(skip_serializing_if = "Option::is_none")]
269    pub target_price: Option<FormattedValue<f64>>,
270
271    /// Day change percentage
272    #[serde(skip_serializing_if = "Option::is_none")]
273    pub day_change_percent: Option<FormattedValue<f64>>,
274
275    /// Year-to-date return
276    #[serde(skip_serializing_if = "Option::is_none")]
277    pub ytd_return: Option<FormattedValue<f64>>,
278
279    /// Analyst rating (e.g., "Strong Buy", "Buy", "Hold")
280    #[serde(skip_serializing_if = "Option::is_none")]
281    pub rating: Option<String>,
282}
283
284/// An ETF tracking the sector
285#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
286#[cfg_attr(feature = "dataframe", derive(crate::ToDataFrame))]
287#[serde(rename_all = "camelCase")]
288#[non_exhaustive]
289pub struct SectorETF {
290    /// ETF symbol
291    pub symbol: String,
292
293    /// ETF name
294    #[serde(skip_serializing_if = "Option::is_none")]
295    pub name: Option<String>,
296
297    /// Net assets under management
298    #[serde(skip_serializing_if = "Option::is_none")]
299    pub net_assets: Option<FormattedValue<f64>>,
300
301    /// Expense ratio
302    #[serde(skip_serializing_if = "Option::is_none")]
303    pub expense_ratio: Option<FormattedValue<f64>>,
304
305    /// Last traded price
306    #[serde(skip_serializing_if = "Option::is_none")]
307    pub last_price: Option<FormattedValue<f64>>,
308
309    /// Year-to-date return
310    #[serde(skip_serializing_if = "Option::is_none")]
311    pub ytd_return: Option<FormattedValue<f64>>,
312}
313
314/// A mutual fund in the sector
315#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
316#[cfg_attr(feature = "dataframe", derive(crate::ToDataFrame))]
317#[serde(rename_all = "camelCase")]
318#[non_exhaustive]
319pub struct SectorMutualFund {
320    /// Fund symbol
321    pub symbol: String,
322
323    /// Fund name
324    #[serde(skip_serializing_if = "Option::is_none")]
325    pub name: Option<String>,
326
327    /// Net assets under management
328    #[serde(skip_serializing_if = "Option::is_none")]
329    pub net_assets: Option<FormattedValue<f64>>,
330
331    /// Expense ratio
332    #[serde(skip_serializing_if = "Option::is_none")]
333    pub expense_ratio: Option<FormattedValue<f64>>,
334
335    /// Last traded price (NAV)
336    #[serde(skip_serializing_if = "Option::is_none")]
337    pub last_price: Option<FormattedValue<f64>>,
338
339    /// Year-to-date return
340    #[serde(skip_serializing_if = "Option::is_none")]
341    pub ytd_return: Option<FormattedValue<f64>>,
342}
343
344/// An industry within the sector
345#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
346#[cfg_attr(feature = "dataframe", derive(crate::ToDataFrame))]
347#[serde(rename_all = "camelCase")]
348#[non_exhaustive]
349pub struct SectorIndustry {
350    /// Industry symbol
351    #[serde(skip_serializing_if = "Option::is_none")]
352    pub symbol: Option<String>,
353
354    /// Industry key for API calls
355    #[serde(skip_serializing_if = "Option::is_none")]
356    pub key: Option<String>,
357
358    /// Industry name
359    pub name: String,
360
361    /// Weight in sector (percentage)
362    #[serde(skip_serializing_if = "Option::is_none")]
363    pub market_weight: Option<FormattedValue<f64>>,
364
365    /// Day change percentage
366    #[serde(skip_serializing_if = "Option::is_none")]
367    pub day_change_percent: Option<FormattedValue<f64>>,
368
369    /// Year-to-date return
370    #[serde(skip_serializing_if = "Option::is_none")]
371    pub ytd_return: Option<FormattedValue<f64>>,
372}
373
374/// A research report about the sector
375#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
376#[cfg_attr(feature = "dataframe", derive(crate::ToDataFrame))]
377#[serde(rename_all = "camelCase")]
378#[non_exhaustive]
379pub struct ResearchReport {
380    /// Report ID
381    pub id: String,
382
383    /// Report headline/summary (may contain HTML)
384    #[serde(skip_serializing_if = "Option::is_none")]
385    pub headline: Option<String>,
386
387    /// Research provider (e.g., "Argus Research", "Morningstar")
388    #[serde(skip_serializing_if = "Option::is_none")]
389    pub provider: Option<String>,
390
391    /// Report publication date (ISO 8601)
392    #[serde(skip_serializing_if = "Option::is_none")]
393    pub report_date: Option<String>,
394
395    /// Full report title
396    #[serde(skip_serializing_if = "Option::is_none")]
397    pub report_title: Option<String>,
398
399    /// Report type (e.g., "Technical Analysis", "Analyst Report")
400    #[serde(skip_serializing_if = "Option::is_none")]
401    pub report_type: Option<String>,
402
403    /// Target price (if applicable)
404    #[serde(skip_serializing_if = "Option::is_none")]
405    pub target_price: Option<f64>,
406
407    /// Target price status (e.g., "Maintained", "Raised")
408    #[serde(skip_serializing_if = "Option::is_none")]
409    pub target_price_status: Option<String>,
410
411    /// Investment rating (e.g., "Bullish", "Bearish")
412    #[serde(skip_serializing_if = "Option::is_none")]
413    pub investment_rating: Option<String>,
414}
415
416// ============================================================================
417// Conversion implementations
418// ============================================================================
419
420impl Sector {
421    /// Parse Yahoo Finance sector response JSON
422    pub(crate) fn from_response(json: &serde_json::Value) -> Result<Self, String> {
423        let raw: RawSectorResponse = serde_json::from_value(json.clone())
424            .map_err(|e| format!("Failed to parse sector response: {}", e))?;
425
426        let data = raw.data;
427
428        // Convert overview
429        let overview = data.overview.map(|o| SectorOverview {
430            companies_count: o.companies_count,
431            market_cap: o.market_cap,
432            description: o.description,
433            industries_count: o.industries_count,
434            market_weight: o.market_weight,
435            employee_count: o.employee_count,
436        });
437
438        // Convert performance
439        let performance = data.performance.map(|p| SectorPerformance {
440            ytd_change_percent: p.ytd_change_percent,
441            day_change_percent: p.reg_market_change_percent,
442            one_year_change_percent: p.one_year_change_percent,
443            three_year_change_percent: p.three_year_change_percent,
444            five_year_change_percent: p.five_year_change_percent,
445        });
446
447        // Convert benchmark
448        let (benchmark, benchmark_name) = match data.performance_overview_benchmark {
449            Some(b) => (
450                Some(SectorPerformance {
451                    ytd_change_percent: b.ytd_change_percent,
452                    day_change_percent: b.reg_market_change_percent,
453                    one_year_change_percent: b.one_year_change_percent,
454                    three_year_change_percent: b.three_year_change_percent,
455                    five_year_change_percent: b.five_year_change_percent,
456                }),
457                b.name,
458            ),
459            None => (None, None),
460        };
461
462        // Convert top companies
463        let top_companies = data
464            .top_companies
465            .into_iter()
466            .map(|c| SectorCompany {
467                symbol: c.symbol,
468                name: c.name,
469                market_cap: c.market_cap,
470                market_weight: c.market_weight,
471                last_price: c.last_price,
472                target_price: c.target_price,
473                day_change_percent: c.reg_market_change_percent,
474                ytd_return: c.ytd_return,
475                rating: c.rating,
476            })
477            .collect();
478
479        // Convert ETFs
480        let top_etfs = data
481            .top_etfs
482            .into_iter()
483            .map(|e| SectorETF {
484                symbol: e.symbol,
485                name: e.name,
486                net_assets: e.net_assets,
487                expense_ratio: e.expense_ratio,
488                last_price: e.last_price,
489                ytd_return: e.ytd_return,
490            })
491            .collect();
492
493        // Convert mutual funds
494        let top_mutual_funds = data
495            .top_mutual_funds
496            .into_iter()
497            .map(|f| SectorMutualFund {
498                symbol: f.symbol,
499                name: f.name,
500                net_assets: f.net_assets,
501                expense_ratio: f.expense_ratio,
502                last_price: f.last_price,
503                ytd_return: f.ytd_return,
504            })
505            .collect();
506
507        // Convert industries
508        let industries = data
509            .industries
510            .into_iter()
511            .map(|i| SectorIndustry {
512                symbol: i.symbol,
513                key: i.key,
514                name: i.name,
515                market_weight: i.market_weight,
516                day_change_percent: i.reg_market_change_percent,
517                ytd_return: i.ytd_return,
518            })
519            .collect();
520
521        // Convert research reports
522        let research_reports = data
523            .research_reports
524            .into_iter()
525            .map(|r| ResearchReport {
526                id: r.id,
527                headline: r.head_html,
528                provider: r.provider,
529                report_date: r.report_date,
530                report_title: r.report_title,
531                report_type: r.report_type,
532                target_price: r.target_price,
533                target_price_status: r.target_price_status,
534                investment_rating: r.investment_rating,
535            })
536            .collect();
537
538        Ok(Self {
539            name: data.name,
540            symbol: data.symbol,
541            key: data.key,
542            overview,
543            performance,
544            benchmark,
545            benchmark_name,
546            top_companies,
547            top_etfs,
548            top_mutual_funds,
549            industries,
550            research_reports,
551        })
552    }
553}