1use std::sync::Arc;
2
3use longbridge_httpcli::{DcRegion, 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 async fn get_dc<R, Q>(&self, path: &'static str, query: Q, dc_restrict: DcRegion) -> Result<R>
86 where
87 R: DeserializeOwned + Send + Sync + 'static,
88 Q: Serialize + Send + Sync,
89 {
90 Ok(self
91 .0
92 .http_cli
93 .request(Method::GET, path)
94 .dc_restrict(dc_restrict)
95 .query_params(query)
96 .response::<Json<R>>()
97 .send()
98 .with_subscriber(self.0.log_subscriber.clone())
99 .await?
100 .0)
101 }
102
103 pub async fn financial_report(
109 &self,
110 symbol: impl Into<String>,
111 kind: FinancialReportKind,
112 period: Option<FinancialReportPeriod>,
113 ) -> Result<FinancialReports> {
114 let kind_str = match kind {
115 FinancialReportKind::IncomeStatement => "IS",
116 FinancialReportKind::BalanceSheet => "BS",
117 FinancialReportKind::CashFlow => "CF",
118 FinancialReportKind::All => "ALL",
119 };
120 let period_str = period.map(|p| match p {
121 FinancialReportPeriod::Annual => "af",
122 FinancialReportPeriod::SemiAnnual => "saf",
123 FinancialReportPeriod::Q1 => "q1",
124 FinancialReportPeriod::Q2 => "q2",
125 FinancialReportPeriod::Q3 => "q3",
126 FinancialReportPeriod::QuarterlyFull => "qf",
127 FinancialReportPeriod::ThreeQ => "3q",
128 });
129 #[derive(Serialize)]
130 struct Query {
131 counter_id: String,
132 kind: &'static str,
133 #[serde(skip_serializing_if = "Option::is_none")]
134 report: Option<&'static str>,
135 }
136 self.get(
137 "/v1/quote/financial-reports",
138 Query {
139 counter_id: symbol_to_counter_id(&symbol.into()),
140 kind: kind_str,
141 report: period_str,
142 },
143 )
144 .await
145 }
146
147 pub async fn institution_rating(&self, symbol: impl Into<String>) -> Result<InstitutionRating> {
154 #[derive(Serialize)]
155 struct Query {
156 counter_id: String,
157 }
158 let cid = symbol_to_counter_id(&symbol.into());
159 let q = Query { counter_id: cid };
160 let (latest, summary) = tokio::join!(
161 self.get::<InstitutionRatingLatest, _>(
162 "/v1/quote/institution-rating-latest",
163 Query {
164 counter_id: q.counter_id.clone()
165 }
166 ),
167 self.get::<InstitutionRatingSummary, _>(
168 "/v1/quote/institution-ratings",
169 Query {
170 counter_id: q.counter_id.clone()
171 }
172 ),
173 );
174 Ok(InstitutionRating {
175 latest: latest?,
176 summary: summary?,
177 })
178 }
179
180 pub async fn institution_rating_detail(
184 &self,
185 symbol: impl Into<String>,
186 ) -> Result<InstitutionRatingDetail> {
187 #[derive(Serialize)]
188 struct Query {
189 counter_id: String,
190 }
191 self.get(
192 "/v1/quote/institution-ratings/detail",
193 Query {
194 counter_id: symbol_to_counter_id(&symbol.into()),
195 },
196 )
197 .await
198 }
199
200 pub async fn dividend(&self, symbol: impl Into<String>) -> Result<DividendList> {
206 #[derive(Serialize)]
207 struct Query {
208 counter_id: String,
209 }
210 self.get(
211 "/v1/quote/dividends",
212 Query {
213 counter_id: symbol_to_counter_id(&symbol.into()),
214 },
215 )
216 .await
217 }
218
219 pub async fn dividend_detail(&self, symbol: impl Into<String>) -> Result<DividendList> {
223 #[derive(Serialize)]
224 struct Query {
225 counter_id: String,
226 }
227 self.get(
228 "/v1/quote/dividends/details",
229 Query {
230 counter_id: symbol_to_counter_id(&symbol.into()),
231 },
232 )
233 .await
234 }
235
236 pub async fn forecast_eps(&self, symbol: impl Into<String>) -> Result<ForecastEps> {
242 #[derive(Serialize)]
243 struct Query {
244 counter_id: String,
245 }
246 self.get(
247 "/v1/quote/forecast-eps",
248 Query {
249 counter_id: symbol_to_counter_id(&symbol.into()),
250 },
251 )
252 .await
253 }
254
255 pub async fn consensus(&self, symbol: impl Into<String>) -> Result<FinancialConsensus> {
261 #[derive(Serialize)]
262 struct Query {
263 counter_id: String,
264 }
265 self.get(
266 "/v1/quote/financial-consensus-detail",
267 Query {
268 counter_id: symbol_to_counter_id(&symbol.into()),
269 },
270 )
271 .await
272 }
273
274 pub async fn valuation(&self, symbol: impl Into<String>) -> Result<ValuationData> {
280 #[derive(Serialize)]
281 struct Query {
282 counter_id: String,
283 indicator: &'static str,
284 range: &'static str,
285 }
286 self.get(
287 "/v1/quote/valuation",
288 Query {
289 counter_id: symbol_to_counter_id(&symbol.into()),
290 indicator: "pe",
291 range: "1",
292 },
293 )
294 .await
295 }
296
297 pub async fn valuation_history(
301 &self,
302 symbol: impl Into<String>,
303 ) -> Result<ValuationHistoryResponse> {
304 #[derive(Serialize)]
305 struct Query {
306 counter_id: String,
307 }
308 self.get(
309 "/v1/quote/valuation/detail",
310 Query {
311 counter_id: symbol_to_counter_id(&symbol.into()),
312 },
313 )
314 .await
315 }
316
317 pub async fn industry_valuation(
323 &self,
324 symbol: impl Into<String>,
325 ) -> Result<IndustryValuationList> {
326 #[derive(Serialize)]
327 struct Query {
328 counter_id: String,
329 }
330 self.get(
331 "/v1/quote/industry-valuation-comparison",
332 Query {
333 counter_id: symbol_to_counter_id(&symbol.into()),
334 },
335 )
336 .await
337 }
338
339 pub async fn industry_valuation_dist(
343 &self,
344 symbol: impl Into<String>,
345 ) -> Result<IndustryValuationDist> {
346 #[derive(Serialize)]
347 struct Query {
348 counter_id: String,
349 }
350 self.get(
351 "/v1/quote/industry-valuation-distribution",
352 Query {
353 counter_id: symbol_to_counter_id(&symbol.into()),
354 },
355 )
356 .await
357 }
358
359 pub async fn company(&self, symbol: impl Into<String>) -> Result<CompanyOverview> {
365 #[derive(Serialize)]
366 struct Query {
367 counter_id: String,
368 }
369 self.get(
370 "/v1/quote/comp-overview",
371 Query {
372 counter_id: symbol_to_counter_id(&symbol.into()),
373 },
374 )
375 .await
376 }
377
378 pub async fn executive(&self, symbol: impl Into<String>) -> Result<ExecutiveList> {
384 #[derive(Serialize)]
385 struct Query {
386 counter_ids: String,
387 }
388 self.get(
389 "/v1/quote/company-professionals",
390 Query {
391 counter_ids: symbol_to_counter_id(&symbol.into()),
392 },
393 )
394 .await
395 }
396
397 pub async fn shareholder(&self, symbol: impl Into<String>) -> Result<ShareholderList> {
403 #[derive(Serialize)]
404 struct Query {
405 counter_id: String,
406 }
407 self.get(
408 "/v1/quote/shareholders",
409 Query {
410 counter_id: symbol_to_counter_id(&symbol.into()),
411 },
412 )
413 .await
414 }
415
416 pub async fn fund_holder(&self, symbol: impl Into<String>) -> Result<FundHolders> {
422 #[derive(Serialize)]
423 struct Query {
424 counter_id: String,
425 }
426 self.get(
427 "/v1/quote/fund-holders",
428 Query {
429 counter_id: symbol_to_counter_id(&symbol.into()),
430 },
431 )
432 .await
433 }
434
435 pub async fn corp_action(&self, symbol: impl Into<String>) -> Result<CorpActions> {
441 #[derive(Serialize)]
442 struct Query {
443 counter_id: String,
444 req_type: &'static str,
445 version: &'static str,
446 }
447 self.get(
448 "/v1/quote/company-act",
449 Query {
450 counter_id: symbol_to_counter_id(&symbol.into()),
451 req_type: "1",
452 version: "3",
453 },
454 )
455 .await
456 }
457
458 pub async fn invest_relation(&self, symbol: impl Into<String>) -> Result<InvestRelations> {
464 #[derive(Serialize)]
465 struct Query {
466 counter_id: String,
467 count: &'static str,
468 }
469 self.get(
470 "/v1/quote/invest-relations",
471 Query {
472 counter_id: symbol_to_counter_id(&symbol.into()),
473 count: "0",
474 },
475 )
476 .await
477 }
478
479 pub async fn operating(&self, symbol: impl Into<String>) -> Result<OperatingList> {
485 #[derive(Serialize)]
486 struct Query {
487 counter_id: String,
488 }
489 self.get_dc(
490 "/v1/quote/operatings",
491 Query {
492 counter_id: symbol_to_counter_id(&symbol.into()),
493 },
494 DcRegion::Ap,
495 )
496 .await
497 }
498
499 pub async fn buyback(&self, symbol: impl Into<String>) -> Result<BuybackData> {
505 #[derive(Serialize)]
506 struct Query {
507 counter_id: String,
508 }
509 self.get(
510 "/v1/quote/buy-backs",
511 Query {
512 counter_id: symbol_to_counter_id(&symbol.into()),
513 },
514 )
515 .await
516 }
517
518 pub async fn ratings(&self, symbol: impl Into<String>) -> Result<StockRatings> {
524 #[derive(Serialize)]
525 struct Query {
526 counter_id: String,
527 }
528 self.get(
529 "/v1/quote/ratings",
530 Query {
531 counter_id: symbol_to_counter_id(&symbol.into()),
532 },
533 )
534 .await
535 }
536
537 pub async fn business_segments(&self, symbol: impl Into<String>) -> Result<BusinessSegments> {
543 #[derive(Serialize)]
544 struct Query {
545 counter_id: String,
546 }
547 self.get(
548 "/v1/quote/fundamentals/business-segments",
549 Query {
550 counter_id: symbol_to_counter_id(&symbol.into()),
551 },
552 )
553 .await
554 }
555
556 pub async fn business_segments_history(
560 &self,
561 symbol: impl Into<String>,
562 report: Option<&'static str>,
563 cate: Option<String>,
564 ) -> Result<BusinessSegmentsHistory> {
565 #[derive(Serialize)]
566 struct Query {
567 counter_id: String,
568 #[serde(skip_serializing_if = "Option::is_none")]
569 report: Option<&'static str>,
570 #[serde(skip_serializing_if = "Option::is_none")]
571 cate: Option<String>,
572 }
573 self.get(
574 "/v1/quote/fundamentals/business-segments/history",
575 Query {
576 counter_id: symbol_to_counter_id(&symbol.into()),
577 report,
578 cate,
579 },
580 )
581 .await
582 }
583
584 pub async fn shareholder_top(
590 &self,
591 symbol: impl Into<String>,
592 ) -> Result<ShareholderTopResponse> {
593 #[derive(Serialize)]
594 struct Query {
595 counter_id: String,
596 }
597 let raw: serde_json::Value = self
598 .get(
599 "/v1/quote/shareholders/top",
600 Query {
601 counter_id: symbol_to_counter_id(&symbol.into()),
602 },
603 )
604 .await?;
605 Ok(ShareholderTopResponse { data: raw })
606 }
607
608 pub async fn institution_rating_views(
614 &self,
615 symbol: impl Into<String>,
616 ) -> Result<InstitutionRatingViews> {
617 #[derive(Serialize)]
618 struct Query {
619 counter_id: String,
620 }
621 self.get(
622 "/v1/quote/ratings/institutional",
623 Query {
624 counter_id: symbol_to_counter_id(&symbol.into()),
625 },
626 )
627 .await
628 }
629
630 pub async fn shareholder_detail(
636 &self,
637 symbol: impl Into<String>,
638 object_id: i64,
639 ) -> Result<ShareholderDetailResponse> {
640 #[derive(Serialize)]
641 struct Query {
642 counter_id: String,
643 object_id: String,
644 }
645 let raw: serde_json::Value = self
646 .get(
647 "/v1/quote/shareholders/holding",
648 Query {
649 counter_id: symbol_to_counter_id(&symbol.into()),
650 object_id: object_id.to_string(),
651 },
652 )
653 .await?;
654 Ok(ShareholderDetailResponse { data: raw })
655 }
656
657 pub async fn industry_rank(
663 &self,
664 market: impl Into<String>,
665 indicator: impl Into<String>,
666 sort_type: impl Into<String>,
667 limit: u32,
668 ) -> Result<IndustryRankResponse> {
669 #[derive(Serialize)]
670 struct Query {
671 market: String,
672 indicator: String,
673 sort_type: String,
674 limit: u32,
675 }
676 self.get(
677 "/v1/quote/industry/rank",
678 Query {
679 market: market.into(),
680 indicator: indicator.into(),
681 sort_type: sort_type.into(),
682 limit,
683 },
684 )
685 .await
686 }
687
688 pub async fn industry_peers(
694 &self,
695 counter_id: impl Into<String>,
696 market: impl Into<String>,
697 industry_id: Option<String>,
698 ) -> Result<IndustryPeersResponse> {
699 let raw = counter_id.into();
700 let cid = if raw.contains('/') {
701 raw
702 } else {
703 symbol_to_counter_id(&raw)
704 };
705 #[derive(Serialize)]
706 struct Query {
707 #[serde(rename = "type")]
708 kind: &'static str,
709 market: String,
710 industry_id: String,
711 counter_id: String,
712 }
713 self.get(
714 "/v1/quote/industries/peers",
715 Query {
716 kind: "1",
717 market: market.into(),
718 industry_id: industry_id.unwrap_or_default(),
719 counter_id: cid,
720 },
721 )
722 .await
723 }
724
725 pub async fn financial_report_snapshot(
731 &self,
732 symbol: impl Into<String>,
733 report: Option<&'static str>,
734 fiscal_year: Option<i32>,
735 fiscal_period: Option<&'static str>,
736 ) -> Result<FinancialReportSnapshot> {
737 #[derive(Serialize)]
738 struct Query {
739 counter_id: String,
740 #[serde(skip_serializing_if = "Option::is_none")]
741 report: Option<&'static str>,
742 #[serde(skip_serializing_if = "Option::is_none")]
743 fiscal_year: Option<i32>,
744 #[serde(skip_serializing_if = "Option::is_none")]
745 fiscal_period: Option<&'static str>,
746 }
747 self.get(
748 "/v1/quote/financials/earnings-snapshot",
749 Query {
750 counter_id: symbol_to_counter_id(&symbol.into()),
751 report,
752 fiscal_year,
753 fiscal_period,
754 },
755 )
756 .await
757 }
758
759 pub async fn valuation_comparison(
765 &self,
766 symbol: impl Into<String>,
767 currency: impl Into<String>,
768 comparison_symbols: Option<Vec<String>>,
769 ) -> Result<ValuationComparisonResponse> {
770 #[derive(Serialize)]
771 struct Query {
772 counter_id: String,
773 currency: String,
774 #[serde(skip_serializing_if = "Option::is_none")]
775 comparison_counter_ids: Option<String>,
776 }
777 let comparison_counter_ids = comparison_symbols.map(|syms| {
778 let ids: Vec<String> = syms.iter().map(|s| symbol_to_counter_id(s)).collect();
779 serde_json::to_string(&ids).unwrap_or_default()
780 });
781 let raw: serde_json::Value = self
782 .get(
783 "/v1/quote/compare/valuation",
784 Query {
785 counter_id: symbol_to_counter_id(&symbol.into()),
786 currency: currency.into(),
787 comparison_counter_ids,
788 },
789 )
790 .await?;
791 let list = raw["list"]
792 .as_array()
793 .cloned()
794 .unwrap_or_default()
795 .into_iter()
796 .map(|item| {
797 let history = item["history"]
798 .as_array()
799 .cloned()
800 .unwrap_or_default()
801 .into_iter()
802 .map(|h| ValuationHistoryPoint {
803 date: unix_secs_str_to_rfc3339(h["date"].as_str().unwrap_or("")),
804 pe: h["pe"].as_str().unwrap_or("").to_string(),
805 pb: h["pb"].as_str().unwrap_or("").to_string(),
806 ps: h["ps"].as_str().unwrap_or("").to_string(),
807 })
808 .collect();
809 ValuationComparisonItem {
810 symbol: counter_id_to_symbol(item["counter_id"].as_str().unwrap_or("")),
811 name: item["name"].as_str().unwrap_or("").to_string(),
812 currency: item["currency"].as_str().unwrap_or("").to_string(),
813 market_value: item["market_value"].as_str().unwrap_or("").to_string(),
814 price_close: item["price_close"].as_str().unwrap_or("").to_string(),
815 pe: item["pe"].as_str().unwrap_or("").to_string(),
816 pb: item["pb"].as_str().unwrap_or("").to_string(),
817 ps: item["ps"].as_str().unwrap_or("").to_string(),
818 roe: item["roe"].as_str().unwrap_or("").to_string(),
819 eps: item["eps"].as_str().unwrap_or("").to_string(),
820 bps: item["bps"].as_str().unwrap_or("").to_string(),
821 dps: item["dps"].as_str().unwrap_or("").to_string(),
822 div_yld: item["div_yld"].as_str().unwrap_or("").to_string(),
823 assets: item["assets"].as_str().unwrap_or("").to_string(),
824 history,
825 }
826 })
827 .collect();
828 Ok(ValuationComparisonResponse { list })
829 }
830
831 pub async fn etf_asset_allocation(
838 &self,
839 symbol: impl Into<String>,
840 ) -> Result<AssetAllocationResponse> {
841 #[derive(Serialize)]
842 struct Query {
843 counter_id: String,
844 }
845 self.get(
846 "/v1/quote/etf-asset-allocation",
847 Query {
848 counter_id: symbol_to_counter_id(&symbol.into()),
849 },
850 )
851 .await
852 }
853
854 pub async fn macroeconomic_indicators(
865 &self,
866 country: Option<MacroeconomicCountry>,
867 keyword: Option<impl Into<String>>,
868 offset: Option<i32>,
869 limit: Option<i32>,
870 ) -> Result<MacroeconomicIndicatorListResponse> {
871 self.macroeconomic_indicators_v2(country, keyword, offset, limit)
872 .await
873 }
874
875 pub(crate) async fn macroeconomic_indicators_v2(
879 &self,
880 country: Option<MacroeconomicCountry>,
881 keyword: Option<impl Into<String>>,
882 offset: Option<i32>,
883 limit: Option<i32>,
884 ) -> Result<MacroeconomicIndicatorListResponse> {
885 #[derive(Serialize)]
886 struct Query {
887 market: String,
888 #[serde(skip_serializing_if = "Option::is_none")]
889 keyword: Option<String>,
890 #[serde(skip_serializing_if = "Option::is_none")]
891 offset: Option<i32>,
892 #[serde(skip_serializing_if = "Option::is_none")]
893 limit: Option<i32>,
894 }
895 let market = country
896 .map(|c| match c {
897 MacroeconomicCountry::HongKong => "HK",
898 MacroeconomicCountry::China => "CN",
899 MacroeconomicCountry::UnitedStates => "US",
900 MacroeconomicCountry::EuroZone => "EU",
901 MacroeconomicCountry::Japan => "JP",
902 MacroeconomicCountry::Singapore => "SG",
903 })
904 .unwrap_or("ALL")
905 .to_string();
906
907 let raw: V2MacroIndicatorListResponse = self
908 .get(
909 "/v2/quote/macrodata",
910 Query {
911 market,
912 keyword: keyword.map(|k| k.into()),
913 offset,
914 limit,
915 },
916 )
917 .await?;
918
919 let total = raw.total;
920 let data = raw
921 .indicator_list
922 .into_iter()
923 .map(|ind| MacroeconomicIndicator {
924 indicator_code: ind.indicator_id.to_string(),
925 country: ind.market,
926 name: ind.indicator_name,
927 periodicity: ind.frequence,
928 describe: ind.description,
929 importance: ind.importance,
930 ..Default::default()
931 })
932 .collect::<Vec<_>>();
933 let count = if total > 0 { total } else { data.len() as i32 };
934 Ok(MacroeconomicIndicatorListResponse { data, count })
935 }
936
937 pub async fn macroeconomic(
945 &self,
946 indicator_code: impl Into<String>,
947 start_date: Option<impl Into<String>>,
948 end_date: Option<impl Into<String>>,
949 offset: Option<i32>,
950 limit: Option<i32>,
951 ) -> Result<MacroeconomicResponse> {
952 self.macroeconomic_v2(
953 indicator_code,
954 start_date,
955 end_date,
956 offset,
957 limit,
958 None::<String>,
959 )
960 .await
961 }
962
963 pub(crate) async fn macroeconomic_v2(
968 &self,
969 indicator_code: impl Into<String>,
970 start_date: Option<impl Into<String>>,
971 end_date: Option<impl Into<String>>,
972 offset: Option<i32>,
973 limit: Option<i32>,
974 sort: Option<impl Into<String>>,
975 ) -> Result<MacroeconomicResponse> {
976 #[derive(Serialize)]
977 struct Query {
978 #[serde(skip_serializing_if = "Option::is_none")]
979 start_date: Option<String>,
980 #[serde(skip_serializing_if = "Option::is_none")]
981 end_date: Option<String>,
982 #[serde(skip_serializing_if = "Option::is_none")]
983 offset: Option<i32>,
984 #[serde(skip_serializing_if = "Option::is_none")]
985 limit: Option<i32>,
986 #[serde(skip_serializing_if = "Option::is_none")]
987 sort: Option<String>,
988 }
989 let path = format!("/v2/quote/macrodata/{}", indicator_code.into());
990 let raw: V2MacroIndicatorDataResponse = self
991 .0
992 .http_cli
993 .request(Method::GET, path)
994 .query_params(Query {
995 start_date: start_date.map(|d| d.into()),
996 end_date: end_date.map(|d| d.into()),
997 offset,
998 limit,
999 sort: Some(sort.map(|s| s.into()).unwrap_or_else(|| "desc".to_string())),
1000 })
1001 .response::<Json<V2MacroIndicatorDataResponse>>()
1002 .send()
1003 .with_subscriber(self.0.log_subscriber.clone())
1004 .await?
1005 .0;
1006
1007 let total = raw.total;
1008 let detail = raw.indicator;
1009 let unit_english = detail.unit.clone();
1010 let count = detail.indicator_data.len() as i32;
1011
1012 let info = MacroeconomicIndicator {
1013 indicator_code: detail.indicator_id.to_string(),
1014 country: detail.market,
1015 name: detail.indicator_name,
1016 describe: detail.description,
1017 periodicity: detail.frequence,
1018 importance: detail.importance,
1019 ..Default::default()
1020 };
1021
1022 let data = detail
1023 .indicator_data
1024 .into_iter()
1025 .map(|d| {
1026 use time::format_description::well_known::Rfc3339;
1027 let release_at = time::OffsetDateTime::parse(&d.published_time, &Rfc3339)
1028 .ok()
1029 .or_else(|| {
1030 time::PrimitiveDateTime::parse(
1032 &d.published_time,
1033 &time::macros::format_description!(
1034 "[year]-[month]-[day]T[hour]:[minute]:[second]"
1035 ),
1036 )
1037 .ok()
1038 .map(|dt| dt.assume_utc())
1039 });
1040 Macroeconomic {
1041 period: d.observation_date,
1042 release_at,
1043 actual_value: d.actual_data,
1044 previous_value: d.previous_data,
1045 forecast_value: d.estimated_data,
1046 unit: unit_english.clone(),
1047 ..Default::default()
1048 }
1049 })
1050 .collect();
1051
1052 let count = if total > 0 { total } else { count };
1053 Ok(MacroeconomicResponse { info, data, count })
1054 }
1055
1056 pub async fn us_company_overview(
1066 &self,
1067 symbol: impl Into<String>,
1068 ) -> Result<USCompanyOverview> {
1069 #[derive(Serialize)]
1070 struct Query {
1071 counter_id: String,
1072 }
1073 self.get_dc(
1074 "/v1/us/stock-info/company-overview",
1075 Query {
1076 counter_id: symbol_to_counter_id(&symbol.into()),
1077 },
1078 DcRegion::Us,
1079 )
1080 .await
1081 }
1082
1083 pub async fn us_valuation_overview(
1089 &self,
1090 symbol: impl Into<String>,
1091 ) -> Result<USValuationOverview> {
1092 #[derive(Serialize)]
1093 struct Query {
1094 counter_id: String,
1095 }
1096 self.get_dc(
1097 "/v1/us/stock-info/valuation-overview",
1098 Query {
1099 counter_id: symbol_to_counter_id(&symbol.into()),
1100 },
1101 DcRegion::Us,
1102 )
1103 .await
1104 }
1105
1106 pub async fn us_financial_overview(
1114 &self,
1115 symbol: impl Into<String>,
1116 report: impl Into<String>,
1117 ) -> Result<USFinancialOverview> {
1118 #[derive(Serialize)]
1119 struct Query {
1120 counter_id: String,
1121 report: String,
1122 }
1123 self.get_dc(
1124 "/v1/us/stock-info/finn-overview",
1125 Query {
1126 counter_id: symbol_to_counter_id(&symbol.into()),
1127 report: report.into(),
1128 },
1129 DcRegion::Us,
1130 )
1131 .await
1132 }
1133
1134 pub async fn us_financial_statement(
1143 &self,
1144 symbol: impl Into<String>,
1145 kind: impl Into<String>,
1146 report: impl Into<String>,
1147 ) -> Result<USFinancialStatement> {
1148 #[derive(Serialize)]
1149 struct Query {
1150 counter_id: String,
1151 kind: String,
1152 report: String,
1153 }
1154 self.get_dc(
1155 "/v1/us/quote/financials/statements",
1156 Query {
1157 counter_id: symbol_to_counter_id(&symbol.into()),
1158 kind: kind.into(),
1159 report: report.into(),
1160 },
1161 DcRegion::Us,
1162 )
1163 .await
1164 }
1165
1166 pub async fn us_key_financial_metrics(
1174 &self,
1175 symbol: impl Into<String>,
1176 report: impl Into<String>,
1177 ) -> Result<USKeyFinancialMetrics> {
1178 #[derive(Serialize)]
1179 struct Query {
1180 counter_id: String,
1181 report: String,
1182 }
1183 self.get_dc(
1184 "/v1/us/stock-info/fin-keyfactor",
1185 Query {
1186 counter_id: symbol_to_counter_id(&symbol.into()),
1187 report: report.into(),
1188 },
1189 DcRegion::Us,
1190 )
1191 .await
1192 }
1193
1194 pub async fn us_analyst_consensus(
1202 &self,
1203 symbol: impl Into<String>,
1204 report: impl Into<String>,
1205 ) -> Result<USAnalystConsensus> {
1206 #[derive(Serialize)]
1207 struct Query {
1208 counter_id: String,
1209 report: String,
1210 }
1211 self.get_dc(
1212 "/v1/us/stock-info/fin-consensus",
1213 Query {
1214 counter_id: symbol_to_counter_id(&symbol.into()),
1215 report: report.into(),
1216 },
1217 DcRegion::Us,
1218 )
1219 .await
1220 }
1221
1222 pub async fn us_etf_dividend_info(
1228 &self,
1229 symbol: impl Into<String>,
1230 ) -> Result<USETFDividendInfo> {
1231 #[derive(Serialize)]
1232 struct Query {
1233 counter_id: String,
1234 }
1235 self.get_dc(
1236 "/v1/us/stock-info/etf-dividend-info",
1237 Query {
1238 counter_id: symbol_to_counter_id(&symbol.into()),
1239 },
1240 DcRegion::Us,
1241 )
1242 .await
1243 }
1244
1245 pub async fn us_company_dividends(
1251 &self,
1252 symbol: impl Into<String>,
1253 ) -> Result<USCompanyDividends> {
1254 #[derive(Serialize)]
1255 struct Query {
1256 counter_id: String,
1257 }
1258 self.get_dc(
1259 "/v1/us/stock-info/company-dividends",
1260 Query {
1261 counter_id: symbol_to_counter_id(&symbol.into()),
1262 },
1263 DcRegion::Us,
1264 )
1265 .await
1266 }
1267
1268 pub async fn us_etf_files(
1277 &self,
1278 symbol: impl Into<String>,
1279 size: Option<u32>,
1280 ) -> Result<USETFFilesResponse> {
1281 #[derive(Serialize)]
1282 struct Query {
1283 counter_id: String,
1284 #[serde(skip_serializing_if = "Option::is_none")]
1285 size: Option<u32>,
1286 }
1287 self.get_dc(
1288 "/v1/us/stock-info/etf-files",
1289 Query {
1290 counter_id: symbol_to_counter_id(&symbol.into()),
1291 size,
1292 },
1293 DcRegion::Us,
1294 )
1295 .await
1296 }
1297}