Skip to main content

futu_mcp/handlers/reference/
company.rs

1//! mcp/handlers/reference/company — v10.6 company profile / executives adapters.
2
3use std::sync::Arc;
4
5use anyhow::{Result, anyhow, bail};
6use futu_net::client::FutuClient;
7use prost::Message;
8use serde::Serialize;
9
10use crate::state::parse_symbol;
11
12#[derive(Serialize)]
13struct CompanyProfileOut {
14    symbol: String,
15    item_list: Vec<CompanyProfileItemOut>,
16}
17
18#[derive(Serialize)]
19struct CompanyProfileItemOut {
20    name: Option<String>,
21    value: Option<String>,
22    field_type: Option<i32>,
23}
24
25pub async fn get_company_profile(client: &Arc<FutuClient>, symbol: &str) -> Result<String> {
26    let sec = parse_symbol(symbol)?;
27    let req = futu_proto::qot_get_company_profile::Request {
28        c2s: futu_proto::qot_get_company_profile::C2s {
29            security: futu_proto::qot_common::Security {
30                market: sec.market as i32,
31                code: sec.code.clone(),
32            },
33        },
34    };
35    let body = req.encode_to_vec();
36    let frame = client
37        .request(futu_core::proto_id::QOT_GET_COMPANY_PROFILE, body)
38        .await?;
39    let resp = futu_proto::qot_get_company_profile::Response::decode(frame.body.as_ref())
40        .map_err(|e| anyhow!("decode company_profile: {e}"))?;
41    if resp.ret_type != 0 {
42        bail!(
43            "company_profile ret_type={} msg={:?}",
44            resp.ret_type,
45            resp.ret_msg
46        );
47    }
48    let s2c = resp.s2c.ok_or_else(|| anyhow!("missing s2c"))?;
49    let out = CompanyProfileOut {
50        symbol: symbol.to_string(),
51        item_list: s2c
52            .item_list
53            .iter()
54            .map(|item| CompanyProfileItemOut {
55                name: item.name.clone(),
56                value: item.value.clone(),
57                field_type: item.field_type,
58            })
59            .collect(),
60    };
61    Ok(serde_json::to_string_pretty(&out)?)
62}
63
64#[derive(Serialize)]
65struct CompanyExecutivesOut {
66    symbol: String,
67    director_list: Vec<DirectorOut>,
68}
69
70#[derive(Serialize)]
71struct DirectorOut {
72    display_leader_name: Option<String>,
73    leader_name: Option<String>,
74    position_name: Option<String>,
75    begin_date: Option<u64>,
76    begin_date_str: Option<String>,
77    leader_gender: Option<String>,
78    leader_age: Option<String>,
79    highest_education: Option<String>,
80    annual_salary: Option<u64>,
81    issue_date: Option<u64>,
82    issue_date_str: Option<String>,
83}
84
85pub async fn get_company_executives(client: &Arc<FutuClient>, symbol: &str) -> Result<String> {
86    let sec = parse_symbol(symbol)?;
87    let req = futu_proto::qot_get_company_executives::Request {
88        c2s: futu_proto::qot_get_company_executives::C2s {
89            security: futu_proto::qot_common::Security {
90                market: sec.market as i32,
91                code: sec.code.clone(),
92            },
93        },
94    };
95    let body = req.encode_to_vec();
96    let frame = client
97        .request(futu_core::proto_id::QOT_GET_COMPANY_EXECUTIVES, body)
98        .await?;
99    let resp = futu_proto::qot_get_company_executives::Response::decode(frame.body.as_ref())
100        .map_err(|e| anyhow!("decode company_executives: {e}"))?;
101    if resp.ret_type != 0 {
102        bail!(
103            "company_executives ret_type={} msg={:?}",
104            resp.ret_type,
105            resp.ret_msg
106        );
107    }
108    let s2c = resp.s2c.ok_or_else(|| anyhow!("missing s2c"))?;
109    let out = CompanyExecutivesOut {
110        symbol: symbol.to_string(),
111        director_list: s2c
112            .director_list
113            .iter()
114            .map(|item| DirectorOut {
115                display_leader_name: item.display_leader_name.clone(),
116                leader_name: item.leader_name.clone(),
117                position_name: item.position_name.clone(),
118                begin_date: item.begin_date,
119                begin_date_str: item.begin_date_str.clone(),
120                leader_gender: item.leader_gender.clone(),
121                leader_age: item.leader_age.clone(),
122                highest_education: item.highest_education.clone(),
123                annual_salary: item.annual_salary,
124                issue_date: item.issue_date,
125                issue_date_str: item.issue_date_str.clone(),
126            })
127            .collect(),
128    };
129    Ok(serde_json::to_string_pretty(&out)?)
130}
131
132#[derive(Serialize)]
133struct CompanyExecutiveBackgroundOut {
134    symbol: String,
135    leader_name: String,
136    brief_background: Option<String>,
137}
138
139pub async fn get_company_executive_background(
140    client: &Arc<FutuClient>,
141    symbol: &str,
142    leader_name: &str,
143) -> Result<String> {
144    let sec = parse_symbol(symbol)?;
145    let req = futu_proto::qot_get_company_executive_background::Request {
146        c2s: futu_proto::qot_get_company_executive_background::C2s {
147            security: futu_proto::qot_common::Security {
148                market: sec.market as i32,
149                code: sec.code.clone(),
150            },
151            leader_name: Some(leader_name.to_string()),
152        },
153    };
154    let body = req.encode_to_vec();
155    let frame = client
156        .request(
157            futu_core::proto_id::QOT_GET_COMPANY_EXECUTIVE_BACKGROUND,
158            body,
159        )
160        .await?;
161    let resp =
162        futu_proto::qot_get_company_executive_background::Response::decode(frame.body.as_ref())
163            .map_err(|e| anyhow!("decode company_executive_background: {e}"))?;
164    if resp.ret_type != 0 {
165        bail!(
166            "company_executive_background ret_type={} msg={:?}",
167            resp.ret_type,
168            resp.ret_msg
169        );
170    }
171    let s2c = resp.s2c.ok_or_else(|| anyhow!("missing s2c"))?;
172    let out = CompanyExecutiveBackgroundOut {
173        symbol: symbol.to_string(),
174        leader_name: leader_name.to_string(),
175        brief_background: s2c.brief_background,
176    };
177    Ok(serde_json::to_string_pretty(&out)?)
178}
179
180#[derive(Serialize)]
181struct CompanyOperationalEfficiencyOut {
182    symbol: String,
183    item_list: Vec<OperationalEfficiencyItemOut>,
184    next_key: Option<String>,
185    currency_code: Option<String>,
186}
187
188#[derive(Serialize)]
189struct OperationalEfficiencyItemOut {
190    fiscal_year: Option<i32>,
191    financial_type: Option<i32>,
192    period_text: Option<String>,
193    end_date: Option<i64>,
194    end_date_str: Option<String>,
195    employee_num: Option<i64>,
196    employee_num_yoy: Option<f64>,
197    income_per_capita: Option<f64>,
198    income_per_capita_yoy: Option<f64>,
199    profit_per_capita: Option<f64>,
200    profit_per_capita_yoy: Option<f64>,
201    net_profit_per_capita: Option<f64>,
202    net_profit_per_capita_yoy: Option<f64>,
203}
204
205pub async fn get_company_operational_efficiency(
206    client: &Arc<FutuClient>,
207    symbol: &str,
208    next_key: Option<&str>,
209    num: Option<i32>,
210    currency_code: Option<&str>,
211    financial_type: Option<i32>,
212) -> Result<String> {
213    let sec = parse_symbol(symbol)?;
214    let req = futu_proto::qot_get_company_operational_efficiency::Request {
215        c2s: futu_proto::qot_get_company_operational_efficiency::C2s {
216            security: futu_proto::qot_common::Security {
217                market: sec.market as i32,
218                code: sec.code.clone(),
219            },
220            next_key: next_key.map(str::to_string),
221            num,
222            currency_code: currency_code.map(str::to_string),
223            financial_type,
224        },
225    };
226    let body = req.encode_to_vec();
227    let frame = client
228        .request(
229            futu_core::proto_id::QOT_GET_COMPANY_OPERATIONAL_EFFICIENCY,
230            body,
231        )
232        .await?;
233    let resp =
234        futu_proto::qot_get_company_operational_efficiency::Response::decode(frame.body.as_ref())
235            .map_err(|e| anyhow!("decode company_operational_efficiency: {e}"))?;
236    if resp.ret_type != 0 {
237        bail!(
238            "company_operational_efficiency ret_type={} msg={:?}",
239            resp.ret_type,
240            resp.ret_msg
241        );
242    }
243    let s2c = resp.s2c.ok_or_else(|| anyhow!("missing s2c"))?;
244    let out = CompanyOperationalEfficiencyOut {
245        symbol: symbol.to_string(),
246        item_list: s2c
247            .item_list
248            .iter()
249            .map(|item| OperationalEfficiencyItemOut {
250                fiscal_year: item.fiscal_year,
251                financial_type: item.financial_type,
252                period_text: item.period_text.clone(),
253                end_date: item.end_date,
254                end_date_str: item.end_date_str.clone(),
255                employee_num: item.employee_num,
256                employee_num_yoy: item.employee_num_yoy,
257                income_per_capita: item.income_per_capita,
258                income_per_capita_yoy: item.income_per_capita_yoy,
259                profit_per_capita: item.profit_per_capita,
260                profit_per_capita_yoy: item.profit_per_capita_yoy,
261                net_profit_per_capita: item.net_profit_per_capita,
262                net_profit_per_capita_yoy: item.net_profit_per_capita_yoy,
263            })
264            .collect(),
265        next_key: s2c.next_key,
266        currency_code: s2c.currency_code,
267    };
268    Ok(serde_json::to_string_pretty(&out)?)
269}
270
271pub async fn get_financials_earnings_price_move(
272    client: &Arc<FutuClient>,
273    symbol: &str,
274    period_count: Option<i32>,
275) -> Result<String> {
276    let sec = parse_symbol(symbol)?;
277    let req = futu_proto::qot_get_financials_earnings_price_move::Request {
278        c2s: futu_proto::qot_get_financials_earnings_price_move::C2s {
279            security: futu_proto::qot_common::Security {
280                market: sec.market as i32,
281                code: sec.code.clone(),
282            },
283            period_count,
284        },
285    };
286    let body = req.encode_to_vec();
287    let frame = client
288        .request(
289            futu_core::proto_id::QOT_GET_FINANCIALS_EARNINGS_PRICE_MOVE,
290            body,
291        )
292        .await?;
293    let resp =
294        futu_proto::qot_get_financials_earnings_price_move::Response::decode(frame.body.as_ref())
295            .map_err(|e| anyhow!("decode financials_earnings_price_move: {e}"))?;
296    if resp.ret_type != 0 {
297        bail!(
298            "financials_earnings_price_move ret_type={} msg={:?}",
299            resp.ret_type,
300            resp.ret_msg
301        );
302    }
303    let s2c = resp.s2c.ok_or_else(|| anyhow!("missing s2c"))?;
304    Ok(serde_json::to_string_pretty(&serde_json::json!({
305        "symbol": symbol,
306        "detail_list": s2c.detail_list,
307    }))?)
308}
309
310pub async fn get_financials_earnings_price_history(
311    client: &Arc<FutuClient>,
312    symbol: &str,
313) -> Result<String> {
314    let sec = parse_symbol(symbol)?;
315    let req = futu_proto::qot_get_financials_earnings_price_history::Request {
316        c2s: futu_proto::qot_get_financials_earnings_price_history::C2s {
317            security: futu_proto::qot_common::Security {
318                market: sec.market as i32,
319                code: sec.code.clone(),
320            },
321        },
322    };
323    let body = req.encode_to_vec();
324    let frame = client
325        .request(
326            futu_core::proto_id::QOT_GET_FINANCIALS_EARNINGS_PRICE_HISTORY,
327            body,
328        )
329        .await?;
330    let resp = futu_proto::qot_get_financials_earnings_price_history::Response::decode(
331        frame.body.as_ref(),
332    )
333    .map_err(|e| anyhow!("decode financials_earnings_price_history: {e}"))?;
334    if resp.ret_type != 0 {
335        bail!(
336            "financials_earnings_price_history ret_type={} msg={:?}",
337            resp.ret_type,
338            resp.ret_msg
339        );
340    }
341    let s2c = resp.s2c.ok_or_else(|| anyhow!("missing s2c"))?;
342    Ok(serde_json::to_string_pretty(&serde_json::json!({
343        "symbol": symbol,
344        "detail_list": s2c.detail_list,
345    }))?)
346}
347
348#[derive(Serialize)]
349struct FinancialsStatementsOut {
350    symbol: String,
351    structure_list: Vec<futu_proto::qot_get_financials_statements::FinancialFieldInfo>,
352    report_list: Vec<futu_proto::qot_get_financials_statements::FinancialReport>,
353    next_key: Option<String>,
354}
355
356pub async fn get_financials_statements(
357    client: &Arc<FutuClient>,
358    symbol: &str,
359    statement_type: Option<i32>,
360    financial_type: Option<i32>,
361    currency_code: Option<&str>,
362    next_key: Option<&str>,
363    num: Option<i32>,
364) -> Result<String> {
365    let sec = parse_symbol(symbol)?;
366    let req = futu_proto::qot_get_financials_statements::Request {
367        c2s: futu_proto::qot_get_financials_statements::C2s {
368            security: futu_proto::qot_common::Security {
369                market: sec.market as i32,
370                code: sec.code.clone(),
371            },
372            statement_type,
373            financial_type,
374            currency_code: currency_code.map(str::to_string),
375            next_key: next_key.map(str::to_string),
376            num,
377        },
378    };
379    let body = req.encode_to_vec();
380    let frame = client
381        .request(futu_core::proto_id::QOT_GET_FINANCIALS_STATEMENTS, body)
382        .await?;
383    let resp = futu_proto::qot_get_financials_statements::Response::decode(frame.body.as_ref())
384        .map_err(|e| anyhow!("decode financials_statements: {e}"))?;
385    if resp.ret_type != 0 {
386        bail!(
387            "financials_statements ret_type={} msg={:?}",
388            resp.ret_type,
389            resp.ret_msg
390        );
391    }
392    let s2c = resp.s2c.ok_or_else(|| anyhow!("missing s2c"))?;
393    let out = FinancialsStatementsOut {
394        symbol: symbol.to_string(),
395        structure_list: s2c.structure_list,
396        report_list: s2c.report_list,
397        next_key: s2c.next_key,
398    };
399    Ok(serde_json::to_string_pretty(&out)?)
400}
401
402#[derive(Serialize)]
403struct FinancialsRevenueBreakdownOut {
404    symbol: String,
405    period: Option<String>,
406    breakdown_list: Vec<futu_proto::qot_get_financials_revenue_breakdown::RevenueBreakdownGroup>,
407    currency_code: Option<String>,
408    screen_date_list: Vec<futu_proto::qot_get_financials_revenue_breakdown::ScreenDate>,
409}
410
411pub async fn get_financials_revenue_breakdown(
412    client: &Arc<FutuClient>,
413    symbol: &str,
414    date: Option<u32>,
415    financial_type: Option<i32>,
416    currency_code: Option<&str>,
417) -> Result<String> {
418    let sec = parse_symbol(symbol)?;
419    let req = futu_proto::qot_get_financials_revenue_breakdown::Request {
420        c2s: futu_proto::qot_get_financials_revenue_breakdown::C2s {
421            security: futu_proto::qot_common::Security {
422                market: sec.market as i32,
423                code: sec.code.clone(),
424            },
425            date,
426            financial_type,
427            currency_code: currency_code.map(str::to_string),
428        },
429    };
430    let body = req.encode_to_vec();
431    let frame = client
432        .request(
433            futu_core::proto_id::QOT_GET_FINANCIALS_REVENUE_BREAKDOWN,
434            body,
435        )
436        .await?;
437    let resp =
438        futu_proto::qot_get_financials_revenue_breakdown::Response::decode(frame.body.as_ref())
439            .map_err(|e| anyhow!("decode financials_revenue_breakdown: {e}"))?;
440    if resp.ret_type != 0 {
441        bail!(
442            "financials_revenue_breakdown ret_type={} msg={:?}",
443            resp.ret_type,
444            resp.ret_msg
445        );
446    }
447    let s2c = resp.s2c.ok_or_else(|| anyhow!("missing s2c"))?;
448    let out = FinancialsRevenueBreakdownOut {
449        symbol: symbol.to_string(),
450        period: s2c.period,
451        breakdown_list: s2c.breakdown_list,
452        currency_code: s2c.currency_code,
453        screen_date_list: s2c.screen_date_list,
454    };
455    Ok(serde_json::to_string_pretty(&out)?)
456}
457
458#[derive(Serialize)]
459struct ResearchAnalystConsensusOut {
460    symbol: String,
461    highest: Option<f64>,
462    average: Option<f64>,
463    lowest: Option<f64>,
464    rating: Option<i32>,
465    total: Option<i32>,
466    update_time: Option<i64>,
467    update_time_str: Option<String>,
468    buy: Option<f64>,
469    hold: Option<f64>,
470    sell: Option<f64>,
471    strong_buy: Option<f64>,
472    underperform: Option<f64>,
473}
474
475pub async fn get_research_analyst_consensus(
476    client: &Arc<FutuClient>,
477    symbol: &str,
478) -> Result<String> {
479    let sec = parse_symbol(symbol)?;
480    let req = futu_proto::qot_get_research_analyst_consensus::Request {
481        c2s: futu_proto::qot_get_research_analyst_consensus::C2s {
482            security: futu_proto::qot_common::Security {
483                market: sec.market as i32,
484                code: sec.code.clone(),
485            },
486        },
487    };
488    let body = req.encode_to_vec();
489    let frame = client
490        .request(
491            futu_core::proto_id::QOT_GET_RESEARCH_ANALYST_CONSENSUS,
492            body,
493        )
494        .await?;
495    let resp =
496        futu_proto::qot_get_research_analyst_consensus::Response::decode(frame.body.as_ref())
497            .map_err(|e| anyhow!("decode research_analyst_consensus: {e}"))?;
498    if resp.ret_type != 0 {
499        bail!(
500            "research_analyst_consensus ret_type={} msg={:?}",
501            resp.ret_type,
502            resp.ret_msg
503        );
504    }
505    let s2c = resp.s2c.ok_or_else(|| anyhow!("missing s2c"))?;
506    let out = ResearchAnalystConsensusOut {
507        symbol: symbol.to_string(),
508        highest: s2c.highest,
509        average: s2c.average,
510        lowest: s2c.lowest,
511        rating: s2c.rating,
512        total: s2c.total,
513        update_time: s2c.update_time,
514        update_time_str: s2c.update_time_str,
515        buy: s2c.buy,
516        hold: s2c.hold,
517        sell: s2c.sell,
518        strong_buy: s2c.strong_buy,
519        underperform: s2c.underperform,
520    };
521    Ok(serde_json::to_string_pretty(&out)?)
522}
523
524#[derive(Serialize)]
525struct ResearchRatingSummaryOut {
526    symbol: String,
527    s2c: futu_proto::qot_get_research_rating_summary::S2c,
528}
529
530pub async fn get_research_rating_summary(
531    client: &Arc<FutuClient>,
532    symbol: &str,
533    rating_dimension_type: Option<i32>,
534    uid: Option<&str>,
535    next_key: Option<&str>,
536    num: Option<i32>,
537) -> Result<String> {
538    let sec = parse_symbol(symbol)?;
539    let req = futu_proto::qot_get_research_rating_summary::Request {
540        c2s: futu_proto::qot_get_research_rating_summary::C2s {
541            security: futu_proto::qot_common::Security {
542                market: sec.market as i32,
543                code: sec.code.clone(),
544            },
545            rating_dimension_type,
546            uid: uid.map(str::to_string),
547            next_key: next_key.map(str::to_string),
548            num,
549        },
550    };
551    let body = req.encode_to_vec();
552    let frame = client
553        .request(futu_core::proto_id::QOT_GET_RESEARCH_RATING_SUMMARY, body)
554        .await?;
555    let resp = futu_proto::qot_get_research_rating_summary::Response::decode(frame.body.as_ref())
556        .map_err(|e| anyhow!("decode research_rating_summary: {e}"))?;
557    if resp.ret_type != 0 {
558        bail!(
559            "research_rating_summary ret_type={} msg={:?}",
560            resp.ret_type,
561            resp.ret_msg
562        );
563    }
564    let s2c = resp.s2c.ok_or_else(|| anyhow!("missing s2c"))?;
565    let out = ResearchRatingSummaryOut {
566        symbol: symbol.to_string(),
567        s2c,
568    };
569    Ok(serde_json::to_string_pretty(&out)?)
570}
571
572#[derive(Serialize)]
573struct ResearchMorningstarReportOut {
574    symbol: String,
575    s2c: futu_proto::qot_get_research_morningstar_report::S2c,
576}
577
578pub async fn get_research_morningstar_report(
579    client: &Arc<FutuClient>,
580    symbol: &str,
581) -> Result<String> {
582    let sec = parse_symbol(symbol)?;
583    let req = futu_proto::qot_get_research_morningstar_report::Request {
584        c2s: futu_proto::qot_get_research_morningstar_report::C2s {
585            security: futu_proto::qot_common::Security {
586                market: sec.market as i32,
587                code: sec.code.clone(),
588            },
589        },
590    };
591    let body = req.encode_to_vec();
592    let frame = client
593        .request(
594            futu_core::proto_id::QOT_GET_RESEARCH_MORNINGSTAR_REPORT,
595            body,
596        )
597        .await?;
598    let resp =
599        futu_proto::qot_get_research_morningstar_report::Response::decode(frame.body.as_ref())
600            .map_err(|e| anyhow!("decode research_morningstar_report: {e}"))?;
601    if resp.ret_type != 0 {
602        bail!(
603            "research_morningstar_report ret_type={} msg={:?}",
604            resp.ret_type,
605            resp.ret_msg
606        );
607    }
608    let s2c = resp.s2c.ok_or_else(|| anyhow!("missing s2c"))?;
609    let out = ResearchMorningstarReportOut {
610        symbol: symbol.to_string(),
611        s2c,
612    };
613    Ok(serde_json::to_string_pretty(&out)?)
614}
615
616#[derive(Serialize)]
617struct ValuationDetailOut {
618    symbol: String,
619    s2c: futu_proto::qot_get_valuation_detail::S2c,
620}
621
622pub async fn get_valuation_detail(
623    client: &Arc<FutuClient>,
624    symbol: &str,
625    valuation_type: Option<i32>,
626    interval_type: Option<i32>,
627) -> Result<String> {
628    let sec = parse_symbol(symbol)?;
629    let req = futu_proto::qot_get_valuation_detail::Request {
630        c2s: futu_proto::qot_get_valuation_detail::C2s {
631            security: futu_proto::qot_common::Security {
632                market: sec.market as i32,
633                code: sec.code.clone(),
634            },
635            valuation_type,
636            interval_type,
637        },
638    };
639    let body = req.encode_to_vec();
640    let frame = client
641        .request(futu_core::proto_id::QOT_GET_VALUATION_DETAIL, body)
642        .await?;
643    let resp = futu_proto::qot_get_valuation_detail::Response::decode(frame.body.as_ref())
644        .map_err(|e| anyhow!("decode valuation_detail: {e}"))?;
645    if resp.ret_type != 0 {
646        bail!(
647            "valuation_detail ret_type={} msg={:?}",
648            resp.ret_type,
649            resp.ret_msg
650        );
651    }
652    let s2c = resp.s2c.ok_or_else(|| anyhow!("missing s2c"))?;
653    let out = ValuationDetailOut {
654        symbol: symbol.to_string(),
655        s2c,
656    };
657    Ok(serde_json::to_string_pretty(&out)?)
658}
659
660#[derive(Serialize)]
661struct ValuationPlateStockListOut {
662    symbol: String,
663    s2c: futu_proto::qot_get_valuation_plate_stock_list::S2c,
664}
665
666pub async fn get_valuation_plate_stock_list(
667    client: &Arc<FutuClient>,
668    symbol: &str,
669    valuation_type: Option<i32>,
670    next_key: Option<&str>,
671    num: Option<i32>,
672    sort_type: Option<i32>,
673    sort_id: Option<i32>,
674    filter_security: Option<&str>,
675) -> Result<String> {
676    let sec = parse_symbol(symbol)?;
677    let filter_security = match filter_security {
678        Some(symbol) if !symbol.trim().is_empty() => {
679            let sec = parse_symbol(symbol)?;
680            Some(futu_proto::qot_common::Security {
681                market: sec.market as i32,
682                code: sec.code,
683            })
684        }
685        _ => None,
686    };
687    let req = futu_proto::qot_get_valuation_plate_stock_list::Request {
688        c2s: futu_proto::qot_get_valuation_plate_stock_list::C2s {
689            security: futu_proto::qot_common::Security {
690                market: sec.market as i32,
691                code: sec.code.clone(),
692            },
693            valuation_type,
694            next_key: next_key.map(str::to_string),
695            num,
696            sort_type,
697            sort_id,
698            filter_security,
699        },
700    };
701    let body = req.encode_to_vec();
702    let frame = client
703        .request(
704            futu_core::proto_id::QOT_GET_VALUATION_PLATE_STOCK_LIST,
705            body,
706        )
707        .await?;
708    let resp =
709        futu_proto::qot_get_valuation_plate_stock_list::Response::decode(frame.body.as_ref())
710            .map_err(|e| anyhow!("decode valuation_plate_stock_list: {e}"))?;
711    if resp.ret_type != 0 {
712        bail!(
713            "valuation_plate_stock_list ret_type={} msg={:?}",
714            resp.ret_type,
715            resp.ret_msg
716        );
717    }
718    let s2c = resp.s2c.ok_or_else(|| anyhow!("missing s2c"))?;
719    let out = ValuationPlateStockListOut {
720        symbol: symbol.to_string(),
721        s2c,
722    };
723    Ok(serde_json::to_string_pretty(&out)?)
724}