1use crate::models::quote::FormattedValue;
2use serde::{Deserialize, Serialize};
3
4#[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#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
134#[serde(rename_all = "camelCase")]
135#[non_exhaustive]
136pub struct Sector {
137 pub name: String,
139
140 #[serde(skip_serializing_if = "Option::is_none")]
142 pub symbol: Option<String>,
143
144 pub key: String,
146
147 #[serde(skip_serializing_if = "Option::is_none")]
149 pub overview: Option<SectorOverview>,
150
151 #[serde(skip_serializing_if = "Option::is_none")]
153 pub performance: Option<SectorPerformance>,
154
155 #[serde(skip_serializing_if = "Option::is_none")]
157 pub benchmark: Option<SectorPerformance>,
158
159 #[serde(skip_serializing_if = "Option::is_none")]
161 pub benchmark_name: Option<String>,
162
163 #[serde(skip_serializing_if = "Vec::is_empty")]
165 pub top_companies: Vec<SectorCompany>,
166
167 #[serde(skip_serializing_if = "Vec::is_empty")]
169 pub top_etfs: Vec<SectorETF>,
170
171 #[serde(skip_serializing_if = "Vec::is_empty")]
173 pub top_mutual_funds: Vec<SectorMutualFund>,
174
175 #[serde(skip_serializing_if = "Vec::is_empty")]
177 pub industries: Vec<SectorIndustry>,
178
179 #[serde(skip_serializing_if = "Vec::is_empty")]
181 pub research_reports: Vec<ResearchReport>,
182}
183
184#[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 #[serde(skip_serializing_if = "Option::is_none")]
192 pub companies_count: Option<i64>,
193
194 #[serde(skip_serializing_if = "Option::is_none")]
196 pub market_cap: Option<FormattedValue<f64>>,
197
198 #[serde(skip_serializing_if = "Option::is_none")]
200 pub description: Option<String>,
201
202 #[serde(skip_serializing_if = "Option::is_none")]
204 pub industries_count: Option<i64>,
205
206 #[serde(skip_serializing_if = "Option::is_none")]
208 pub market_weight: Option<FormattedValue<f64>>,
209
210 #[serde(skip_serializing_if = "Option::is_none")]
212 pub employee_count: Option<FormattedValue<i64>>,
213}
214
215#[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 #[serde(skip_serializing_if = "Option::is_none")]
223 pub ytd_change_percent: Option<FormattedValue<f64>>,
224
225 #[serde(skip_serializing_if = "Option::is_none")]
227 pub day_change_percent: Option<FormattedValue<f64>>,
228
229 #[serde(skip_serializing_if = "Option::is_none")]
231 pub one_year_change_percent: Option<FormattedValue<f64>>,
232
233 #[serde(skip_serializing_if = "Option::is_none")]
235 pub three_year_change_percent: Option<FormattedValue<f64>>,
236
237 #[serde(skip_serializing_if = "Option::is_none")]
239 pub five_year_change_percent: Option<FormattedValue<f64>>,
240}
241
242#[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 pub symbol: String,
250
251 #[serde(skip_serializing_if = "Option::is_none")]
253 pub name: Option<String>,
254
255 #[serde(skip_serializing_if = "Option::is_none")]
257 pub market_cap: Option<FormattedValue<f64>>,
258
259 #[serde(skip_serializing_if = "Option::is_none")]
261 pub market_weight: Option<FormattedValue<f64>>,
262
263 #[serde(skip_serializing_if = "Option::is_none")]
265 pub last_price: Option<FormattedValue<f64>>,
266
267 #[serde(skip_serializing_if = "Option::is_none")]
269 pub target_price: Option<FormattedValue<f64>>,
270
271 #[serde(skip_serializing_if = "Option::is_none")]
273 pub day_change_percent: Option<FormattedValue<f64>>,
274
275 #[serde(skip_serializing_if = "Option::is_none")]
277 pub ytd_return: Option<FormattedValue<f64>>,
278
279 #[serde(skip_serializing_if = "Option::is_none")]
281 pub rating: Option<String>,
282}
283
284#[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 pub symbol: String,
292
293 #[serde(skip_serializing_if = "Option::is_none")]
295 pub name: Option<String>,
296
297 #[serde(skip_serializing_if = "Option::is_none")]
299 pub net_assets: Option<FormattedValue<f64>>,
300
301 #[serde(skip_serializing_if = "Option::is_none")]
303 pub expense_ratio: Option<FormattedValue<f64>>,
304
305 #[serde(skip_serializing_if = "Option::is_none")]
307 pub last_price: Option<FormattedValue<f64>>,
308
309 #[serde(skip_serializing_if = "Option::is_none")]
311 pub ytd_return: Option<FormattedValue<f64>>,
312}
313
314#[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 pub symbol: String,
322
323 #[serde(skip_serializing_if = "Option::is_none")]
325 pub name: Option<String>,
326
327 #[serde(skip_serializing_if = "Option::is_none")]
329 pub net_assets: Option<FormattedValue<f64>>,
330
331 #[serde(skip_serializing_if = "Option::is_none")]
333 pub expense_ratio: Option<FormattedValue<f64>>,
334
335 #[serde(skip_serializing_if = "Option::is_none")]
337 pub last_price: Option<FormattedValue<f64>>,
338
339 #[serde(skip_serializing_if = "Option::is_none")]
341 pub ytd_return: Option<FormattedValue<f64>>,
342}
343
344#[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 #[serde(skip_serializing_if = "Option::is_none")]
352 pub symbol: Option<String>,
353
354 #[serde(skip_serializing_if = "Option::is_none")]
356 pub key: Option<String>,
357
358 pub name: String,
360
361 #[serde(skip_serializing_if = "Option::is_none")]
363 pub market_weight: Option<FormattedValue<f64>>,
364
365 #[serde(skip_serializing_if = "Option::is_none")]
367 pub day_change_percent: Option<FormattedValue<f64>>,
368
369 #[serde(skip_serializing_if = "Option::is_none")]
371 pub ytd_return: Option<FormattedValue<f64>>,
372}
373
374#[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 pub id: String,
382
383 #[serde(skip_serializing_if = "Option::is_none")]
385 pub headline: Option<String>,
386
387 #[serde(skip_serializing_if = "Option::is_none")]
389 pub provider: Option<String>,
390
391 #[serde(skip_serializing_if = "Option::is_none")]
393 pub report_date: Option<String>,
394
395 #[serde(skip_serializing_if = "Option::is_none")]
397 pub report_title: Option<String>,
398
399 #[serde(skip_serializing_if = "Option::is_none")]
401 pub report_type: Option<String>,
402
403 #[serde(skip_serializing_if = "Option::is_none")]
405 pub target_price: Option<f64>,
406
407 #[serde(skip_serializing_if = "Option::is_none")]
409 pub target_price_status: Option<String>,
410
411 #[serde(skip_serializing_if = "Option::is_none")]
413 pub investment_rating: Option<String>,
414}
415
416impl Sector {
421 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 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 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 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 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 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 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 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 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}