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::{Config, Result, fundamental::types::*, utils::counter::symbol_to_counter_id};
8
9struct InnerFundamentalContext {
10 http_cli: HttpClient,
11 log_subscriber: Arc<dyn Subscriber + Send + Sync>,
12}
13
14impl Drop for InnerFundamentalContext {
15 fn drop(&mut self) {
16 dispatcher::with_default(&self.log_subscriber.clone().into(), || {
17 tracing::info!("fundamental context dropped");
18 });
19 }
20}
21
22#[derive(Clone)]
25pub struct FundamentalContext(Arc<InnerFundamentalContext>);
26
27impl FundamentalContext {
28 pub fn new(config: Arc<Config>) -> Self {
30 let log_subscriber = config.create_log_subscriber("fundamental");
31 dispatcher::with_default(&log_subscriber.clone().into(), || {
32 tracing::info!(language = ?config.language, "creating fundamental context");
33 });
34 let ctx = Self(Arc::new(InnerFundamentalContext {
35 http_cli: config.create_http_client(),
36 log_subscriber,
37 }));
38 dispatcher::with_default(&ctx.0.log_subscriber.clone().into(), || {
39 tracing::info!("fundamental context created");
40 });
41 ctx
42 }
43
44 #[inline]
46 pub fn log_subscriber(&self) -> Arc<dyn Subscriber + Send + Sync> {
47 self.0.log_subscriber.clone()
48 }
49
50 async fn get<R, Q>(&self, path: &'static str, query: Q) -> Result<R>
51 where
52 R: DeserializeOwned + Send + Sync + 'static,
53 Q: Serialize + Send + Sync,
54 {
55 Ok(self
56 .0
57 .http_cli
58 .request(Method::GET, path)
59 .query_params(query)
60 .response::<Json<R>>()
61 .send()
62 .with_subscriber(self.0.log_subscriber.clone())
63 .await?
64 .0)
65 }
66
67 pub async fn financial_report(
73 &self,
74 symbol: impl Into<String>,
75 kind: FinancialReportKind,
76 period: Option<FinancialReportPeriod>,
77 ) -> Result<FinancialReports> {
78 let kind_str = match kind {
79 FinancialReportKind::IncomeStatement => "IS",
80 FinancialReportKind::BalanceSheet => "BS",
81 FinancialReportKind::CashFlow => "CF",
82 FinancialReportKind::All => "ALL",
83 };
84 let period_str = period.map(|p| match p {
85 FinancialReportPeriod::Annual => "af",
86 FinancialReportPeriod::SemiAnnual => "saf",
87 FinancialReportPeriod::Q1 => "q1",
88 FinancialReportPeriod::Q2 => "q2",
89 FinancialReportPeriod::Q3 => "q3",
90 FinancialReportPeriod::QuarterlyFull => "qf",
91 FinancialReportPeriod::ThreeQ => "3q",
92 });
93 #[derive(Serialize)]
94 struct Query {
95 counter_id: String,
96 kind: &'static str,
97 #[serde(skip_serializing_if = "Option::is_none")]
98 report: Option<&'static str>,
99 }
100 self.get(
101 "/v1/quote/financial-reports",
102 Query {
103 counter_id: symbol_to_counter_id(&symbol.into()),
104 kind: kind_str,
105 report: period_str,
106 },
107 )
108 .await
109 }
110
111 pub async fn institution_rating(&self, symbol: impl Into<String>) -> Result<InstitutionRating> {
118 #[derive(Serialize)]
119 struct Query {
120 counter_id: String,
121 }
122 let cid = symbol_to_counter_id(&symbol.into());
123 let q = Query { counter_id: cid };
124 let (latest, summary) = tokio::join!(
125 self.get::<InstitutionRatingLatest, _>(
126 "/v1/quote/institution-rating-latest",
127 Query {
128 counter_id: q.counter_id.clone()
129 }
130 ),
131 self.get::<InstitutionRatingSummary, _>(
132 "/v1/quote/institution-ratings",
133 Query {
134 counter_id: q.counter_id.clone()
135 }
136 ),
137 );
138 Ok(InstitutionRating {
139 latest: latest?,
140 summary: summary?,
141 })
142 }
143
144 pub async fn institution_rating_detail(
148 &self,
149 symbol: impl Into<String>,
150 ) -> Result<InstitutionRatingDetail> {
151 #[derive(Serialize)]
152 struct Query {
153 counter_id: String,
154 }
155 self.get(
156 "/v1/quote/institution-ratings/detail",
157 Query {
158 counter_id: symbol_to_counter_id(&symbol.into()),
159 },
160 )
161 .await
162 }
163
164 pub async fn dividend(&self, symbol: impl Into<String>) -> Result<DividendList> {
170 #[derive(Serialize)]
171 struct Query {
172 counter_id: String,
173 }
174 self.get(
175 "/v1/quote/dividends",
176 Query {
177 counter_id: symbol_to_counter_id(&symbol.into()),
178 },
179 )
180 .await
181 }
182
183 pub async fn dividend_detail(&self, symbol: impl Into<String>) -> Result<DividendList> {
187 #[derive(Serialize)]
188 struct Query {
189 counter_id: String,
190 }
191 self.get(
192 "/v1/quote/dividends/details",
193 Query {
194 counter_id: symbol_to_counter_id(&symbol.into()),
195 },
196 )
197 .await
198 }
199
200 pub async fn forecast_eps(&self, symbol: impl Into<String>) -> Result<ForecastEps> {
206 #[derive(Serialize)]
207 struct Query {
208 counter_id: String,
209 }
210 self.get(
211 "/v1/quote/forecast-eps",
212 Query {
213 counter_id: symbol_to_counter_id(&symbol.into()),
214 },
215 )
216 .await
217 }
218
219 pub async fn consensus(&self, symbol: impl Into<String>) -> Result<FinancialConsensus> {
225 #[derive(Serialize)]
226 struct Query {
227 counter_id: String,
228 }
229 self.get(
230 "/v1/quote/financial-consensus-detail",
231 Query {
232 counter_id: symbol_to_counter_id(&symbol.into()),
233 },
234 )
235 .await
236 }
237
238 pub async fn valuation(&self, symbol: impl Into<String>) -> Result<ValuationData> {
244 #[derive(Serialize)]
245 struct Query {
246 counter_id: String,
247 indicator: &'static str,
248 range: &'static str,
249 }
250 self.get(
251 "/v1/quote/valuation",
252 Query {
253 counter_id: symbol_to_counter_id(&symbol.into()),
254 indicator: "pe",
255 range: "1",
256 },
257 )
258 .await
259 }
260
261 pub async fn valuation_history(
265 &self,
266 symbol: impl Into<String>,
267 ) -> Result<ValuationHistoryResponse> {
268 #[derive(Serialize)]
269 struct Query {
270 counter_id: String,
271 }
272 self.get(
273 "/v1/quote/valuation/detail",
274 Query {
275 counter_id: symbol_to_counter_id(&symbol.into()),
276 },
277 )
278 .await
279 }
280
281 pub async fn industry_valuation(
287 &self,
288 symbol: impl Into<String>,
289 ) -> Result<IndustryValuationList> {
290 #[derive(Serialize)]
291 struct Query {
292 counter_id: String,
293 }
294 self.get(
295 "/v1/quote/industry-valuation-comparison",
296 Query {
297 counter_id: symbol_to_counter_id(&symbol.into()),
298 },
299 )
300 .await
301 }
302
303 pub async fn industry_valuation_dist(
307 &self,
308 symbol: impl Into<String>,
309 ) -> Result<IndustryValuationDist> {
310 #[derive(Serialize)]
311 struct Query {
312 counter_id: String,
313 }
314 self.get(
315 "/v1/quote/industry-valuation-distribution",
316 Query {
317 counter_id: symbol_to_counter_id(&symbol.into()),
318 },
319 )
320 .await
321 }
322
323 pub async fn company(&self, symbol: impl Into<String>) -> Result<CompanyOverview> {
329 #[derive(Serialize)]
330 struct Query {
331 counter_id: String,
332 }
333 self.get(
334 "/v1/quote/comp-overview",
335 Query {
336 counter_id: symbol_to_counter_id(&symbol.into()),
337 },
338 )
339 .await
340 }
341
342 pub async fn executive(&self, symbol: impl Into<String>) -> Result<ExecutiveList> {
348 #[derive(Serialize)]
349 struct Query {
350 counter_ids: String,
351 }
352 self.get(
353 "/v1/quote/company-professionals",
354 Query {
355 counter_ids: symbol_to_counter_id(&symbol.into()),
356 },
357 )
358 .await
359 }
360
361 pub async fn shareholder(&self, symbol: impl Into<String>) -> Result<ShareholderList> {
367 #[derive(Serialize)]
368 struct Query {
369 counter_id: String,
370 }
371 self.get(
372 "/v1/quote/shareholders",
373 Query {
374 counter_id: symbol_to_counter_id(&symbol.into()),
375 },
376 )
377 .await
378 }
379
380 pub async fn fund_holder(&self, symbol: impl Into<String>) -> Result<FundHolders> {
386 #[derive(Serialize)]
387 struct Query {
388 counter_id: String,
389 }
390 self.get(
391 "/v1/quote/fund-holders",
392 Query {
393 counter_id: symbol_to_counter_id(&symbol.into()),
394 },
395 )
396 .await
397 }
398
399 pub async fn corp_action(&self, symbol: impl Into<String>) -> Result<CorpActions> {
405 #[derive(Serialize)]
406 struct Query {
407 counter_id: String,
408 req_type: &'static str,
409 version: &'static str,
410 }
411 self.get(
412 "/v1/quote/company-act",
413 Query {
414 counter_id: symbol_to_counter_id(&symbol.into()),
415 req_type: "1",
416 version: "3",
417 },
418 )
419 .await
420 }
421
422 pub async fn invest_relation(&self, symbol: impl Into<String>) -> Result<InvestRelations> {
428 #[derive(Serialize)]
429 struct Query {
430 counter_id: String,
431 count: &'static str,
432 }
433 self.get(
434 "/v1/quote/invest-relations",
435 Query {
436 counter_id: symbol_to_counter_id(&symbol.into()),
437 count: "0",
438 },
439 )
440 .await
441 }
442
443 pub async fn operating(&self, symbol: impl Into<String>) -> Result<OperatingList> {
449 #[derive(Serialize)]
450 struct Query {
451 counter_id: String,
452 }
453 self.get(
454 "/v1/quote/operatings",
455 Query {
456 counter_id: symbol_to_counter_id(&symbol.into()),
457 },
458 )
459 .await
460 }
461
462 pub async fn buyback(&self, symbol: impl Into<String>) -> Result<BuybackData> {
468 #[derive(Serialize)]
469 struct Query {
470 counter_id: String,
471 }
472 self.get(
473 "/v1/quote/buy-backs",
474 Query {
475 counter_id: symbol_to_counter_id(&symbol.into()),
476 },
477 )
478 .await
479 }
480
481 pub async fn ratings(&self, symbol: impl Into<String>) -> Result<StockRatings> {
487 #[derive(Serialize)]
488 struct Query {
489 counter_id: String,
490 }
491 self.get(
492 "/v1/quote/ratings",
493 Query {
494 counter_id: symbol_to_counter_id(&symbol.into()),
495 },
496 )
497 .await
498 }
499}