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