Skip to main content

futu_mcp/handlers/reference/
user_security.rs

1//! mcp/handlers/reference/user_security — user_security_group / get_user_security / modify_user_security / holding_change
2//! (v1.4.110 CC Batch L: 拆自 reference.rs L329-371 + 679-790 + 1061-1102)
3
4use std::sync::Arc;
5
6use anyhow::{Result, anyhow, bail};
7use futu_net::client::FutuClient;
8use prost::Message;
9use serde::Serialize;
10
11use crate::state::parse_symbol;
12
13// ============================================================
14// get_user_security_group / Qot_GetUserSecurityGroup (CMD 3222)
15// ============================================================
16
17#[derive(Serialize)]
18struct UserSecurityGroupOut {
19    group_name: String,
20    group_type: i32,
21}
22
23/// 自选股分组列表。`group_type`:1=自定义 / 2=系统 / 3=全部。
24pub async fn get_user_security_group(client: &Arc<FutuClient>, group_type: i32) -> Result<String> {
25    let req = futu_proto::qot_get_user_security_group::Request {
26        c2s: futu_proto::qot_get_user_security_group::C2s {
27            group_type,
28            header: None, // v1.4.110 codex Slice 1 schema 占位
29        },
30    };
31    let body = req.encode_to_vec();
32    let frame = client
33        .request(futu_core::proto_id::QOT_GET_USER_SECURITY_GROUP, body)
34        .await?;
35    let resp = futu_proto::qot_get_user_security_group::Response::decode(frame.body.as_ref())
36        .map_err(|e| anyhow!("decode user_security_group: {e}"))?;
37    if resp.ret_type != 0 {
38        bail!(
39            "user_security_group ret_type={} msg={:?}",
40            resp.ret_type,
41            resp.ret_msg
42        );
43    }
44    let s2c = resp.s2c.ok_or_else(|| anyhow!("missing s2c"))?;
45    let out: Vec<UserSecurityGroupOut> = s2c
46        .group_list
47        .iter()
48        .map(|g| UserSecurityGroupOut {
49            group_name: g.group_name.clone(),
50            group_type: g.group_type,
51        })
52        .collect();
53    Ok(serde_json::to_string_pretty(&out)?)
54}
55
56// ============================================================
57// get_stock_filter / Qot_StockFilter (CMD 3215)
58// ============================================================
59
60#[derive(Serialize)]
61struct HoldingChangeOut {
62    code: String,
63    holder_name: String,
64    holding_qty: f64,
65    holding_ratio: f64,
66    change_qty: f64,
67    change_ratio: f64,
68    time: String,
69}
70
71/// 高管 / 机构 / 基金持股变动。`holder_category`: 1=机构 / 2=基金 / 3=高管。
72pub async fn get_holding_change(
73    client: &Arc<FutuClient>,
74    symbol: &str,
75    holder_category: i32,
76    begin_time: Option<&str>,
77    end_time: Option<&str>,
78) -> Result<String> {
79    let s = parse_symbol(symbol)?;
80    let req = futu_proto::qot_get_holding_change_list::Request {
81        c2s: futu_proto::qot_get_holding_change_list::C2s {
82            security: futu_proto::qot_common::Security {
83                market: s.market as i32,
84                code: s.code,
85            },
86            holder_category,
87            begin_time: begin_time.map(String::from),
88            end_time: end_time.map(String::from),
89            header: None, // v1.4.110 codex Slice 1 schema 占位
90        },
91    };
92    let body = req.encode_to_vec();
93    let frame = client
94        .request(futu_core::proto_id::QOT_GET_HOLDING_CHANGE_LIST, body)
95        .await?;
96    let resp = futu_proto::qot_get_holding_change_list::Response::decode(frame.body.as_ref())
97        .map_err(|e| anyhow!("decode holding_change: {e}"))?;
98    if resp.ret_type != 0 {
99        bail!(
100            "holding_change ret_type={} msg={:?}",
101            resp.ret_type,
102            resp.ret_msg
103        );
104    }
105    let s2c = resp.s2c.ok_or_else(|| anyhow!("missing s2c"))?;
106    let out: Vec<HoldingChangeOut> = s2c
107        .holding_change_list
108        .iter()
109        .map(|h| HoldingChangeOut {
110            code: s2c.security.code.clone(),
111            holder_name: h.holder_name.clone(),
112            holding_qty: h.holding_qty,
113            holding_ratio: h.holding_ratio,
114            change_qty: h.change_qty,
115            change_ratio: h.change_ratio,
116            time: h.time.clone(),
117        })
118        .collect();
119    Ok(serde_json::to_string_pretty(&out)?)
120}
121
122// ============================================================
123// modify_user_security / Qot_ModifyUserSecurity (CMD 3214)
124// ============================================================
125
126/// 修改自选股分组。`op`:1=ADD_INTO(加入)/ 2=DEL(从分组删)/ 3=MOVE_OUT。
127pub async fn modify_user_security(
128    client: &Arc<FutuClient>,
129    group_name: &str,
130    op: i32,
131    symbols: &[String],
132) -> Result<String> {
133    let sec_list: Vec<_> = symbols
134        .iter()
135        .map(|s| parse_symbol(s))
136        .collect::<Result<Vec<_>>>()?;
137    let proto_secs: Vec<_> = sec_list
138        .iter()
139        .map(|s| futu_proto::qot_common::Security {
140            market: s.market as i32,
141            code: s.code.clone(),
142        })
143        .collect();
144    let req = futu_proto::qot_modify_user_security::Request {
145        c2s: futu_proto::qot_modify_user_security::C2s {
146            group_name: group_name.to_string(),
147            op,
148            security_list: proto_secs,
149            header: None,
150        },
151    };
152    let body = req.encode_to_vec();
153    let frame = client
154        .request(futu_core::proto_id::QOT_MODIFY_USER_SECURITY, body)
155        .await?;
156    let resp = futu_proto::qot_modify_user_security::Response::decode(frame.body.as_ref())
157        .map_err(|e| anyhow!("decode modify_user_security: {e}"))?;
158    if resp.ret_type != 0 {
159        bail!(
160            "modify_user_security ret_type={} msg={:?}",
161            resp.ret_type,
162            resp.ret_msg
163        );
164    }
165    Ok(serde_json::to_string_pretty(&serde_json::json!({
166        "ok": true,
167        "op": op,
168        "group_name": group_name,
169        "count": symbols.len(),
170    }))?)
171}
172
173// ============================================================
174// get_user_security / Qot_GetUserSecurity (CMD 3213) — v1.4.30
175// ============================================================
176
177#[derive(Serialize)]
178struct UserSecurityOut {
179    code: String,
180    name: String,
181    lot_size: i32,
182    sec_type: i32,
183}
184
185/// 查询自选股分组下的股票列表。`group_name`:组名(用 `futu_get_user_security_group`
186/// 先列出所有分组)。
187pub async fn get_user_security(client: &Arc<FutuClient>, group_name: &str) -> Result<String> {
188    let req = futu_proto::qot_get_user_security::Request {
189        c2s: futu_proto::qot_get_user_security::C2s {
190            group_name: group_name.to_string(),
191            header: None,
192        },
193    };
194    let body = req.encode_to_vec();
195    let frame = client
196        .request(futu_core::proto_id::QOT_GET_USER_SECURITY, body)
197        .await?;
198    let resp = futu_proto::qot_get_user_security::Response::decode(frame.body.as_ref())
199        .map_err(|e| anyhow!("decode user_security: {e}"))?;
200    if resp.ret_type != 0 {
201        bail!(
202            "user_security ret_type={} msg={:?}",
203            resp.ret_type,
204            resp.ret_msg
205        );
206    }
207    let s2c = resp.s2c.ok_or_else(|| anyhow!("missing s2c"))?;
208    let out: Vec<UserSecurityOut> = s2c
209        .static_info_list
210        .iter()
211        .map(|s| UserSecurityOut {
212            code: s.basic.security.code.clone(),
213            name: s.basic.name.clone(),
214            lot_size: s.basic.lot_size,
215            sec_type: s.basic.sec_type,
216        })
217        .collect();
218    Ok(serde_json::to_string_pretty(&out)?)
219}