1use 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 (0..=7).contains(&value) || (9..=11).contains(&value)
45}
46
47fn is_revenue_breakdown_f10_type(value: i32) -> bool {
48 (0..=7).contains(&value) || value == 9
52}
53
54fn is_valuation_type_with_unknown(value: i32) -> bool {
55 (0..=3).contains(&value)
57}
58
59fn is_valuation_plate_sort_id(value: i32) -> bool {
60 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}