Skip to main content

futu_mcp/tool_args/qot/
f10.rs

1//! MCP QOT F10, financials, research, and valuation request schemas.
2
3use rmcp::schemars;
4use serde::Deserialize;
5
6fn validate_optional_i32_range(
7    tool: &str,
8    field: &str,
9    value: Option<i32>,
10    min: i32,
11    max: i32,
12) -> Result<(), String> {
13    if let Some(value) = value
14        && !(min..=max).contains(&value)
15    {
16        return Err(format!(
17            "{field} must be in {min}..={max} for {tool}, got {value}"
18        ));
19    }
20    Ok(())
21}
22
23fn validate_optional_i32_where(
24    tool: &str,
25    field: &str,
26    value: Option<i32>,
27    expected: &str,
28    is_valid: impl Fn(i32) -> bool,
29) -> Result<(), String> {
30    if let Some(value) = value
31        && !is_valid(value)
32    {
33        return Err(format!(
34            "{field} must be {expected} for {tool}, got {value}"
35        ));
36    }
37    Ok(())
38}
39
40fn is_financial_statements_f10_type(value: i32) -> bool {
41    // C++ Qot_GetFinancialsStatements accepts Qot_Common.F10Type
42    // 0..7 and 9..11. Ref:
43    // FutuOpenD/Src/APIServer/Business/Quote/APIServer_Qot_FinancialsStatements.cpp:107-117
44    (0..=7).contains(&value) || (9..=11).contains(&value)
45}
46
47fn is_revenue_breakdown_f10_type(value: i32) -> bool {
48    // C++ Qot_GetFinancialsRevenueBreakdown accepts Qot_Common.F10Type
49    // 0..7 and 9. Ref:
50    // FutuOpenD/Src/APIServer/Business/Quote/APIServer_Qot_FinancialsRevenueBreakdown.cpp:50-59
51    (0..=7).contains(&value) || value == 9
52}
53
54fn is_valuation_type_with_unknown(value: i32) -> bool {
55    // Qot_Common.ValuationType: Unknown(0), PE(1), PB(2), PS(3).
56    (0..=3).contains(&value)
57}
58
59fn is_valuation_plate_sort_id(value: i32) -> bool {
60    // C++ maps public SortField Unknown/MarketCap/Valuation/
61    // ForwardValuation/HistoricalPercentile and rejects everything else.
62    // Ref: FutuOpenD/Src/APIServer/Business/Quote/APIServer_Qot_ValuationPlateStockList.cpp:82-96
63    matches!(value, 0 | 51 | 52 | 53 | 54)
64}
65
66#[derive(Debug, Deserialize, schemars::JsonSchema)]
67#[serde(deny_unknown_fields)]
68pub struct CompanyExecutiveBackgroundReq {
69    #[schemars(
70        description = "Security symbol in MARKET.CODE format, e.g. HK.00700, US.AAPL \
71                       (alias: code / stock / security for SDK compat)"
72    )]
73    #[serde(alias = "code", alias = "stock", alias = "security")]
74    pub symbol: String,
75    #[schemars(description = "Executive/director leader name from get_company_executives")]
76    #[serde(alias = "leaderName")]
77    pub leader_name: String,
78}
79
80#[derive(Debug, Deserialize, schemars::JsonSchema)]
81#[serde(deny_unknown_fields)]
82pub struct CompanyOperationalEfficiencyReq {
83    #[schemars(
84        description = "Security symbol in MARKET.CODE format, e.g. HK.00700, US.AAPL \
85                       (alias: code / stock / security for SDK compat)"
86    )]
87    #[serde(alias = "code", alias = "stock", alias = "security")]
88    pub symbol: String,
89    #[schemars(description = "Pagination key from previous response; alias: nextKey")]
90    #[serde(default, alias = "nextKey")]
91    pub next_key: Option<String>,
92    #[schemars(description = "Rows per page, 1-50. Omit to use backend default 10.")]
93    #[serde(default)]
94    pub num: Option<i32>,
95    #[schemars(description = "Currency code, e.g. CNY / USD / HKD; alias: currencyCode")]
96    #[serde(default, alias = "currencyCode")]
97    pub currency_code: Option<String>,
98    #[schemars(
99        description = "Compatibility-only field; the backend ignores financialType for company operational efficiency and always queries the fixed annual view. Alias: financialType"
100    )]
101    #[serde(default, alias = "financialType")]
102    pub financial_type: Option<i32>,
103}
104
105impl CompanyOperationalEfficiencyReq {
106    pub fn validate(&self) -> Result<(), String> {
107        validate_optional_i32_range(
108            "futu_get_company_operational_efficiency",
109            "num",
110            self.num,
111            1,
112            50,
113        )
114    }
115}
116
117#[derive(Debug, Deserialize, schemars::JsonSchema)]
118#[serde(deny_unknown_fields)]
119pub struct FinancialsEarningsPriceMoveReq {
120    #[schemars(
121        description = "Security symbol in MARKET.CODE format, e.g. HK.00700, US.AAPL \
122                       (alias: code / stock / security for SDK compat)"
123    )]
124    #[serde(alias = "code", alias = "stock", alias = "security")]
125    pub symbol: String,
126    #[schemars(description = "Financial period count, 1-50. Omit to use backend default 10.")]
127    #[serde(default, alias = "periodCount")]
128    pub period_count: Option<i32>,
129}
130
131impl FinancialsEarningsPriceMoveReq {
132    pub fn validate(&self) -> Result<(), String> {
133        validate_optional_i32_range(
134            "futu_get_financials_earnings_price_move",
135            "period_count",
136            self.period_count,
137            1,
138            50,
139        )
140    }
141}
142
143#[derive(Debug, Deserialize, schemars::JsonSchema)]
144#[serde(deny_unknown_fields)]
145pub struct FinancialsEarningsPriceHistoryReq {
146    #[schemars(
147        description = "Security symbol in MARKET.CODE format, e.g. HK.00700, US.AAPL \
148                       (alias: code / stock / security for SDK compat)"
149    )]
150    #[serde(alias = "code", alias = "stock", alias = "security")]
151    pub symbol: String,
152}
153
154#[derive(Debug, Deserialize, schemars::JsonSchema)]
155#[serde(deny_unknown_fields)]
156pub struct FinancialsStatementsReq {
157    #[schemars(
158        description = "Security symbol in MARKET.CODE format, e.g. HK.00700, US.AAPL \
159                       (alias: code / stock / security for SDK compat)"
160    )]
161    #[serde(alias = "code", alias = "stock", alias = "security")]
162    pub symbol: String,
163    #[schemars(
164        description = "FinancialStatementsType: 0=unknown, 1=income, 2=balance_sheet, 3=cash_flow, 4=main_index; omit to use backend default 1. Alias: statementType"
165    )]
166    #[serde(default, alias = "statementType")]
167    pub statement_type: Option<i32>,
168    #[schemars(
169        description = "F10Type financial period filter, supports 0-7 and 9-11; omit to use backend default 10. Alias: financialType"
170    )]
171    #[serde(default, alias = "financialType")]
172    pub financial_type: Option<i32>,
173    #[schemars(description = "Currency code, e.g. CNY / USD / HKD; alias: currencyCode")]
174    #[serde(default, alias = "currencyCode")]
175    pub currency_code: Option<String>,
176    #[schemars(description = "Pagination key from previous response; alias: nextKey")]
177    #[serde(default, alias = "nextKey")]
178    pub next_key: Option<String>,
179    #[schemars(description = "Rows per page, 1-50. Omit to use backend default 10.")]
180    #[serde(default)]
181    pub num: Option<i32>,
182}
183
184impl FinancialsStatementsReq {
185    pub fn validate(&self) -> Result<(), String> {
186        validate_optional_i32_range("futu_get_financials_statements", "num", self.num, 1, 50)?;
187        validate_optional_i32_range(
188            "futu_get_financials_statements",
189            "statement_type",
190            self.statement_type,
191            0,
192            4,
193        )?;
194        validate_optional_i32_where(
195            "futu_get_financials_statements",
196            "financial_type",
197            self.financial_type,
198            "0..=7 or 9..=11",
199            is_financial_statements_f10_type,
200        )
201    }
202}
203
204#[derive(Debug, Deserialize, schemars::JsonSchema)]
205#[serde(deny_unknown_fields)]
206pub struct FinancialsRevenueBreakdownReq {
207    #[schemars(
208        description = "Security symbol in MARKET.CODE format, e.g. HK.00700, US.AAPL \
209                       (alias: code / stock / security for SDK compat)"
210    )]
211    #[serde(alias = "code", alias = "stock", alias = "security")]
212    pub symbol: String,
213    #[schemars(description = "Screen date timestamp from screenDateList; alias: screenDate")]
214    #[serde(default, alias = "screenDate")]
215    pub date: Option<u32>,
216    #[schemars(
217        description = "F10Type financial period filter, supports 0-7 and 9; omit to use backend default 0. Alias: financialType"
218    )]
219    #[serde(default, alias = "financialType")]
220    pub financial_type: Option<i32>,
221    #[schemars(description = "Currency code, e.g. CNY / USD / HKD; alias: currencyCode")]
222    #[serde(default, alias = "currencyCode")]
223    pub currency_code: Option<String>,
224}
225
226impl FinancialsRevenueBreakdownReq {
227    pub fn validate(&self) -> Result<(), String> {
228        validate_optional_i32_where(
229            "futu_get_financials_revenue_breakdown",
230            "financial_type",
231            self.financial_type,
232            "0..=7 or 9",
233            is_revenue_breakdown_f10_type,
234        )
235    }
236}
237
238#[derive(Debug, Deserialize, schemars::JsonSchema)]
239#[serde(deny_unknown_fields)]
240pub struct ResearchAnalystConsensusReq {
241    #[schemars(
242        description = "Security symbol in MARKET.CODE format, e.g. HK.00700, US.AAPL \
243                       (alias: code / stock / security for SDK compat)"
244    )]
245    #[serde(alias = "code", alias = "stock", alias = "security")]
246    pub symbol: String,
247}
248
249#[derive(Debug, Deserialize, schemars::JsonSchema)]
250#[serde(deny_unknown_fields)]
251pub struct ResearchRatingSummaryReq {
252    #[schemars(
253        description = "Security symbol in MARKET.CODE format, e.g. US.AAPL, CA.SHOP \
254                       (alias: code / stock / security for SDK compat)"
255    )]
256    #[serde(alias = "code", alias = "stock", alias = "security")]
257    pub symbol: String,
258    #[schemars(description = "ResearchRatingDimensionType: 0/1=institution, 2=analyst")]
259    #[serde(default, alias = "ratingDimensionType")]
260    pub rating_dimension_type: Option<i32>,
261    #[schemars(description = "Institution or analyst uid; empty means summary list")]
262    #[serde(default)]
263    pub uid: Option<String>,
264    #[schemars(description = "Pagination key; alias: nextKey")]
265    #[serde(default, alias = "nextKey")]
266    pub next_key: Option<String>,
267    #[schemars(description = "Page size, 1..20; omitted uses backend default 10")]
268    #[serde(default)]
269    pub num: Option<i32>,
270}
271
272impl ResearchRatingSummaryReq {
273    pub fn validate(&self) -> Result<(), String> {
274        validate_optional_i32_range("futu_get_research_rating_summary", "num", self.num, 1, 20)?;
275        validate_optional_i32_range(
276            "futu_get_research_rating_summary",
277            "rating_dimension_type",
278            self.rating_dimension_type,
279            0,
280            2,
281        )
282    }
283}
284
285#[derive(Debug, Deserialize, schemars::JsonSchema)]
286#[serde(deny_unknown_fields)]
287pub struct ResearchMorningstarReportReq {
288    #[schemars(description = "Security symbol in MARKET.CODE format, e.g. US.AAPL \
289                       (alias: code / stock / security for SDK compat)")]
290    #[serde(alias = "code", alias = "stock", alias = "security")]
291    pub symbol: String,
292}
293
294#[derive(Debug, Deserialize, schemars::JsonSchema)]
295#[serde(deny_unknown_fields)]
296pub struct ValuationDetailReq {
297    #[schemars(
298        description = "Security symbol in MARKET.CODE format, e.g. HK.00700, US.AAPL, HK.800000 \
299                       (alias: code / stock / security for SDK compat)"
300    )]
301    #[serde(alias = "code", alias = "stock", alias = "security")]
302    pub symbol: String,
303    #[schemars(
304        description = "ValuationType: 0=recommended, 1=PE, 2=PB, 3=PS; alias: valuationType"
305    )]
306    #[serde(default, alias = "valuationType")]
307    pub valuation_type: Option<i32>,
308    #[schemars(
309        description = "ValuationIntervalType: 0/3=1y default, 1=3m, 2=6m, 4=3y, 5=since2019, 6=5y, 7=10y, 8=2y, 9=20y, 10=30y; omit uses backend default 3. Alias: intervalType"
310    )]
311    #[serde(default, alias = "intervalType")]
312    pub interval_type: Option<i32>,
313}
314
315impl ValuationDetailReq {
316    pub fn validate(&self) -> Result<(), String> {
317        validate_optional_i32_where(
318            "futu_get_valuation_detail",
319            "valuation_type",
320            self.valuation_type,
321            "0..=3",
322            is_valuation_type_with_unknown,
323        )?;
324        validate_optional_i32_range(
325            "futu_get_valuation_detail",
326            "interval_type",
327            self.interval_type,
328            0,
329            10,
330        )
331    }
332}
333
334#[derive(Debug, Deserialize, schemars::JsonSchema)]
335#[serde(deny_unknown_fields)]
336pub struct ValuationPlateStockListReq {
337    #[schemars(
338        description = "Plate or index symbol in MARKET.CODE format, e.g. HK.BK0001, HK.800000 \
339                       (alias: code / stock / security for SDK compat)"
340    )]
341    #[serde(alias = "code", alias = "stock", alias = "security")]
342    pub symbol: String,
343    #[schemars(description = "ValuationType: 0/1=PE, 2=PB, 3=PS; alias: valuationType")]
344    #[serde(default, alias = "valuationType")]
345    pub valuation_type: Option<i32>,
346    #[schemars(description = "Pagination key; alias: nextKey")]
347    #[serde(default, alias = "nextKey")]
348    pub next_key: Option<String>,
349    #[schemars(description = "Page size, 1..50; omitted uses backend default 10")]
350    #[serde(default)]
351    pub num: Option<i32>,
352    #[schemars(
353        description = "SortType: 0/2=asc, 1=desc; omit to use backend default asc. Alias: sortType"
354    )]
355    #[serde(default, alias = "sortType")]
356    pub sort_type: Option<i32>,
357    #[schemars(
358        description = "SortField: 51=market_cap, 52=valuation, 53=forward, 54=percentile; alias: sortId"
359    )]
360    #[serde(default, alias = "sortId")]
361    pub sort_id: Option<i32>,
362    #[schemars(
363        description = "Optional plate filter for index components, MARKET.CODE; alias: filterSecurity"
364    )]
365    #[serde(default, alias = "filterSecurity")]
366    pub filter_security: Option<String>,
367}
368
369impl ValuationPlateStockListReq {
370    pub fn validate(&self) -> Result<(), String> {
371        validate_optional_i32_range(
372            "futu_get_valuation_plate_stock_list",
373            "num",
374            self.num,
375            1,
376            50,
377        )?;
378        validate_optional_i32_where(
379            "futu_get_valuation_plate_stock_list",
380            "valuation_type",
381            self.valuation_type,
382            "0..=3",
383            is_valuation_type_with_unknown,
384        )?;
385        validate_optional_i32_where(
386            "futu_get_valuation_plate_stock_list",
387            "sort_id",
388            self.sort_id,
389            "0, 51, 52, 53, or 54",
390            is_valuation_plate_sort_id,
391        )
392    }
393}