From 558b19294c90ae1de9f5a5b612649f3daa4cd2d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A2=81=E7=AB=A0=E6=B4=AA?= Date: Fri, 15 May 2026 19:28:49 +0800 Subject: [PATCH] feat(fundamental): add business-segments, institution-rating-views, industry-rank, industry-peers, financial-report-snapshot APIs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ported from longbridge-terminal PR #202: - business_segments / business_segments_history — GET /v1/quote/fundamentals/business-segments[/history] - institution_rating_views — GET /v1/quote/ratings/institutional - industry_rank — GET /v1/quote/industry/rank - industry_peers — GET /v1/quote/industries/peers - financial_report_snapshot — GET /v1/quote/financials/earnings-snapshot Includes types, async context methods, and blocking wrappers. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- CHANGELOG.md | 13 ++ rust/src/blocking/fundamental.rs | 69 +++++++++ rust/src/fundamental/context.rs | 178 +++++++++++++++++++++++ rust/src/fundamental/types.rs | 240 +++++++++++++++++++++++++++++++ 4 files changed, 500 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 13a1666f3..458d650f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,19 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Added + +- **Rust:** Six new `FundamentalContext` methods: + - `business_segments` — GET `/v1/quote/fundamentals/business-segments`: latest business segment breakdown (name, percent). + - `business_segments_history` — GET `/v1/quote/fundamentals/business-segments/history`: historical business and regional segment breakdowns with optional `report` and `cate` filters. + - `institution_rating_views` — GET `/v1/quote/ratings/institutional`: historical rating distribution time-series (buy/over/hold/under/sell/total per date). + - `industry_rank` — GET `/v1/quote/industry/rank`: industry leaderboard for a market with configurable indicator and sort direction. + - `industry_peers` — GET `/v1/quote/industries/peers`: recursive industry peer chain; accepts both symbol-style (`AAPL.US`) and raw counter IDs (`BK/US/123`). + - `financial_report_snapshot` — GET `/v1/quote/financials/earnings-snapshot`: earnings snapshot with forecast (revenue/EBIT/EPS) and reported (P&L, cash-flows, balance-sheet ratios) metrics. +- All new methods are also available on `FundamentalContextSync` (blocking API). + # [4.1.0] ## Breaking changes diff --git a/rust/src/blocking/fundamental.rs b/rust/src/blocking/fundamental.rs index c2e674432..b0e539c51 100644 --- a/rust/src/blocking/fundamental.rs +++ b/rust/src/blocking/fundamental.rs @@ -179,4 +179,73 @@ impl FundamentalContextSync { self.rt .call(move |ctx| async move { ctx.ratings(symbol).await }) } + + /// Get latest business segment breakdown + pub fn business_segments( + &self, + symbol: impl Into + Send + 'static, + ) -> Result { + self.rt + .call(move |ctx| async move { ctx.business_segments(symbol).await }) + } + + /// Get historical business segment breakdowns + pub fn business_segments_history( + &self, + symbol: impl Into + Send + 'static, + report: Option<&'static str>, + cate: Option, + ) -> Result { + self.rt.call( + move |ctx| async move { ctx.business_segments_history(symbol, report, cate).await }, + ) + } + + /// Get historical institutional rating views + pub fn institution_rating_views( + &self, + symbol: impl Into + Send + 'static, + ) -> Result { + self.rt + .call(move |ctx| async move { ctx.institution_rating_views(symbol).await }) + } + + /// Get industry rank for a market + pub fn industry_rank( + &self, + market: impl Into + Send + 'static, + indicator: impl Into + Send + 'static, + sort_type: impl Into + Send + 'static, + limit: u32, + ) -> Result { + self.rt.call(move |ctx| async move { + ctx.industry_rank(market, indicator, sort_type, limit).await + }) + } + + /// Get industry peer chain + pub fn industry_peers( + &self, + counter_id: impl Into + Send + 'static, + market: impl Into + Send + 'static, + industry_id: Option, + ) -> Result { + self.rt.call( + move |ctx| async move { ctx.industry_peers(counter_id, market, industry_id).await }, + ) + } + + /// Get financial report snapshot (earnings snapshot) + pub fn financial_report_snapshot( + &self, + symbol: impl Into + Send + 'static, + report: Option<&'static str>, + fiscal_year: Option, + fiscal_period: Option<&'static str>, + ) -> Result { + self.rt.call(move |ctx| async move { + ctx.financial_report_snapshot(symbol, report, fiscal_year, fiscal_period) + .await + }) + } } diff --git a/rust/src/fundamental/context.rs b/rust/src/fundamental/context.rs index 015d3c992..a71fffbd0 100644 --- a/rust/src/fundamental/context.rs +++ b/rust/src/fundamental/context.rs @@ -496,4 +496,182 @@ impl FundamentalContext { ) .await } + + // ── business_segments ──────────────────────────────────────── + + /// Get the latest business segment breakdown for a security. + /// + /// Path: `GET /v1/quote/fundamentals/business-segments` + pub async fn business_segments(&self, symbol: impl Into) -> Result { + #[derive(Serialize)] + struct Query { + counter_id: String, + } + self.get( + "/v1/quote/fundamentals/business-segments", + Query { + counter_id: symbol_to_counter_id(&symbol.into()), + }, + ) + .await + } + + /// Get historical business segment breakdowns for a security. + /// + /// Path: `GET /v1/quote/fundamentals/business-segments/history` + pub async fn business_segments_history( + &self, + symbol: impl Into, + report: Option<&'static str>, + cate: Option, + ) -> Result { + #[derive(Serialize)] + struct Query { + counter_id: String, + #[serde(skip_serializing_if = "Option::is_none")] + report: Option<&'static str>, + #[serde(skip_serializing_if = "Option::is_none")] + cate: Option, + } + self.get( + "/v1/quote/fundamentals/business-segments/history", + Query { + counter_id: symbol_to_counter_id(&symbol.into()), + report, + cate, + }, + ) + .await + } + + // ── institution_rating_views ────────────────────────────────── + + /// Get historical institutional rating view time-series for a security. + /// + /// Path: `GET /v1/quote/ratings/institutional` + pub async fn institution_rating_views( + &self, + symbol: impl Into, + ) -> Result { + #[derive(Serialize)] + struct Query { + counter_id: String, + } + self.get( + "/v1/quote/ratings/institutional", + Query { + counter_id: symbol_to_counter_id(&symbol.into()), + }, + ) + .await + } + + // ── industry_rank ───────────────────────────────────────────── + + /// Get industry rank for a market. + /// + /// Path: `GET /v1/quote/industry/rank` + /// + /// `indicator` is a numeric string `"0"`–`"7"`; + /// `sort_type` is `"0"` (ascending) or `"1"` (descending). + pub async fn industry_rank( + &self, + market: impl Into, + indicator: impl Into, + sort_type: impl Into, + limit: u32, + ) -> Result { + #[derive(Serialize)] + struct Query { + market: String, + indicator: String, + sort_type: String, + limit: u32, + } + self.get( + "/v1/quote/industry/rank", + Query { + market: market.into(), + indicator: indicator.into(), + sort_type: sort_type.into(), + limit, + }, + ) + .await + } + + // ── industry_peers ──────────────────────────────────────────── + + /// Get the industry peer chain for a security or industry. + /// + /// Path: `GET /v1/quote/industries/peers` + /// + /// `counter_id` may be a regular symbol (e.g. `"AAPL.US"`) or an industry + /// counter ID (e.g. `"BK/US/123"`) — pass it through as-is if it already + /// contains a `/`. + pub async fn industry_peers( + &self, + counter_id: impl Into, + market: impl Into, + industry_id: Option, + ) -> Result { + let raw = counter_id.into(); + let cid = if raw.contains('/') { + raw + } else { + symbol_to_counter_id(&raw) + }; + #[derive(Serialize)] + struct Query { + #[serde(rename = "type")] + kind: &'static str, + market: String, + industry_id: String, + counter_id: String, + } + self.get( + "/v1/quote/industries/peers", + Query { + kind: "1", + market: market.into(), + industry_id: industry_id.unwrap_or_default(), + counter_id: cid, + }, + ) + .await + } + + // ── financial_report_snapshot ───────────────────────────────── + + /// Get a financial report snapshot (earnings snapshot) for a security. + /// + /// Path: `GET /v1/quote/financials/earnings-snapshot` + pub async fn financial_report_snapshot( + &self, + symbol: impl Into, + report: Option<&'static str>, + fiscal_year: Option, + fiscal_period: Option<&'static str>, + ) -> Result { + #[derive(Serialize)] + struct Query { + counter_id: String, + #[serde(skip_serializing_if = "Option::is_none")] + report: Option<&'static str>, + #[serde(skip_serializing_if = "Option::is_none")] + fiscal_year: Option, + #[serde(skip_serializing_if = "Option::is_none")] + fiscal_period: Option<&'static str>, + } + self.get( + "/v1/quote/financials/earnings-snapshot", + Query { + counter_id: symbol_to_counter_id(&symbol.into()), + report, + fiscal_year, + fiscal_period, + }, + ) + .await + } } diff --git a/rust/src/fundamental/types.rs b/rust/src/fundamental/types.rs index a9c5b6af5..ad60b9d36 100644 --- a/rust/src/fundamental/types.rs +++ b/rust/src/fundamental/types.rs @@ -1110,6 +1110,246 @@ pub enum FinancialReportKind { All, } +// ── business_segments ───────────────────────────────────────────── + +/// Response for [`crate::FundamentalContext::business_segments`] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct BusinessSegments { + /// Report date + pub date: String, + /// Total revenue + pub total: String, + /// Reporting currency + pub currency: String, + /// Business segment breakdown + #[serde(default)] + pub business: Vec, +} + +/// One business segment item (latest snapshot) +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct BusinessSegmentItem { + /// Segment name + pub name: String, + /// Percentage of total revenue + pub percent: String, +} + +/// Response for [`crate::FundamentalContext::business_segments_history`] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct BusinessSegmentsHistory { + /// Historical snapshots + #[serde(default)] + pub historical: Vec, +} + +/// One historical business segments snapshot +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct BusinessSegmentsHistoricalItem { + /// Report date + pub date: String, + /// Total revenue + pub total: String, + /// Reporting currency + pub currency: String, + /// Business segment breakdown + #[serde(default)] + pub business: Vec, + /// Regional breakdown + #[serde(default)] + pub regionals: Vec, +} + +/// One business/regional segment item in a historical snapshot +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct BusinessSegmentHistoryItem { + /// Segment name + pub name: String, + /// Percentage of total + pub percent: String, + /// Absolute value + pub value: String, +} + +// ── institution_rating_views ────────────────────────────────────── + +/// Response for [`crate::FundamentalContext::institution_rating_views`] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct InstitutionRatingViews { + /// Historical rating distribution snapshots + #[serde(default)] + pub elist: Vec, +} + +/// One historical rating distribution snapshot +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct InstitutionRatingViewItem { + /// Date as unix timestamp (int64) + pub date: i64, + /// Number of "Buy" ratings + pub buy: i32, + /// Number of "Outperform" ratings + pub over: i32, + /// Number of "Hold" ratings + pub hold: i32, + /// Number of "Underperform" ratings + pub under: i32, + /// Number of "Sell" ratings + pub sell: i32, + /// Total analyst count + pub total: i32, +} + +// ── industry_rank ───────────────────────────────────────────────── + +/// Response for [`crate::FundamentalContext::industry_rank`] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct IndustryRankResponse { + /// Grouped rank items + #[serde(default)] + pub items: Vec, +} + +/// A group of ranked industry items +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct IndustryRankGroup { + /// Items in this group + #[serde(default)] + pub lists: Vec, +} + +/// One ranked industry item +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct IndustryRankItem { + /// Industry / sector name + pub name: String, + /// Counter ID of the industry + pub counter_id: String, + /// Change percentage + pub chg: String, + /// Name of the leading stock + pub leading_name: String, + /// Ticker of the leading stock + pub leading_ticker: String, + /// Change percentage of the leading stock + pub leading_chg: String, + /// Value label name + pub value_name: String, + /// Value data + pub value_data: String, +} + +// ── industry_peers ──────────────────────────────────────────────── + +/// Response for [`crate::FundamentalContext::industry_peers`] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct IndustryPeersResponse { + /// Top-level industry node info + pub top: IndustryPeersTop, + /// Root peer chain node (may be absent if no data) + pub chain: Option, +} + +/// Top-level industry info in the peers response +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct IndustryPeersTop { + /// Industry name + pub name: String, + /// Market code + pub market: String, +} + +/// A node in the recursive industry peer chain +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct IndustryPeerNode { + /// Node name + pub name: String, + /// Counter ID + pub counter_id: String, + /// Number of stocks in this node + pub stock_num: String, + /// Change percentage + pub chg: String, + /// Year-to-date change + pub ytd_chg: String, + /// Child nodes (recursive) + #[serde(default)] + pub next: Vec, +} + +// ── financial_report_snapshot ───────────────────────────────────── + +/// Response for [`crate::FundamentalContext::financial_report_snapshot`] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct FinancialReportSnapshot { + /// Company name + pub name: String, + /// Ticker code + pub ticker: String, + /// Fiscal period start date + pub fp_start: String, + /// Fiscal period end date + pub fp_end: String, + /// Reporting currency + pub currency: String, + /// Report description + pub report_desc: String, + /// Forecast revenue + pub fo_revenue: Option, + /// Forecast EBIT + pub fo_ebit: Option, + /// Forecast EPS + pub fo_eps: Option, + /// Reported revenue + pub fr_revenue: Option, + /// Reported net profit + pub fr_profit: Option, + /// Reported operating cash flow + pub fr_operate_cash: Option, + /// Reported investing cash flow + pub fr_invest_cash: Option, + /// Reported financing cash flow + pub fr_finance_cash: Option, + /// Reported total assets + pub fr_total_assets: Option, + /// Reported total liabilities + pub fr_total_liability: Option, + /// ROE TTM + pub fr_roe_ttm: String, + /// Profit margin + pub fr_profit_margin: String, + /// Profit margin TTM + pub fr_profit_margin_ttm: String, + /// Asset turnover TTM + pub fr_asset_turn_ttm: String, + /// Leverage TTM + pub fr_leverage_ttm: String, + /// Debt-to-assets ratio + pub fr_debt_assets_ratio: String, +} + +/// A forecast metric in the financial report snapshot +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SnapshotForecastMetric { + /// Actual value + pub value: String, + /// Year-over-year change + pub yoy: String, + /// Beat/miss description + pub cmp_desc: String, + /// Consensus estimate value + pub est_value: String, +} + +/// A reported metric in the financial report snapshot +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SnapshotReportedMetric { + /// Actual value + pub value: String, + /// Year-over-year change + pub yoy: String, +} + /// Financial report period type #[derive(Debug, Copy, Clone, Eq, PartialEq, Serialize, Deserialize)] pub enum FinancialReportPeriod {