1use std::sync::Arc;
2
3use longbridge_httpcli::{HttpClient, Json, Method};
4use serde::{Serialize, de::DeserializeOwned};
5use tracing::{Subscriber, dispatcher, instrument::WithSubscriber};
6
7use crate::{
8 Config, Result,
9 fundamental::types::*,
10 utils::counter::{counter_id_to_symbol, symbol_to_counter_id},
11};
12
13fn unix_secs_str_to_rfc3339(s: &str) -> String {
15 s.parse::<i64>()
16 .ok()
17 .and_then(|ts| time::OffsetDateTime::from_unix_timestamp(ts).ok())
18 .map(|dt| {
19 use time::format_description::well_known::Rfc3339;
20 dt.format(&Rfc3339).unwrap_or_default()
21 })
22 .unwrap_or_else(|| s.to_string())
23}
24
25struct InnerFundamentalContext {
26 http_cli: HttpClient,
27 log_subscriber: Arc<dyn Subscriber + Send + Sync>,
28}
29
30impl Drop for InnerFundamentalContext {
31 fn drop(&mut self) {
32 dispatcher::with_default(&self.log_subscriber.clone().into(), || {
33 tracing::info!("fundamental context dropped");
34 });
35 }
36}
37
38#[derive(Clone)]
41pub struct FundamentalContext(Arc<InnerFundamentalContext>);
42
43impl FundamentalContext {
44 pub fn new(config: Arc<Config>) -> Self {
46 let log_subscriber = config.create_log_subscriber("fundamental");
47 dispatcher::with_default(&log_subscriber.clone().into(), || {
48 tracing::info!(language = ?config.language, "creating fundamental context");
49 });
50 let ctx = Self(Arc::new(InnerFundamentalContext {
51 http_cli: config.create_http_client(),
52 log_subscriber,
53 }));
54 dispatcher::with_default(&ctx.0.log_subscriber.clone().into(), || {
55 tracing::info!("fundamental context created");
56 });
57 ctx
58 }
59
60 #[inline]
62 pub fn log_subscriber(&self) -> Arc<dyn Subscriber + Send + Sync> {
63 self.0.log_subscriber.clone()
64 }
65
66 async fn get<R, Q>(&self, path: &'static str, query: Q) -> Result<R>
67 where
68 R: DeserializeOwned + Send + Sync + 'static,
69 Q: Serialize + Send + Sync,
70 {
71 Ok(self
72 .0
73 .http_cli
74 .request(Method::GET, path)
75 .query_params(query)
76 .response::<Json<R>>()
77 .send()
78 .with_subscriber(self.0.log_subscriber.clone())
79 .await?
80 .0)
81 }
82
83 pub async fn financial_report(
89 &self,
90 symbol: impl Into<String>,
91 kind: FinancialReportKind,
92 period: Option<FinancialReportPeriod>,
93 ) -> Result<FinancialReports> {
94 let kind_str = match kind {
95 FinancialReportKind::IncomeStatement => "IS",
96 FinancialReportKind::BalanceSheet => "BS",
97 FinancialReportKind::CashFlow => "CF",
98 FinancialReportKind::All => "ALL",
99 };
100 let period_str = period.map(|p| match p {
101 FinancialReportPeriod::Annual => "af",
102 FinancialReportPeriod::SemiAnnual => "saf",
103 FinancialReportPeriod::Q1 => "q1",
104 FinancialReportPeriod::Q2 => "q2",
105 FinancialReportPeriod::Q3 => "q3",
106 FinancialReportPeriod::QuarterlyFull => "qf",
107 FinancialReportPeriod::ThreeQ => "3q",
108 });
109 #[derive(Serialize)]
110 struct Query {
111 counter_id: String,
112 kind: &'static str,
113 #[serde(skip_serializing_if = "Option::is_none")]
114 report: Option<&'static str>,
115 }
116 self.get(
117 "/v1/quote/financial-reports",
118 Query {
119 counter_id: symbol_to_counter_id(&symbol.into()),
120 kind: kind_str,
121 report: period_str,
122 },
123 )
124 .await
125 }
126
127 pub async fn institution_rating(&self, symbol: impl Into<String>) -> Result<InstitutionRating> {
134 #[derive(Serialize)]
135 struct Query {
136 counter_id: String,
137 }
138 let cid = symbol_to_counter_id(&symbol.into());
139 let q = Query { counter_id: cid };
140 let (latest, summary) = tokio::join!(
141 self.get::<InstitutionRatingLatest, _>(
142 "/v1/quote/institution-rating-latest",
143 Query {
144 counter_id: q.counter_id.clone()
145 }
146 ),
147 self.get::<InstitutionRatingSummary, _>(
148 "/v1/quote/institution-ratings",
149 Query {
150 counter_id: q.counter_id.clone()
151 }
152 ),
153 );
154 Ok(InstitutionRating {
155 latest: latest?,
156 summary: summary?,
157 })
158 }
159
160 pub async fn institution_rating_detail(
164 &self,
165 symbol: impl Into<String>,
166 ) -> Result<InstitutionRatingDetail> {
167 #[derive(Serialize)]
168 struct Query {
169 counter_id: String,
170 }
171 self.get(
172 "/v1/quote/institution-ratings/detail",
173 Query {
174 counter_id: symbol_to_counter_id(&symbol.into()),
175 },
176 )
177 .await
178 }
179
180 pub async fn dividend(&self, symbol: impl Into<String>) -> Result<DividendList> {
186 #[derive(Serialize)]
187 struct Query {
188 counter_id: String,
189 }
190 self.get(
191 "/v1/quote/dividends",
192 Query {
193 counter_id: symbol_to_counter_id(&symbol.into()),
194 },
195 )
196 .await
197 }
198
199 pub async fn dividend_detail(&self, symbol: impl Into<String>) -> Result<DividendList> {
203 #[derive(Serialize)]
204 struct Query {
205 counter_id: String,
206 }
207 self.get(
208 "/v1/quote/dividends/details",
209 Query {
210 counter_id: symbol_to_counter_id(&symbol.into()),
211 },
212 )
213 .await
214 }
215
216 pub async fn forecast_eps(&self, symbol: impl Into<String>) -> Result<ForecastEps> {
222 #[derive(Serialize)]
223 struct Query {
224 counter_id: String,
225 }
226 self.get(
227 "/v1/quote/forecast-eps",
228 Query {
229 counter_id: symbol_to_counter_id(&symbol.into()),
230 },
231 )
232 .await
233 }
234
235 pub async fn consensus(&self, symbol: impl Into<String>) -> Result<FinancialConsensus> {
241 #[derive(Serialize)]
242 struct Query {
243 counter_id: String,
244 }
245 self.get(
246 "/v1/quote/financial-consensus-detail",
247 Query {
248 counter_id: symbol_to_counter_id(&symbol.into()),
249 },
250 )
251 .await
252 }
253
254 pub async fn valuation(&self, symbol: impl Into<String>) -> Result<ValuationData> {
260 #[derive(Serialize)]
261 struct Query {
262 counter_id: String,
263 indicator: &'static str,
264 range: &'static str,
265 }
266 self.get(
267 "/v1/quote/valuation",
268 Query {
269 counter_id: symbol_to_counter_id(&symbol.into()),
270 indicator: "pe",
271 range: "1",
272 },
273 )
274 .await
275 }
276
277 pub async fn valuation_history(
281 &self,
282 symbol: impl Into<String>,
283 ) -> Result<ValuationHistoryResponse> {
284 #[derive(Serialize)]
285 struct Query {
286 counter_id: String,
287 }
288 self.get(
289 "/v1/quote/valuation/detail",
290 Query {
291 counter_id: symbol_to_counter_id(&symbol.into()),
292 },
293 )
294 .await
295 }
296
297 pub async fn industry_valuation(
303 &self,
304 symbol: impl Into<String>,
305 ) -> Result<IndustryValuationList> {
306 #[derive(Serialize)]
307 struct Query {
308 counter_id: String,
309 }
310 self.get(
311 "/v1/quote/industry-valuation-comparison",
312 Query {
313 counter_id: symbol_to_counter_id(&symbol.into()),
314 },
315 )
316 .await
317 }
318
319 pub async fn industry_valuation_dist(
323 &self,
324 symbol: impl Into<String>,
325 ) -> Result<IndustryValuationDist> {
326 #[derive(Serialize)]
327 struct Query {
328 counter_id: String,
329 }
330 self.get(
331 "/v1/quote/industry-valuation-distribution",
332 Query {
333 counter_id: symbol_to_counter_id(&symbol.into()),
334 },
335 )
336 .await
337 }
338
339 pub async fn company(&self, symbol: impl Into<String>) -> Result<CompanyOverview> {
345 #[derive(Serialize)]
346 struct Query {
347 counter_id: String,
348 }
349 self.get(
350 "/v1/quote/comp-overview",
351 Query {
352 counter_id: symbol_to_counter_id(&symbol.into()),
353 },
354 )
355 .await
356 }
357
358 pub async fn executive(&self, symbol: impl Into<String>) -> Result<ExecutiveList> {
364 #[derive(Serialize)]
365 struct Query {
366 counter_ids: String,
367 }
368 self.get(
369 "/v1/quote/company-professionals",
370 Query {
371 counter_ids: symbol_to_counter_id(&symbol.into()),
372 },
373 )
374 .await
375 }
376
377 pub async fn shareholder(&self, symbol: impl Into<String>) -> Result<ShareholderList> {
383 #[derive(Serialize)]
384 struct Query {
385 counter_id: String,
386 }
387 self.get(
388 "/v1/quote/shareholders",
389 Query {
390 counter_id: symbol_to_counter_id(&symbol.into()),
391 },
392 )
393 .await
394 }
395
396 pub async fn fund_holder(&self, symbol: impl Into<String>) -> Result<FundHolders> {
402 #[derive(Serialize)]
403 struct Query {
404 counter_id: String,
405 }
406 self.get(
407 "/v1/quote/fund-holders",
408 Query {
409 counter_id: symbol_to_counter_id(&symbol.into()),
410 },
411 )
412 .await
413 }
414
415 pub async fn corp_action(&self, symbol: impl Into<String>) -> Result<CorpActions> {
421 #[derive(Serialize)]
422 struct Query {
423 counter_id: String,
424 req_type: &'static str,
425 version: &'static str,
426 }
427 self.get(
428 "/v1/quote/company-act",
429 Query {
430 counter_id: symbol_to_counter_id(&symbol.into()),
431 req_type: "1",
432 version: "3",
433 },
434 )
435 .await
436 }
437
438 pub async fn invest_relation(&self, symbol: impl Into<String>) -> Result<InvestRelations> {
444 #[derive(Serialize)]
445 struct Query {
446 counter_id: String,
447 count: &'static str,
448 }
449 self.get(
450 "/v1/quote/invest-relations",
451 Query {
452 counter_id: symbol_to_counter_id(&symbol.into()),
453 count: "0",
454 },
455 )
456 .await
457 }
458
459 pub async fn operating(&self, symbol: impl Into<String>) -> Result<OperatingList> {
465 #[derive(Serialize)]
466 struct Query {
467 counter_id: String,
468 }
469 self.get(
470 "/v1/quote/operatings",
471 Query {
472 counter_id: symbol_to_counter_id(&symbol.into()),
473 },
474 )
475 .await
476 }
477
478 pub async fn buyback(&self, symbol: impl Into<String>) -> Result<BuybackData> {
484 #[derive(Serialize)]
485 struct Query {
486 counter_id: String,
487 }
488 self.get(
489 "/v1/quote/buy-backs",
490 Query {
491 counter_id: symbol_to_counter_id(&symbol.into()),
492 },
493 )
494 .await
495 }
496
497 pub async fn ratings(&self, symbol: impl Into<String>) -> Result<StockRatings> {
503 #[derive(Serialize)]
504 struct Query {
505 counter_id: String,
506 }
507 self.get(
508 "/v1/quote/ratings",
509 Query {
510 counter_id: symbol_to_counter_id(&symbol.into()),
511 },
512 )
513 .await
514 }
515
516 pub async fn business_segments(&self, symbol: impl Into<String>) -> Result<BusinessSegments> {
522 #[derive(Serialize)]
523 struct Query {
524 counter_id: String,
525 }
526 self.get(
527 "/v1/quote/fundamentals/business-segments",
528 Query {
529 counter_id: symbol_to_counter_id(&symbol.into()),
530 },
531 )
532 .await
533 }
534
535 pub async fn business_segments_history(
539 &self,
540 symbol: impl Into<String>,
541 report: Option<&'static str>,
542 cate: Option<String>,
543 ) -> Result<BusinessSegmentsHistory> {
544 #[derive(Serialize)]
545 struct Query {
546 counter_id: String,
547 #[serde(skip_serializing_if = "Option::is_none")]
548 report: Option<&'static str>,
549 #[serde(skip_serializing_if = "Option::is_none")]
550 cate: Option<String>,
551 }
552 self.get(
553 "/v1/quote/fundamentals/business-segments/history",
554 Query {
555 counter_id: symbol_to_counter_id(&symbol.into()),
556 report,
557 cate,
558 },
559 )
560 .await
561 }
562
563 pub async fn shareholder_top(
569 &self,
570 symbol: impl Into<String>,
571 ) -> Result<ShareholderTopResponse> {
572 #[derive(Serialize)]
573 struct Query {
574 counter_id: String,
575 }
576 let raw: serde_json::Value = self
577 .get(
578 "/v1/quote/shareholders/top",
579 Query {
580 counter_id: symbol_to_counter_id(&symbol.into()),
581 },
582 )
583 .await?;
584 Ok(ShareholderTopResponse { data: raw })
585 }
586
587 pub async fn institution_rating_views(
593 &self,
594 symbol: impl Into<String>,
595 ) -> Result<InstitutionRatingViews> {
596 #[derive(Serialize)]
597 struct Query {
598 counter_id: String,
599 }
600 self.get(
601 "/v1/quote/ratings/institutional",
602 Query {
603 counter_id: symbol_to_counter_id(&symbol.into()),
604 },
605 )
606 .await
607 }
608
609 pub async fn shareholder_detail(
615 &self,
616 symbol: impl Into<String>,
617 object_id: i64,
618 ) -> Result<ShareholderDetailResponse> {
619 #[derive(Serialize)]
620 struct Query {
621 counter_id: String,
622 object_id: String,
623 }
624 let raw: serde_json::Value = self
625 .get(
626 "/v1/quote/shareholders/holding",
627 Query {
628 counter_id: symbol_to_counter_id(&symbol.into()),
629 object_id: object_id.to_string(),
630 },
631 )
632 .await?;
633 Ok(ShareholderDetailResponse { data: raw })
634 }
635
636 pub async fn industry_rank(
642 &self,
643 market: impl Into<String>,
644 indicator: impl Into<String>,
645 sort_type: impl Into<String>,
646 limit: u32,
647 ) -> Result<IndustryRankResponse> {
648 #[derive(Serialize)]
649 struct Query {
650 market: String,
651 indicator: String,
652 sort_type: String,
653 limit: u32,
654 }
655 self.get(
656 "/v1/quote/industry/rank",
657 Query {
658 market: market.into(),
659 indicator: indicator.into(),
660 sort_type: sort_type.into(),
661 limit,
662 },
663 )
664 .await
665 }
666
667 pub async fn industry_peers(
673 &self,
674 counter_id: impl Into<String>,
675 market: impl Into<String>,
676 industry_id: Option<String>,
677 ) -> Result<IndustryPeersResponse> {
678 let raw = counter_id.into();
679 let cid = if raw.contains('/') {
680 raw
681 } else {
682 symbol_to_counter_id(&raw)
683 };
684 #[derive(Serialize)]
685 struct Query {
686 #[serde(rename = "type")]
687 kind: &'static str,
688 market: String,
689 industry_id: String,
690 counter_id: String,
691 }
692 self.get(
693 "/v1/quote/industries/peers",
694 Query {
695 kind: "1",
696 market: market.into(),
697 industry_id: industry_id.unwrap_or_default(),
698 counter_id: cid,
699 },
700 )
701 .await
702 }
703
704 pub async fn financial_report_snapshot(
710 &self,
711 symbol: impl Into<String>,
712 report: Option<&'static str>,
713 fiscal_year: Option<i32>,
714 fiscal_period: Option<&'static str>,
715 ) -> Result<FinancialReportSnapshot> {
716 #[derive(Serialize)]
717 struct Query {
718 counter_id: String,
719 #[serde(skip_serializing_if = "Option::is_none")]
720 report: Option<&'static str>,
721 #[serde(skip_serializing_if = "Option::is_none")]
722 fiscal_year: Option<i32>,
723 #[serde(skip_serializing_if = "Option::is_none")]
724 fiscal_period: Option<&'static str>,
725 }
726 self.get(
727 "/v1/quote/financials/earnings-snapshot",
728 Query {
729 counter_id: symbol_to_counter_id(&symbol.into()),
730 report,
731 fiscal_year,
732 fiscal_period,
733 },
734 )
735 .await
736 }
737
738 pub async fn valuation_comparison(
744 &self,
745 symbol: impl Into<String>,
746 currency: impl Into<String>,
747 comparison_symbols: Option<Vec<String>>,
748 ) -> Result<ValuationComparisonResponse> {
749 #[derive(Serialize)]
750 struct Query {
751 counter_id: String,
752 currency: String,
753 #[serde(skip_serializing_if = "Option::is_none")]
754 comparison_counter_ids: Option<String>,
755 }
756 let comparison_counter_ids = comparison_symbols.map(|syms| {
757 let ids: Vec<String> = syms.iter().map(|s| symbol_to_counter_id(s)).collect();
758 serde_json::to_string(&ids).unwrap_or_default()
759 });
760 let raw: serde_json::Value = self
761 .get(
762 "/v1/quote/compare/valuation",
763 Query {
764 counter_id: symbol_to_counter_id(&symbol.into()),
765 currency: currency.into(),
766 comparison_counter_ids,
767 },
768 )
769 .await?;
770 let list = raw["list"]
771 .as_array()
772 .cloned()
773 .unwrap_or_default()
774 .into_iter()
775 .map(|item| {
776 let history = item["history"]
777 .as_array()
778 .cloned()
779 .unwrap_or_default()
780 .into_iter()
781 .map(|h| ValuationHistoryPoint {
782 date: unix_secs_str_to_rfc3339(h["date"].as_str().unwrap_or("")),
783 pe: h["pe"].as_str().unwrap_or("").to_string(),
784 pb: h["pb"].as_str().unwrap_or("").to_string(),
785 ps: h["ps"].as_str().unwrap_or("").to_string(),
786 })
787 .collect();
788 ValuationComparisonItem {
789 symbol: counter_id_to_symbol(item["counter_id"].as_str().unwrap_or("")),
790 name: item["name"].as_str().unwrap_or("").to_string(),
791 currency: item["currency"].as_str().unwrap_or("").to_string(),
792 market_value: item["market_value"].as_str().unwrap_or("").to_string(),
793 price_close: item["price_close"].as_str().unwrap_or("").to_string(),
794 pe: item["pe"].as_str().unwrap_or("").to_string(),
795 pb: item["pb"].as_str().unwrap_or("").to_string(),
796 ps: item["ps"].as_str().unwrap_or("").to_string(),
797 roe: item["roe"].as_str().unwrap_or("").to_string(),
798 eps: item["eps"].as_str().unwrap_or("").to_string(),
799 bps: item["bps"].as_str().unwrap_or("").to_string(),
800 dps: item["dps"].as_str().unwrap_or("").to_string(),
801 div_yld: item["div_yld"].as_str().unwrap_or("").to_string(),
802 assets: item["assets"].as_str().unwrap_or("").to_string(),
803 history,
804 }
805 })
806 .collect();
807 Ok(ValuationComparisonResponse { list })
808 }
809
810 pub async fn etf_asset_allocation(
817 &self,
818 symbol: impl Into<String>,
819 ) -> Result<AssetAllocationResponse> {
820 #[derive(Serialize)]
821 struct Query {
822 counter_id: String,
823 }
824 self.get(
825 "/v1/quote/etf-asset-allocation",
826 Query {
827 counter_id: symbol_to_counter_id(&symbol.into()),
828 },
829 )
830 .await
831 }
832
833 pub async fn macroeconomic_indicators(
844 &self,
845 country: Option<MacroeconomicCountry>,
846 keyword: Option<impl Into<String>>,
847 offset: Option<i32>,
848 limit: Option<i32>,
849 ) -> Result<MacroeconomicIndicatorListResponse> {
850 self.macroeconomic_indicators_v2(country, keyword, offset, limit)
851 .await
852 }
853
854 pub(crate) async fn macroeconomic_indicators_v2(
858 &self,
859 country: Option<MacroeconomicCountry>,
860 keyword: Option<impl Into<String>>,
861 offset: Option<i32>,
862 limit: Option<i32>,
863 ) -> Result<MacroeconomicIndicatorListResponse> {
864 #[derive(Serialize)]
865 struct Query {
866 market: String,
867 #[serde(skip_serializing_if = "Option::is_none")]
868 keyword: Option<String>,
869 #[serde(skip_serializing_if = "Option::is_none")]
870 offset: Option<i32>,
871 #[serde(skip_serializing_if = "Option::is_none")]
872 limit: Option<i32>,
873 }
874 let market = country
875 .map(|c| match c {
876 MacroeconomicCountry::HongKong => "HK",
877 MacroeconomicCountry::China => "CN",
878 MacroeconomicCountry::UnitedStates => "US",
879 MacroeconomicCountry::EuroZone => "EU",
880 MacroeconomicCountry::Japan => "JP",
881 MacroeconomicCountry::Singapore => "SG",
882 })
883 .unwrap_or("ALL")
884 .to_string();
885
886 let raw: V2MacroIndicatorListResponse = self
887 .get(
888 "/v2/quote/macrodata",
889 Query {
890 market,
891 keyword: keyword.map(|k| k.into()),
892 offset,
893 limit,
894 },
895 )
896 .await?;
897
898 let total = raw.total;
899 let data = raw
900 .indicator_list
901 .into_iter()
902 .map(|ind| MacroeconomicIndicator {
903 indicator_code: ind.indicator_id.to_string(),
904 country: ind.market,
905 name: ind.indicator_name,
906 periodicity: ind.frequence,
907 describe: ind.description,
908 importance: ind.importance,
909 ..Default::default()
910 })
911 .collect::<Vec<_>>();
912 let count = if total > 0 { total } else { data.len() as i32 };
913 Ok(MacroeconomicIndicatorListResponse { data, count })
914 }
915
916 pub async fn macroeconomic(
924 &self,
925 indicator_code: impl Into<String>,
926 start_date: Option<impl Into<String>>,
927 end_date: Option<impl Into<String>>,
928 offset: Option<i32>,
929 limit: Option<i32>,
930 ) -> Result<MacroeconomicResponse> {
931 self.macroeconomic_v2(
932 indicator_code,
933 start_date,
934 end_date,
935 offset,
936 limit,
937 None::<String>,
938 )
939 .await
940 }
941
942 pub(crate) async fn macroeconomic_v2(
947 &self,
948 indicator_code: impl Into<String>,
949 start_date: Option<impl Into<String>>,
950 end_date: Option<impl Into<String>>,
951 offset: Option<i32>,
952 limit: Option<i32>,
953 sort: Option<impl Into<String>>,
954 ) -> Result<MacroeconomicResponse> {
955 #[derive(Serialize)]
956 struct Query {
957 #[serde(skip_serializing_if = "Option::is_none")]
958 start_date: Option<String>,
959 #[serde(skip_serializing_if = "Option::is_none")]
960 end_date: Option<String>,
961 #[serde(skip_serializing_if = "Option::is_none")]
962 offset: Option<i32>,
963 #[serde(skip_serializing_if = "Option::is_none")]
964 limit: Option<i32>,
965 #[serde(skip_serializing_if = "Option::is_none")]
966 sort: Option<String>,
967 }
968 let path = format!("/v2/quote/macrodata/{}", indicator_code.into());
969 let raw: V2MacroIndicatorDataResponse = self
970 .0
971 .http_cli
972 .request(Method::GET, path)
973 .query_params(Query {
974 start_date: start_date.map(|d| d.into()),
975 end_date: end_date.map(|d| d.into()),
976 offset,
977 limit,
978 sort: Some(sort.map(|s| s.into()).unwrap_or_else(|| "desc".to_string())),
979 })
980 .response::<Json<V2MacroIndicatorDataResponse>>()
981 .send()
982 .with_subscriber(self.0.log_subscriber.clone())
983 .await?
984 .0;
985
986 let total = raw.total;
987 let detail = raw.indicator;
988 let unit_english = detail.unit.clone();
989 let count = detail.indicator_data.len() as i32;
990
991 let info = MacroeconomicIndicator {
992 indicator_code: detail.indicator_id.to_string(),
993 country: detail.market,
994 name: detail.indicator_name,
995 describe: detail.description,
996 periodicity: detail.frequence,
997 importance: detail.importance,
998 ..Default::default()
999 };
1000
1001 let data = detail
1002 .indicator_data
1003 .into_iter()
1004 .map(|d| {
1005 use time::format_description::well_known::Rfc3339;
1006 let release_at = time::OffsetDateTime::parse(&d.published_time, &Rfc3339)
1007 .ok()
1008 .or_else(|| {
1009 time::PrimitiveDateTime::parse(
1011 &d.published_time,
1012 &time::macros::format_description!(
1013 "[year]-[month]-[day]T[hour]:[minute]:[second]"
1014 ),
1015 )
1016 .ok()
1017 .map(|dt| dt.assume_utc())
1018 });
1019 Macroeconomic {
1020 period: d.observation_date,
1021 release_at,
1022 actual_value: d.actual_data,
1023 previous_value: d.previous_data,
1024 forecast_value: d.estimated_data,
1025 unit: unit_english.clone(),
1026 ..Default::default()
1027 }
1028 })
1029 .collect();
1030
1031 let count = if total > 0 { total } else { count };
1032 Ok(MacroeconomicResponse { info, data, count })
1033 }
1034}