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 validate_required_yyyymmdd(tool: &str, field: &str, value: &str) -> Result<(), String> {
41    if value.len() != 8 || !value.bytes().all(|b| b.is_ascii_digit()) {
42        return Err(format!(
43            "{field} must be YYYYMMDD for {tool}, got {value:?}"
44        ));
45    }
46    Ok(())
47}
48
49fn validate_i32_list_range(
50    tool: &str,
51    field: &str,
52    values: &[i32],
53    min: i32,
54    max: i32,
55) -> Result<(), String> {
56    for value in values {
57        if !(min..=max).contains(value) {
58            return Err(format!(
59                "{field} entries must be in {min}..={max} for {tool}, got {value}"
60            ));
61        }
62    }
63    Ok(())
64}
65
66fn default_financial_calendar_estimate_type_list() -> Vec<i32> {
67    vec![1, 2, 3]
68}
69
70fn is_financial_calendar_ranking_type(value: i32) -> bool {
71    (1..=7).contains(&value) || value == 100
72}
73
74fn financial_calendar_new_pc_field_selector()
75-> futu_backend::proto_internal::financial_calendar::NewPcFieldSelector {
76    futu_backend::proto_internal::financial_calendar::NewPcFieldSelector {
77        with_pub_type: Some(true),
78        with_pub_time: Some(true),
79        with_financial_period: Some(true),
80        with_option_volume: Some(true),
81        with_iv: Some(true),
82        with_iv_rank: Some(true),
83        with_iv_pctl: Some(true),
84        with_rt_market_cap: Some(true),
85        with_rt_price: Some(true),
86        with_rt_pe_ttm: Some(true),
87        with_rt_price_last_close: Some(true),
88    }
89}
90
91#[derive(Debug, Clone, Deserialize, schemars::JsonSchema)]
92#[serde(deny_unknown_fields)]
93pub struct FinancialCalendarCustomFilterReq {
94    #[schemars(
95        description = "Financial calendar custom filter type: 1=market_cap, 2=option_volume, 3=iv, 4=iv_rank, 5=iv_pctl, 6=rt_market_cap. Alias: customerFilterType"
96    )]
97    #[serde(alias = "customerFilterType")]
98    pub customer_filter_type: i32,
99    #[schemars(description = "Inclusive lower bound. Alias: customerFilterValueStartAt")]
100    #[serde(default, alias = "customerFilterValueStartAt")]
101    pub customer_filter_value_start_at: Option<i64>,
102    #[schemars(description = "Inclusive upper bound. Alias: customerFilterValueEndAt")]
103    #[serde(default, alias = "customerFilterValueEndAt")]
104    pub customer_filter_value_end_at: Option<i64>,
105}
106
107impl FinancialCalendarCustomFilterReq {
108    fn validate(&self) -> Result<(), String> {
109        validate_i32_list_range(
110            "futu_get_financial_calendar",
111            "custom_filter.customer_filter_type",
112            &[self.customer_filter_type],
113            1,
114            6,
115        )
116    }
117
118    fn to_proto(&self) -> futu_backend::proto_internal::financial_calendar::CustomFilter {
119        futu_backend::proto_internal::financial_calendar::CustomFilter {
120            customer_filter_type: Some(self.customer_filter_type),
121            customer_filter_value_start_at: self.customer_filter_value_start_at,
122            customer_filter_value_end_at: self.customer_filter_value_end_at,
123        }
124    }
125}
126
127#[derive(Debug, Deserialize, schemars::JsonSchema)]
128#[serde(deny_unknown_fields)]
129pub struct FinancialCalendarReq {
130    #[schemars(description = "Start date in YYYYMMDD. Alias: beginDate")]
131    #[serde(alias = "beginDate")]
132    pub begin_date: String,
133    #[schemars(description = "Exclusive end date in YYYYMMDD. Alias: endDate")]
134    #[serde(alias = "endDate")]
135    pub end_date: String,
136    #[schemars(
137        description = "Financial calendar market enum list: HK=1, US=2, CN=3, AU=4, SG=5, CA=6, JP=7, MY=8. Alias: marketList"
138    )]
139    #[serde(default, alias = "marketList")]
140    pub market_list: Vec<i32>,
141    #[schemars(
142        description = "Publication type list: 0=regular/same day, 1=pre-market, 2=after-hours. Alias: pubTypeList"
143    )]
144    #[serde(default, alias = "pubTypeList")]
145    pub pub_type_list: Vec<i32>,
146    #[schemars(
147        description = "Stock filter type list: 1=watchlist, 2=holding, 3=special watch. Alias: stockTypeList"
148    )]
149    #[serde(default, alias = "stockTypeList")]
150    pub stock_type_list: Vec<i32>,
151    #[schemars(
152        description = "Convenience switch for watchlist-only calendar view. Pass watchlist_stock_ids when you want client-cache-exact filtering. Alias: watchlistOnly"
153    )]
154    #[serde(default, alias = "watchlistOnly")]
155    pub watchlist_only: bool,
156    #[schemars(
157        description = "Convenience switch for position-only calendar view. Pass holding_stock_ids when you want client-cache-exact filtering. Alias: positionsOnly"
158    )]
159    #[serde(default, alias = "positionsOnly")]
160    pub positions_only: bool,
161    #[schemars(description = "Result limit, 1-1000. Omit to use backend default.")]
162    #[serde(default)]
163    pub count: Option<i32>,
164    #[schemars(description = "Ranking type. Alias: rankingType")]
165    #[serde(default, alias = "rankingType")]
166    pub ranking_type: Option<i32>,
167    #[schemars(
168        description = "Estimate type list: 1=EPS_GAAP, 2=REVENUE, 3=EBIT. Defaults to [1,2,3]. Alias: estimateTypeList"
169    )]
170    #[serde(
171        default = "default_financial_calendar_estimate_type_list",
172        alias = "estimateTypeList"
173    )]
174    pub estimate_type_list: Vec<i32>,
175    #[schemars(description = "Custom filters. Alias: customFilterList")]
176    #[serde(default, alias = "customFilterList")]
177    pub custom_filter_list: Vec<FinancialCalendarCustomFilterReq>,
178    #[schemars(description = "Explicit watchlist stock_id list. Alias: watchlistStockIds")]
179    #[serde(default, alias = "watchlistStockIds")]
180    pub watchlist_stock_ids: Vec<u64>,
181    #[schemars(description = "Explicit holding stock_id list. Alias: holdingStockIds")]
182    #[serde(default, alias = "holdingStockIds")]
183    pub holding_stock_ids: Vec<u64>,
184}
185
186impl FinancialCalendarReq {
187    pub fn validate(&self) -> Result<(), String> {
188        let tool = "futu_get_financial_calendar";
189        validate_required_yyyymmdd(tool, "begin_date", &self.begin_date)?;
190        validate_required_yyyymmdd(tool, "end_date", &self.end_date)?;
191        if self.begin_date >= self.end_date {
192            return Err(
193                "begin_date must be earlier than end_date for futu_get_financial_calendar"
194                    .to_string(),
195            );
196        }
197        validate_i32_list_range(tool, "market_list", &self.market_list, 1, 8)?;
198        validate_i32_list_range(tool, "pub_type_list", &self.pub_type_list, 0, 2)?;
199        validate_i32_list_range(tool, "stock_type_list", &self.stock_type_list, 1, 3)?;
200        validate_optional_i32_range(tool, "count", self.count, 1, 1000)?;
201        validate_optional_i32_where(
202            tool,
203            "ranking_type",
204            self.ranking_type,
205            "1..=7 or 100",
206            is_financial_calendar_ranking_type,
207        )?;
208        validate_i32_list_range(tool, "estimate_type_list", &self.estimate_type_list, 1, 3)?;
209        for filter in &self.custom_filter_list {
210            filter.validate()?;
211        }
212        Ok(())
213    }
214
215    pub fn to_proto(
216        &self,
217    ) -> futu_backend::proto_internal::financial_calendar::GetFinancialStatementCalendarViewReq
218    {
219        use futu_backend::proto_internal::financial_calendar as fc;
220
221        let mut stock_type_list = self.stock_type_list.clone();
222        if self.watchlist_only && !stock_type_list.contains(&1) {
223            stock_type_list.push(1);
224        }
225        if self.positions_only && !stock_type_list.contains(&2) {
226            stock_type_list.push(2);
227        }
228
229        fc::GetFinancialStatementCalendarViewReq {
230            market_list: self.market_list.clone(),
231            pub_type_list: self.pub_type_list.clone(),
232            stock_type_list,
233            count: self.count,
234            begin_date: Some(self.begin_date.clone()),
235            end_date: Some(self.end_date.clone()),
236            ranking_type: self.ranking_type,
237            estimate_type_list: self.estimate_type_list.clone(),
238            custom_filter_list: self
239                .custom_filter_list
240                .iter()
241                .map(FinancialCalendarCustomFilterReq::to_proto)
242                .collect(),
243            watchlist_stock_ids: self.watchlist_stock_ids.clone(),
244            holding_stock_ids: self.holding_stock_ids.clone(),
245            field_selector: None,
246            new_pc_field_selector: Some(financial_calendar_new_pc_field_selector()),
247        }
248    }
249}
250
251#[derive(Debug, Deserialize, schemars::JsonSchema)]
252#[serde(deny_unknown_fields)]
253pub struct TargetFinancialCalendarReq {
254    #[schemars(description = "Backend stock_id list to query. Alias: stockId")]
255    #[serde(alias = "stockId")]
256    pub stock_id: Vec<u64>,
257    #[schemars(
258        description = "Financial calendar market enum list: HK=1, US=2, CN=3, AU=4, SG=5, CA=6, JP=7, MY=8. Alias: marketList"
259    )]
260    #[serde(default, alias = "marketList")]
261    pub market_list: Vec<i32>,
262    #[schemars(description = "Number of calendar rows to fetch; must be positive.")]
263    pub size: i32,
264    #[schemars(description = "Pagination start offset. Alias: start")]
265    #[serde(default)]
266    pub start: Option<i32>,
267}
268
269impl TargetFinancialCalendarReq {
270    pub fn validate(&self) -> Result<(), String> {
271        let tool = "futu_search_target_financial_calendar";
272        if self.stock_id.is_empty() || self.stock_id.contains(&0) {
273            return Err(format!(
274                "{tool} stock_id must be a non-empty positive uint64 list"
275            ));
276        }
277        validate_i32_list_range(tool, "market_list", &self.market_list, 1, 8)?;
278        if self.size <= 0 {
279            return Err(format!("{tool} size must be positive, got {}", self.size));
280        }
281        if let Some(start) = self.start
282            && start < 0
283        {
284            return Err(format!("{tool} start must be non-negative, got {start}"));
285        }
286        Ok(())
287    }
288
289    pub fn to_proto(
290        &self,
291    ) -> futu_backend::proto_internal::financial_calendar::SearchTargetStockFinancialCalendarReq
292    {
293        futu_backend::proto_internal::financial_calendar::SearchTargetStockFinancialCalendarReq {
294            stock_id: self.stock_id.clone(),
295            market_list: self.market_list.clone(),
296            size: Some(self.size),
297            start: self.start,
298        }
299    }
300}
301
302fn is_financial_statements_f10_type(value: i32) -> bool {
303    // C++ Qot_GetFinancialsStatements accepts Qot_Common.F10Type
304    // 0..7 and 9..11. Ref:
305    // FutuOpenD/Src/APIServer/Business/Quote/APIServer_Qot_FinancialsStatements.cpp:107-117
306    (0..=7).contains(&value) || (9..=11).contains(&value)
307}
308
309fn is_revenue_breakdown_f10_type(value: i32) -> bool {
310    // C++ Qot_GetFinancialsRevenueBreakdown accepts Qot_Common.F10Type
311    // 0..7 and 9. Ref:
312    // FutuOpenD/Src/APIServer/Business/Quote/APIServer_Qot_FinancialsRevenueBreakdown.cpp:50-59
313    (0..=7).contains(&value) || value == 9
314}
315
316fn is_valuation_type_with_unknown(value: i32) -> bool {
317    // Qot_Common.ValuationType: Unknown(0), PE(1), PB(2), PS(3).
318    (0..=3).contains(&value)
319}
320
321fn is_valuation_plate_sort_id(value: i32) -> bool {
322    // C++ maps public SortField Unknown/MarketCap/Valuation/
323    // ForwardValuation/HistoricalPercentile and rejects everything else.
324    // Ref: FutuOpenD/Src/APIServer/Business/Quote/APIServer_Qot_ValuationPlateStockList.cpp:82-96
325    matches!(value, 0 | 51 | 52 | 53 | 54)
326}
327
328#[derive(Debug, Deserialize, schemars::JsonSchema)]
329#[serde(deny_unknown_fields)]
330pub struct CompanyExecutiveBackgroundReq {
331    #[schemars(
332        description = "Security symbol in MARKET.CODE format, e.g. HK.00700, US.AAPL \
333                       (alias: code / stock / security for SDK compat)"
334    )]
335    #[serde(alias = "code", alias = "stock", alias = "security")]
336    pub symbol: String,
337    #[schemars(description = "Executive/director leader name from get_company_executives")]
338    #[serde(alias = "leaderName")]
339    pub leader_name: String,
340}
341
342#[derive(Debug, Deserialize, schemars::JsonSchema)]
343#[serde(deny_unknown_fields)]
344pub struct CompanyOperationalEfficiencyReq {
345    #[schemars(
346        description = "Security symbol in MARKET.CODE format, e.g. HK.00700, US.AAPL \
347                       (alias: code / stock / security for SDK compat)"
348    )]
349    #[serde(alias = "code", alias = "stock", alias = "security")]
350    pub symbol: String,
351    #[schemars(description = "Pagination key from previous response; alias: nextKey")]
352    #[serde(default, alias = "nextKey")]
353    pub next_key: Option<String>,
354    #[schemars(description = "Rows per page, 1-50. Omit to use backend default 10.")]
355    #[serde(default)]
356    pub num: Option<i32>,
357    #[schemars(description = "Currency code, e.g. CNY / USD / HKD; alias: currencyCode")]
358    #[serde(default, alias = "currencyCode")]
359    pub currency_code: Option<String>,
360    #[schemars(
361        description = "Compatibility-only field; the backend ignores financialType for company operational efficiency and always queries the fixed annual view. Alias: financialType"
362    )]
363    #[serde(default, alias = "financialType")]
364    pub financial_type: Option<i32>,
365}
366
367impl CompanyOperationalEfficiencyReq {
368    pub fn validate(&self) -> Result<(), String> {
369        validate_optional_i32_range(
370            "futu_get_company_operational_efficiency",
371            "num",
372            self.num,
373            1,
374            50,
375        )
376    }
377}
378
379#[derive(Debug, Deserialize, schemars::JsonSchema)]
380#[serde(deny_unknown_fields)]
381pub struct FinancialsEarningsPriceMoveReq {
382    #[schemars(
383        description = "Security symbol in MARKET.CODE format, e.g. HK.00700, US.AAPL \
384                       (alias: code / stock / security for SDK compat)"
385    )]
386    #[serde(alias = "code", alias = "stock", alias = "security")]
387    pub symbol: String,
388    #[schemars(description = "Financial period count, 1-50. Omit to use backend default 10.")]
389    #[serde(default, alias = "periodCount")]
390    pub period_count: Option<i32>,
391}
392
393impl FinancialsEarningsPriceMoveReq {
394    pub fn validate(&self) -> Result<(), String> {
395        validate_optional_i32_range(
396            "futu_get_financials_earnings_price_move",
397            "period_count",
398            self.period_count,
399            1,
400            50,
401        )
402    }
403}
404
405#[derive(Debug, Deserialize, schemars::JsonSchema)]
406#[serde(deny_unknown_fields)]
407pub struct FinancialsEarningsPriceHistoryReq {
408    #[schemars(
409        description = "Security symbol in MARKET.CODE format, e.g. HK.00700, US.AAPL \
410                       (alias: code / stock / security for SDK compat)"
411    )]
412    #[serde(alias = "code", alias = "stock", alias = "security")]
413    pub symbol: String,
414}
415
416#[derive(Debug, Deserialize, schemars::JsonSchema)]
417#[serde(deny_unknown_fields)]
418pub struct FinancialsStatementsReq {
419    #[schemars(
420        description = "Security symbol in MARKET.CODE format, e.g. HK.00700, US.AAPL \
421                       (alias: code / stock / security for SDK compat)"
422    )]
423    #[serde(alias = "code", alias = "stock", alias = "security")]
424    pub symbol: String,
425    #[schemars(
426        description = "FinancialStatementsType: 0=unknown, 1=income, 2=balance_sheet, 3=cash_flow, 4=main_index; omit to use backend default 1. Alias: statementType"
427    )]
428    #[serde(default, alias = "statementType")]
429    pub statement_type: Option<i32>,
430    #[schemars(
431        description = "F10Type financial period filter, supports 0-7 and 9-11; omit to use backend default 10. Alias: financialType"
432    )]
433    #[serde(default, alias = "financialType")]
434    pub financial_type: Option<i32>,
435    #[schemars(description = "Currency code, e.g. CNY / USD / HKD; alias: currencyCode")]
436    #[serde(default, alias = "currencyCode")]
437    pub currency_code: Option<String>,
438    #[schemars(description = "Pagination key from previous response; alias: nextKey")]
439    #[serde(default, alias = "nextKey")]
440    pub next_key: Option<String>,
441    #[schemars(description = "Rows per page, 1-50. Omit to use backend default 10.")]
442    #[serde(default)]
443    pub num: Option<i32>,
444}
445
446impl FinancialsStatementsReq {
447    pub fn validate(&self) -> Result<(), String> {
448        validate_optional_i32_range("futu_get_financials_statements", "num", self.num, 1, 50)?;
449        validate_optional_i32_range(
450            "futu_get_financials_statements",
451            "statement_type",
452            self.statement_type,
453            0,
454            4,
455        )?;
456        validate_optional_i32_where(
457            "futu_get_financials_statements",
458            "financial_type",
459            self.financial_type,
460            "0..=7 or 9..=11",
461            is_financial_statements_f10_type,
462        )
463    }
464}
465
466#[derive(Debug, Deserialize, schemars::JsonSchema)]
467#[serde(deny_unknown_fields)]
468pub struct FinancialsRevenueBreakdownReq {
469    #[schemars(
470        description = "Security symbol in MARKET.CODE format, e.g. HK.00700, US.AAPL \
471                       (alias: code / stock / security for SDK compat)"
472    )]
473    #[serde(alias = "code", alias = "stock", alias = "security")]
474    pub symbol: String,
475    #[schemars(description = "Screen date timestamp from screenDateList; alias: screenDate")]
476    #[serde(default, alias = "screenDate")]
477    pub date: Option<u32>,
478    #[schemars(
479        description = "F10Type financial period filter, supports 0-7 and 9; omit to use backend default 0. Alias: financialType"
480    )]
481    #[serde(default, alias = "financialType")]
482    pub financial_type: Option<i32>,
483    #[schemars(description = "Currency code, e.g. CNY / USD / HKD; alias: currencyCode")]
484    #[serde(default, alias = "currencyCode")]
485    pub currency_code: Option<String>,
486}
487
488impl FinancialsRevenueBreakdownReq {
489    pub fn validate(&self) -> Result<(), String> {
490        validate_optional_i32_where(
491            "futu_get_financials_revenue_breakdown",
492            "financial_type",
493            self.financial_type,
494            "0..=7 or 9",
495            is_revenue_breakdown_f10_type,
496        )
497    }
498}
499
500#[derive(Debug, Deserialize, schemars::JsonSchema)]
501#[serde(deny_unknown_fields)]
502pub struct ResearchAnalystConsensusReq {
503    #[schemars(
504        description = "Security symbol in MARKET.CODE format, e.g. HK.00700, US.AAPL \
505                       (alias: code / stock / security for SDK compat)"
506    )]
507    #[serde(alias = "code", alias = "stock", alias = "security")]
508    pub symbol: String,
509}
510
511#[derive(Debug, Deserialize, schemars::JsonSchema)]
512#[serde(deny_unknown_fields)]
513pub struct ResearchRatingSummaryReq {
514    #[schemars(
515        description = "Security symbol in MARKET.CODE format, e.g. US.AAPL, CA.SHOP \
516                       (alias: code / stock / security for SDK compat)"
517    )]
518    #[serde(alias = "code", alias = "stock", alias = "security")]
519    pub symbol: String,
520    #[schemars(description = "ResearchRatingDimensionType: 0/1=institution, 2=analyst")]
521    #[serde(default, alias = "ratingDimensionType")]
522    pub rating_dimension_type: Option<i32>,
523    #[schemars(description = "Institution or analyst uid; empty means summary list")]
524    #[serde(default)]
525    pub uid: Option<String>,
526    #[schemars(description = "Pagination key; alias: nextKey")]
527    #[serde(default, alias = "nextKey")]
528    pub next_key: Option<String>,
529    #[schemars(description = "Page size, 1..20; omitted uses backend default 10")]
530    #[serde(default)]
531    pub num: Option<i32>,
532}
533
534impl ResearchRatingSummaryReq {
535    pub fn validate(&self) -> Result<(), String> {
536        validate_optional_i32_range("futu_get_research_rating_summary", "num", self.num, 1, 20)?;
537        validate_optional_i32_range(
538            "futu_get_research_rating_summary",
539            "rating_dimension_type",
540            self.rating_dimension_type,
541            0,
542            2,
543        )
544    }
545}
546
547#[derive(Debug, Deserialize, schemars::JsonSchema)]
548#[serde(deny_unknown_fields)]
549pub struct ResearchMorningstarReportReq {
550    #[schemars(description = "Security symbol in MARKET.CODE format, e.g. US.AAPL \
551                       (alias: code / stock / security for SDK compat)")]
552    #[serde(alias = "code", alias = "stock", alias = "security")]
553    pub symbol: String,
554}
555
556#[derive(Debug, Deserialize, schemars::JsonSchema)]
557#[serde(deny_unknown_fields)]
558pub struct ValuationDetailReq {
559    #[schemars(
560        description = "Security symbol in MARKET.CODE format, e.g. HK.00700, US.AAPL, HK.800000 \
561                       (alias: code / stock / security for SDK compat)"
562    )]
563    #[serde(alias = "code", alias = "stock", alias = "security")]
564    pub symbol: String,
565    #[schemars(
566        description = "ValuationType: 0=recommended, 1=PE, 2=PB, 3=PS; alias: valuationType"
567    )]
568    #[serde(default, alias = "valuationType")]
569    pub valuation_type: Option<i32>,
570    #[schemars(
571        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"
572    )]
573    #[serde(default, alias = "intervalType")]
574    pub interval_type: Option<i32>,
575}
576
577impl ValuationDetailReq {
578    pub fn validate(&self) -> Result<(), String> {
579        validate_optional_i32_where(
580            "futu_get_valuation_detail",
581            "valuation_type",
582            self.valuation_type,
583            "0..=3",
584            is_valuation_type_with_unknown,
585        )?;
586        validate_optional_i32_range(
587            "futu_get_valuation_detail",
588            "interval_type",
589            self.interval_type,
590            0,
591            10,
592        )
593    }
594}
595
596#[derive(Debug, Deserialize, schemars::JsonSchema)]
597#[serde(deny_unknown_fields)]
598pub struct ValuationPlateStockListReq {
599    #[schemars(
600        description = "Plate or index symbol in MARKET.CODE format, e.g. HK.BK0001, HK.800000 \
601                       (alias: code / stock / security for SDK compat)"
602    )]
603    #[serde(alias = "code", alias = "stock", alias = "security")]
604    pub symbol: String,
605    #[schemars(description = "ValuationType: 0/1=PE, 2=PB, 3=PS; alias: valuationType")]
606    #[serde(default, alias = "valuationType")]
607    pub valuation_type: Option<i32>,
608    #[schemars(description = "Pagination key; alias: nextKey")]
609    #[serde(default, alias = "nextKey")]
610    pub next_key: Option<String>,
611    #[schemars(description = "Page size, 1..50; omitted uses backend default 10")]
612    #[serde(default)]
613    pub num: Option<i32>,
614    #[schemars(
615        description = "SortType: 0/2=asc, 1=desc; omit to use backend default asc. Alias: sortType"
616    )]
617    #[serde(default, alias = "sortType")]
618    pub sort_type: Option<i32>,
619    #[schemars(
620        description = "SortField: 51=market_cap, 52=valuation, 53=forward, 54=percentile; alias: sortId"
621    )]
622    #[serde(default, alias = "sortId")]
623    pub sort_id: Option<i32>,
624    #[schemars(
625        description = "Optional plate filter for index components, MARKET.CODE; alias: filterSecurity"
626    )]
627    #[serde(default, alias = "filterSecurity")]
628    pub filter_security: Option<String>,
629}
630
631impl ValuationPlateStockListReq {
632    pub fn validate(&self) -> Result<(), String> {
633        validate_optional_i32_range(
634            "futu_get_valuation_plate_stock_list",
635            "num",
636            self.num,
637            1,
638            50,
639        )?;
640        validate_optional_i32_where(
641            "futu_get_valuation_plate_stock_list",
642            "valuation_type",
643            self.valuation_type,
644            "0..=3",
645            is_valuation_type_with_unknown,
646        )?;
647        validate_optional_i32_where(
648            "futu_get_valuation_plate_stock_list",
649            "sort_id",
650            self.sort_id,
651            "0, 51, 52, 53, or 54",
652            is_valuation_plate_sort_id,
653        )
654    }
655}
656
657#[cfg(test)]
658#[path = "f10_tests.rs"]
659mod f10_tests;