Skip to main content

futu_mcp/handlers/core/
system.rs

1//! mcp/handlers/core/system — global_state + user_info + quote_rights + delay_statistics
2//! (v1.4.110 CC Batch N: 拆自 core.rs L485-706)
3
4use std::sync::Arc;
5
6use anyhow::{Result, anyhow, bail};
7use futu_net::client::FutuClient;
8use futu_qot::quote_rights::{QuoteRightsProfile, SYS_QUERY_GET_QUOTE_RIGHTS_PROFILE};
9use futu_surface_spec::endpoints::get_delay_statistics::{
10    DEFAULT_QOT_PUSH_STAGE, default_segment_list_vec, default_type_list_vec,
11};
12use prost::Message;
13use serde::Serialize;
14
15#[derive(Serialize)]
16struct GlobalStateOut {
17    market_hk: i32,
18    market_us: i32,
19    market_sh: i32,
20    market_sz: i32,
21    market_hk_future: i32,
22    market_us_future: Option<i32>,
23    market_sg_future: Option<i32>,
24    market_jp_future: Option<i32>,
25    market_sg: Option<i32>,
26    market_my: Option<i32>,
27    market_jp: Option<i32>,
28    qot_logined: bool,
29    trd_logined: bool,
30    server_ver: i32,
31    server_build_no: i32,
32    server_time: i64,
33    conn_id: Option<u64>,
34    // v1.4.98 external reviewer BUG-004 fix: 12 missing fields 补齐
35    time: i64, // proto required field, REST exposed as `time`
36    local_time: Option<f64>,
37    program_status: Option<serde_json::Value>, // ProgramStatus message (proto serde)
38    qot_svr_ip_addr: Option<String>,
39    trd_svr_ip_addr: Option<String>,
40    market_bond: Option<i32>,
41    market_global_index: Option<i32>,
42    market_sg_security: Option<i32>,
43    market_stock_connect: Option<i32>,
44    market_digital_ccy: Option<i32>,
45    market_treasury_yield: Option<i32>,
46    market_fund: Option<i32>,
47}
48
49/// 获取全局状态(各市场交易状态 + 登录状态 + 服务器版本 + 连接 ID)
50pub async fn get_global_state(client: &Arc<FutuClient>) -> Result<String> {
51    let req = futu_proto::get_global_state::Request {
52        c2s: futu_proto::get_global_state::C2s { user_id: 0 },
53    };
54    let body = req.encode_to_vec();
55    let frame = client
56        .request(futu_core::proto_id::GET_GLOBAL_STATE, body)
57        .await?;
58    let resp = futu_proto::get_global_state::Response::decode(frame.body.as_ref())
59        .map_err(|e| anyhow!("decode global_state: {e}"))?;
60    if resp.ret_type != 0 {
61        bail!(
62            "global_state ret_type={} msg={:?}",
63            resp.ret_type,
64            resp.ret_msg
65        );
66    }
67    let s = resp.s2c.ok_or_else(|| anyhow!("missing s2c"))?;
68    let out = GlobalStateOut {
69        market_hk: s.market_hk,
70        market_us: s.market_us,
71        market_sh: s.market_sh,
72        market_sz: s.market_sz,
73        market_hk_future: s.market_hk_future,
74        market_us_future: s.market_us_future,
75        market_sg_future: s.market_sg_future,
76        market_jp_future: s.market_jp_future,
77        market_sg: s.market_sg,
78        market_my: s.market_my,
79        market_jp: s.market_jp,
80        qot_logined: s.qot_logined,
81        trd_logined: s.trd_logined,
82        server_ver: s.server_ver,
83        server_build_no: s.server_build_no,
84        server_time: s.time,
85        conn_id: s.conn_id,
86        // v1.4.98 external reviewer BUG-004 fix: 12 fields
87        time: s.time,
88        local_time: s.local_time,
89        program_status: s.program_status.and_then(|p| serde_json::to_value(p).ok()),
90        qot_svr_ip_addr: s.qot_svr_ip_addr,
91        trd_svr_ip_addr: s.trd_svr_ip_addr,
92        market_bond: s.market_bond,
93        market_global_index: s.market_global_index,
94        market_sg_security: s.market_sg_security,
95        market_stock_connect: s.market_stock_connect,
96        market_digital_ccy: s.market_digital_ccy,
97        market_treasury_yield: s.market_treasury_yield,
98        market_fund: s.market_fund,
99    };
100    Ok(serde_json::to_string_pretty(&out)?)
101}
102
103#[derive(Serialize)]
104struct UserInfoOut {
105    nick_name: Option<String>,
106    user_id: Option<i64>,
107    user_attribution: Option<i32>,
108    hk_qot_right: Option<i32>,
109    us_qot_right: Option<i32>,
110    cn_qot_right: Option<i32>,
111    cc_qot_right: Option<i32>,
112    sg_stock_qot_right: Option<i32>,
113    my_stock_qot_right: Option<i32>,
114    jp_stock_qot_right: Option<i32>,
115    sub_quota: Option<i32>,
116    history_kl_quota: Option<i32>,
117}
118
119/// 获取用户信息(昵称、各市场行情权限、订阅配额、历史 K 线配额)
120pub async fn get_user_info(client: &Arc<FutuClient>) -> Result<String> {
121    let req = futu_proto::get_user_info::Request {
122        c2s: futu_proto::get_user_info::C2s { flag: None },
123    };
124    let body = req.encode_to_vec();
125    let frame = client
126        .request(futu_core::proto_id::GET_USER_INFO, body)
127        .await?;
128    let resp = futu_proto::get_user_info::Response::decode(frame.body.as_ref())
129        .map_err(|e| anyhow!("decode user_info: {e}"))?;
130    if resp.ret_type != 0 {
131        bail!(
132            "user_info ret_type={} msg={:?}",
133            resp.ret_type,
134            resp.ret_msg
135        );
136    }
137    let s = resp.s2c.ok_or_else(|| anyhow!("missing s2c"))?;
138    let out = UserInfoOut {
139        nick_name: s.nick_name,
140        user_id: s.user_id,
141        user_attribution: s.user_attribution,
142        hk_qot_right: s.hk_qot_right,
143        us_qot_right: s.us_qot_right,
144        cn_qot_right: s.cn_qot_right,
145        cc_qot_right: s.cc_qot_right,
146        sg_stock_qot_right: s.sg_stock_qot_right,
147        my_stock_qot_right: s.my_stock_qot_right,
148        jp_stock_qot_right: s.jp_stock_qot_right,
149        sub_quota: s.sub_quota,
150        history_kl_quota: s.history_kl_quota,
151    };
152    Ok(serde_json::to_string_pretty(&out)?)
153}
154
155async fn refresh_quote_rights(client: &Arc<FutuClient>) -> Result<()> {
156    let req = futu_proto::test_cmd::Request {
157        c2s: futu_proto::test_cmd::C2s {
158            cmd: "request_highest_quote_right".to_string(),
159            param_str: None,
160            param_bytes: None,
161        },
162    };
163    let frame = client
164        .request(futu_core::proto_id::TEST_CMD, req.encode_to_vec())
165        .await?;
166    let resp = futu_proto::test_cmd::Response::decode(frame.body.as_ref())
167        .map_err(|e| anyhow!("decode request_highest_quote_right: {e}"))?;
168    if resp.ret_type != 0 {
169        bail!(
170            "request_highest_quote_right ret_type={} msg={:?}",
171            resp.ret_type,
172            resp.ret_msg
173        );
174    }
175    Ok(())
176}
177
178/// 获取行情权限概览(C++ OpenD GUI 风格分组)
179pub async fn get_quote_rights(client: &Arc<FutuClient>, refresh: bool) -> Result<String> {
180    if refresh {
181        refresh_quote_rights(client).await?;
182    }
183    let req = futu_proto::test_cmd::Request {
184        c2s: futu_proto::test_cmd::C2s {
185            cmd: SYS_QUERY_GET_QUOTE_RIGHTS_PROFILE.to_string(),
186            param_str: None,
187            param_bytes: None,
188        },
189    };
190    let frame = client
191        .request(futu_core::proto_id::TEST_CMD, req.encode_to_vec())
192        .await?;
193    let resp = futu_proto::test_cmd::Response::decode(frame.body.as_ref())
194        .map_err(|e| anyhow!("decode {SYS_QUERY_GET_QUOTE_RIGHTS_PROFILE}: {e}"))?;
195    if resp.ret_type != 0 {
196        bail!(
197            "{SYS_QUERY_GET_QUOTE_RIGHTS_PROFILE} ret_type={} msg={:?}",
198            resp.ret_type,
199            resp.ret_msg
200        );
201    }
202    let json = resp
203        .s2c
204        .and_then(|s| s.result_str)
205        .ok_or_else(|| anyhow!("{SYS_QUERY_GET_QUOTE_RIGHTS_PROFILE}: missing result_str"))?;
206    let profile: QuoteRightsProfile = serde_json::from_str(&json)
207        .map_err(|e| anyhow!("parse {SYS_QUERY_GET_QUOTE_RIGHTS_PROFILE} profile: {e}"))?;
208    Ok(serde_json::to_string_pretty(&profile)?)
209}
210
211/// 获取延迟统计(行情推送 / 请求 / 下单三类延迟分布)
212///
213/// 各类分布桶数可能很多(100+ segment),这里只返概要计数 —— 需要原始样本
214/// 的用户直接走 REST `/api/delay-statistics`。
215pub async fn get_delay_statistics(client: &Arc<FutuClient>) -> Result<String> {
216    let req = futu_proto::get_delay_statistics::Request {
217        c2s: futu_proto::get_delay_statistics::C2s {
218            type_list: default_type_list_vec(),
219            qot_push_stage: Some(DEFAULT_QOT_PUSH_STAGE),
220            segment_list: default_segment_list_vec(),
221        },
222    };
223    let body = req.encode_to_vec();
224    let frame = client
225        .request(futu_core::proto_id::GET_DELAY_STATISTICS, body)
226        .await?;
227    let resp = futu_proto::get_delay_statistics::Response::decode(frame.body.as_ref())
228        .map_err(|e| anyhow!("decode delay_statistics: {e}"))?;
229    if resp.ret_type != 0 {
230        bail!(
231            "delay_statistics ret_type={} msg={:?}",
232            resp.ret_type,
233            resp.ret_msg
234        );
235    }
236    let s = resp.s2c.ok_or_else(|| anyhow!("missing s2c"))?;
237    Ok(serde_json::to_string_pretty(&serde_json::json!({
238        "qot_push_statistics_list_len": s.qot_push_statistics_list.len(),
239        "req_reply_statistics_list_len": s.req_reply_statistics_list.len(),
240        "place_order_statistics_list_len": s.place_order_statistics_list.len(),
241        "_note": "summary only; for raw per-segment samples use REST /api/delay-statistics",
242    }))?)
243}
244
245// ============================================================
246// v1.4.30 P2: query_subscription / unsubscribe / unsubscribe_all
247// ============================================================