Skip to main content

longbridge/fundamental/
context.rs

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/// Fundamental data context — financial reports, analyst ratings, dividends,
23/// valuation, company overview and more.
24#[derive(Clone)]
25pub struct FundamentalContext(Arc<InnerFundamentalContext>);
26
27impl FundamentalContext {
28    /// Create a [`FundamentalContext`]
29    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    /// Returns the log subscriber
45    #[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    // ── financial_report ─────────────────────────────────────────
68
69    /// Get financial reports for a security.
70    ///
71    /// Path: `GET /v1/quote/financial-reports`
72    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    // ── institution_rating ────────────────────────────────────────
112
113    /// Get analyst ratings for a security (combines latest + historical).
114    ///
115    /// Path: `GET /v1/quote/institution-rating-latest` +
116    ///       `GET /v1/quote/institution-ratings`
117    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    /// Get historical analyst rating details for a security.
145    ///
146    /// Path: `GET /v1/quote/institution-ratings/detail`
147    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    // ── dividend ──────────────────────────────────────────────────
165
166    /// Get dividend history for a security.
167    ///
168    /// Path: `GET /v1/quote/dividends`
169    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    /// Get detailed dividend information for a security.
184    ///
185    /// Path: `GET /v1/quote/dividends/details`
186    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    // ── forecast_eps ──────────────────────────────────────────────
201
202    /// Get EPS forecasts for a security.
203    ///
204    /// Path: `GET /v1/quote/forecast-eps`
205    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    // ── consensus ─────────────────────────────────────────────────
220
221    /// Get financial consensus estimates for a security.
222    ///
223    /// Path: `GET /v1/quote/financial-consensus-detail`
224    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    // ── valuation ─────────────────────────────────────────────────
239
240    /// Get valuation metrics (PE/PB/PS/dividend yield) for a security.
241    ///
242    /// Path: `GET /v1/quote/valuation`
243    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    /// Get historical valuation data for a security.
262    ///
263    /// Path: `GET /v1/quote/valuation/detail`
264    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    // ── industry_valuation ────────────────────────────────────────
282
283    /// Get valuation comparison against industry peers.
284    ///
285    /// Path: `GET /v1/quote/industry-valuation-comparison`
286    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    /// Get valuation distribution within the industry.
304    ///
305    /// Path: `GET /v1/quote/industry-valuation-distribution`
306    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    // ── company ───────────────────────────────────────────────────
324
325    /// Get company overview information.
326    ///
327    /// Path: `GET /v1/quote/comp-overview`
328    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    // ── executive ─────────────────────────────────────────────────
343
344    /// Get executive and board member information.
345    ///
346    /// Path: `GET /v1/quote/company-professionals`
347    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    // ── shareholder ───────────────────────────────────────────────
362
363    /// Get major shareholders for a security.
364    ///
365    /// Path: `GET /v1/quote/shareholders`
366    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    // ── fund_holder ───────────────────────────────────────────────
381
382    /// Get funds and ETFs that hold a security.
383    ///
384    /// Path: `GET /v1/quote/fund-holders`
385    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    // ── corp_action ───────────────────────────────────────────────
400
401    /// Get corporate actions (dividends, splits, buybacks, etc.).
402    ///
403    /// Path: `GET /v1/quote/company-act`
404    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    // ── invest_relation ───────────────────────────────────────────
423
424    /// Get investor relations / investment holdings.
425    ///
426    /// Path: `GET /v1/quote/invest-relations`
427    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    // ── operating ─────────────────────────────────────────────────
444
445    /// Get operating metrics and financial report summaries.
446    ///
447    /// Path: `GET /v1/quote/operatings`
448    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    // ── buyback ───────────────────────────────────────────────────
463
464    /// Get buyback data for a security.
465    ///
466    /// Path: `GET /v1/quote/buy-backs`
467    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    // ── ratings ───────────────────────────────────────────────────
482
483    /// Get stock ratings for a security.
484    ///
485    /// Path: `GET /v1/quote/ratings`
486    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}