Skip to main content

futu_mcp/handlers/reference/
queries.rs

1//! mcp/handlers/reference/queries — stock_filter / trading_days / rehab / suspend / history_kl_quota
2//! (v1.4.110 CC Batch L: 拆自 reference.rs L372-678)
3
4use std::sync::Arc;
5
6use anyhow::{Result, anyhow, bail};
7use futu_net::client::FutuClient;
8use futu_qot::page_bounds::validate_begin_num;
9use prost::Message;
10use serde::Serialize;
11
12use crate::state::parse_symbol;
13
14#[derive(Serialize)]
15struct StockFilterOut {
16    code: String,
17    name: String,
18}
19
20/// 条件选股(最小可用)。这里只支持 `market + begin + num` 不挂任何 filter,
21/// 返回市场里指定区间的股票列表。真要按 PE / 市值 / 成交量等过滤的用户,
22/// 走 REST `/api/stock-filter` 直接传完整 JSON body(`baseFilterList` /
23/// `accumulateFilterList` / `financialFilterList` 等嵌套结构)。
24///
25/// v1.4.106 codex 0635 ζ36 F3+F4: 不再静默 clamp num. 越界 (begin<0 /
26/// num∉[1, 200]) 走 `Err` 让调用方看到清晰错误. 跨 surface 一致.
27pub async fn get_stock_filter(
28    client: &Arc<FutuClient>,
29    market: i32,
30    begin: i32,
31    num: i32,
32) -> Result<String> {
33    let bounds =
34        validate_begin_num(begin, num, 200, "stock_filter").map_err(|e| anyhow!("{}", e))?;
35    let req = futu_proto::qot_stock_filter::Request {
36        c2s: futu_proto::qot_stock_filter::C2s {
37            begin: bounds.begin,
38            num: bounds.num,
39            market,
40            plate: None,
41            base_filter_list: vec![],
42            accumulate_filter_list: vec![],
43            financial_filter_list: vec![],
44            pattern_filter_list: vec![],
45            custom_indicator_filter_list: vec![],
46            header: None,
47        },
48    };
49    let body = req.encode_to_vec();
50    let frame = client
51        .request(futu_core::proto_id::QOT_STOCK_FILTER, body)
52        .await?;
53    let resp = futu_proto::qot_stock_filter::Response::decode(frame.body.as_ref())
54        .map_err(|e| anyhow!("decode stock_filter: {e}"))?;
55    if resp.ret_type != 0 {
56        bail!(
57            "stock_filter ret_type={} msg={:?}",
58            resp.ret_type,
59            resp.ret_msg
60        );
61    }
62    let s2c = resp.s2c.ok_or_else(|| anyhow!("missing s2c"))?;
63    let out: Vec<StockFilterOut> = s2c
64        .data_list
65        .iter()
66        .map(|d| StockFilterOut {
67            code: d.security.code.clone(),
68            name: d.name.clone(),
69        })
70        .collect();
71    Ok(serde_json::to_string_pretty(&serde_json::json!({
72        "last_page": s2c.last_page,
73        "all_count": s2c.all_count,
74        "stocks": out,
75    }))?)
76}
77
78// ============================================================
79// get_trading_days / Qot_RequestTradeDate (CMD 3219) — v1.4.30
80// ============================================================
81
82#[derive(Serialize)]
83struct TradingDayOut {
84    time: String,
85    trade_date_type: i32,
86}
87
88/// 查询交易日列表。`market`:1=HK / 2=US / 3=CN / 4=NT / 5=ST / 6=JP_Future / 7=SG_Future。
89/// `begin_time` / `end_time` 格式 `yyyy-MM-dd`。注意该交易日通过自然日去除周末 + 节假日
90/// 得到,不含临时休市。
91pub async fn get_trading_days(
92    client: &Arc<FutuClient>,
93    market: i32,
94    begin_time: &str,
95    end_time: &str,
96) -> Result<String> {
97    let req = futu_proto::qot_request_trade_date::Request {
98        c2s: futu_proto::qot_request_trade_date::C2s {
99            market,
100            begin_time: begin_time.to_string(),
101            end_time: end_time.to_string(),
102            security: None,
103            header: None,
104        },
105    };
106    let body = req.encode_to_vec();
107    let frame = client
108        .request(futu_core::proto_id::QOT_REQUEST_TRADE_DATE, body)
109        .await?;
110    let resp = futu_proto::qot_request_trade_date::Response::decode(frame.body.as_ref())
111        .map_err(|e| anyhow!("decode trading_days: {e}"))?;
112    if resp.ret_type != 0 {
113        bail!(
114            "trading_days ret_type={} msg={:?}",
115            resp.ret_type,
116            resp.ret_msg
117        );
118    }
119    let s2c = resp.s2c.ok_or_else(|| anyhow!("missing s2c"))?;
120    let out: Vec<TradingDayOut> = s2c
121        .trade_date_list
122        .iter()
123        .map(|t| TradingDayOut {
124            time: t.time.clone(),
125            trade_date_type: t.trade_date_type.unwrap_or(0),
126        })
127        .collect();
128    Ok(serde_json::to_string_pretty(&out)?)
129}
130
131// ============================================================
132// get_rehab / Qot_RequestRehab (CMD 3105) — v1.4.30
133// ============================================================
134
135#[derive(Serialize)]
136struct RehabOut {
137    time: String,
138    /// 前复权因子 A / B(长期 K 线按前复权对齐价格用)
139    fwd_factor_a: f64,
140    fwd_factor_b: f64,
141    /// 后复权因子 A / B
142    bwd_factor_a: f64,
143    bwd_factor_b: f64,
144    /// 公司行动组合标志位(按位标记送股、配股、分红、转增等事件是否发生)
145    company_act_flag: i64,
146    /// 常见事件参数(`None` 表示当次无此事件)
147    dividend: Option<f64>,
148    sp_dividend: Option<f64>,
149    bonus_base: Option<i32>,
150    bonus_ert: Option<i32>,
151    transfer_base: Option<i32>,
152    transfer_ert: Option<i32>,
153    allot_base: Option<i32>,
154    allot_ert: Option<i32>,
155    allot_price: Option<f64>,
156}
157
158/// 查询复权因子。长期 K 线分析 / 回测必用。
159pub async fn get_rehab(client: &Arc<FutuClient>, symbol: &str) -> Result<String> {
160    let s = parse_symbol(symbol)?;
161    let req = futu_proto::qot_request_rehab::Request {
162        c2s: futu_proto::qot_request_rehab::C2s {
163            security: futu_proto::qot_common::Security {
164                market: s.market as i32,
165                code: s.code,
166            },
167            header: None, // v1.4.110 codex Slice 1 schema 占位
168        },
169    };
170    let body = req.encode_to_vec();
171    let frame = client
172        .request(futu_core::proto_id::QOT_REQUEST_REHAB, body)
173        .await?;
174    let resp = futu_proto::qot_request_rehab::Response::decode(frame.body.as_ref())
175        .map_err(|e| anyhow!("decode rehab: {e}"))?;
176    if resp.ret_type != 0 {
177        bail!("rehab ret_type={} msg={:?}", resp.ret_type, resp.ret_msg);
178    }
179    let s2c = resp.s2c.ok_or_else(|| anyhow!("missing s2c"))?;
180    let out: Vec<RehabOut> = s2c
181        .rehab_list
182        .iter()
183        .map(|r| RehabOut {
184            time: r.time.clone(),
185            fwd_factor_a: r.fwd_factor_a,
186            fwd_factor_b: r.fwd_factor_b,
187            bwd_factor_a: r.bwd_factor_a,
188            bwd_factor_b: r.bwd_factor_b,
189            company_act_flag: r.company_act_flag,
190            dividend: r.dividend,
191            sp_dividend: r.sp_dividend,
192            bonus_base: r.bonus_base,
193            bonus_ert: r.bonus_ert,
194            transfer_base: r.transfer_base,
195            transfer_ert: r.transfer_ert,
196            allot_base: r.allot_base,
197            allot_ert: r.allot_ert,
198            allot_price: r.allot_price,
199        })
200        .collect();
201    Ok(serde_json::to_string_pretty(&out)?)
202}
203
204// ============================================================
205// get_suspend / Qot_GetSuspend (CMD 3201) — v1.4.30
206// ============================================================
207
208#[derive(Serialize)]
209struct SuspendDayOut {
210    time: String,
211}
212
213#[derive(Serialize)]
214struct SecuritySuspendOut {
215    code: String,
216    suspend_list: Vec<SuspendDayOut>,
217}
218
219/// 查询股票停牌日。`begin_time` / `end_time` 格式 `yyyy-MM-dd`。
220pub async fn get_suspend(
221    client: &Arc<FutuClient>,
222    symbols: &[String],
223    begin_time: &str,
224    end_time: &str,
225) -> Result<String> {
226    let sec_list: Vec<_> = symbols
227        .iter()
228        .map(|s| parse_symbol(s))
229        .collect::<Result<Vec<_>>>()?;
230    let proto_secs: Vec<_> = sec_list
231        .iter()
232        .map(|s| futu_proto::qot_common::Security {
233            market: s.market as i32,
234            code: s.code.clone(),
235        })
236        .collect();
237    let req = futu_proto::qot_get_suspend::Request {
238        c2s: futu_proto::qot_get_suspend::C2s {
239            security_list: proto_secs,
240            begin_time: begin_time.to_string(),
241            end_time: end_time.to_string(),
242            header: None,
243        },
244    };
245    let body = req.encode_to_vec();
246    let frame = client
247        .request(futu_core::proto_id::QOT_GET_SUSPEND, body)
248        .await?;
249    let resp = futu_proto::qot_get_suspend::Response::decode(frame.body.as_ref())
250        .map_err(|e| anyhow!("decode suspend: {e}"))?;
251    if resp.ret_type != 0 {
252        bail!("suspend ret_type={} msg={:?}", resp.ret_type, resp.ret_msg);
253    }
254    let s2c = resp.s2c.ok_or_else(|| anyhow!("missing s2c"))?;
255    let out: Vec<SecuritySuspendOut> = s2c
256        .security_suspend_list
257        .iter()
258        .map(|ss| SecuritySuspendOut {
259            code: ss.security.code.clone(),
260            suspend_list: ss
261                .suspend_list
262                .iter()
263                .map(|s| SuspendDayOut {
264                    time: s.time.clone(),
265                })
266                .collect(),
267        })
268        .collect();
269    Ok(serde_json::to_string_pretty(&out)?)
270}
271
272// ============================================================
273// v1.4.30 P2 完成品:history_kl_quota / holding_change / modify_user_security
274//         code_change / set_price_reminder / get_price_reminder /
275//         option_expiration_date
276// ============================================================
277
278#[derive(Serialize)]
279struct HistoryKlQuotaOut {
280    used_quota: i32,
281    remain_quota: i32,
282    details_count: usize,
283}
284
285/// 查历史 K 线下载配额(`use_quota` + `remain_quota`)。
286/// `get_detail=false` 只要概要,`true` 返回每只股票的下载时间(但我们只给
287/// 计数避免超长 response,详情用 REST)。
288pub async fn get_history_kl_quota(client: &Arc<FutuClient>, get_detail: bool) -> Result<String> {
289    let req = futu_proto::qot_request_history_kl_quota::Request {
290        c2s: futu_proto::qot_request_history_kl_quota::C2s {
291            b_get_detail: Some(get_detail),
292            header: None,
293        },
294    };
295    let body = req.encode_to_vec();
296    let frame = client
297        .request(futu_core::proto_id::QOT_REQUEST_HISTORY_KL_QUOTA, body)
298        .await?;
299    let resp = futu_proto::qot_request_history_kl_quota::Response::decode(frame.body.as_ref())
300        .map_err(|e| anyhow!("decode history_kl_quota: {e}"))?;
301    if resp.ret_type != 0 {
302        bail!(
303            "history_kl_quota ret_type={} msg={:?}",
304            resp.ret_type,
305            resp.ret_msg
306        );
307    }
308    let s = resp.s2c.ok_or_else(|| anyhow!("missing s2c"))?;
309    let out = HistoryKlQuotaOut {
310        used_quota: s.used_quota,
311        remain_quota: s.remain_quota,
312        details_count: s.detail_list.len(),
313    };
314    Ok(serde_json::to_string_pretty(&out)?)
315}
316
317// ============================================================
318// get_holding_change / Qot_GetHoldingChangeList (CMD 3208)
319// ============================================================