futu_qot/
market_misc.rs

1// 市场杂项查询: 交易日、停牌、复权、板块、关联股票、持股变动、期权链、历史K线点
2
3use futu_core::error::{FutuError, Result};
4use futu_core::proto_id;
5use futu_net::client::FutuClient;
6
7use crate::types::{QotMarket, Security};
8
9// ===== 交易日 =====
10
11/// 交易日
12#[derive(Debug, Clone)]
13pub struct TradeDate {
14    pub time: String,
15    pub timestamp: f64,
16}
17
18/// 获取交易日列表
19pub async fn get_trade_date(
20    client: &FutuClient,
21    market: QotMarket,
22    begin_time: &str,
23    end_time: &str,
24) -> Result<Vec<TradeDate>> {
25    let req = futu_proto::qot_request_trade_date::Request {
26        c2s: futu_proto::qot_request_trade_date::C2s {
27            market: market as i32,
28            begin_time: begin_time.to_string(),
29            end_time: end_time.to_string(),
30            security: None,
31        },
32    };
33
34    let body = prost::Message::encode_to_vec(&req);
35    let resp_frame = client.request(proto_id::QOT_GET_TRADE_DATE, body).await?;
36    let resp: futu_proto::qot_request_trade_date::Response =
37        prost::Message::decode(resp_frame.body.as_ref()).map_err(FutuError::Proto)?;
38
39    if resp.ret_type != 0 {
40        return Err(FutuError::ServerError {
41            ret_type: resp.ret_type,
42            msg: resp.ret_msg.unwrap_or_default(),
43        });
44    }
45
46    let s2c = resp
47        .s2c
48        .ok_or(FutuError::Codec("missing s2c in GetTradeDate".into()))?;
49
50    Ok(s2c
51        .trade_date_list
52        .iter()
53        .map(|d| TradeDate {
54            time: d.time.clone(),
55            timestamp: d.timestamp.unwrap_or(0.0),
56        })
57        .collect())
58}
59
60// ===== 停牌 =====
61
62/// 获取停牌信息(原始 proto 响应)
63pub async fn get_suspend(
64    client: &FutuClient,
65    securities: &[Security],
66    begin_time: &str,
67    end_time: &str,
68) -> Result<futu_proto::qot_get_suspend::S2c> {
69    let req = futu_proto::qot_get_suspend::Request {
70        c2s: futu_proto::qot_get_suspend::C2s {
71            security_list: securities.iter().map(|s| s.to_proto()).collect(),
72            begin_time: begin_time.to_string(),
73            end_time: end_time.to_string(),
74        },
75    };
76
77    let body = prost::Message::encode_to_vec(&req);
78    let resp_frame = client.request(proto_id::QOT_GET_SUSPEND, body).await?;
79    let resp: futu_proto::qot_get_suspend::Response =
80        prost::Message::decode(resp_frame.body.as_ref()).map_err(FutuError::Proto)?;
81
82    if resp.ret_type != 0 {
83        return Err(FutuError::ServerError {
84            ret_type: resp.ret_type,
85            msg: resp.ret_msg.unwrap_or_default(),
86        });
87    }
88
89    resp.s2c
90        .ok_or(FutuError::Codec("missing s2c in GetSuspend".into()))
91}
92
93// ===== 复权 =====
94
95/// 获取复权信息(原始 proto 响应)
96pub async fn get_rehab(
97    client: &FutuClient,
98    security: &Security,
99) -> Result<futu_proto::qot_request_rehab::S2c> {
100    let req = futu_proto::qot_request_rehab::Request {
101        c2s: futu_proto::qot_request_rehab::C2s {
102            security: security.to_proto(),
103        },
104    };
105
106    let body = prost::Message::encode_to_vec(&req);
107    let resp_frame = client.request(proto_id::QOT_GET_REHAB, body).await?;
108    let resp: futu_proto::qot_request_rehab::Response =
109        prost::Message::decode(resp_frame.body.as_ref()).map_err(FutuError::Proto)?;
110
111    if resp.ret_type != 0 {
112        return Err(FutuError::ServerError {
113            ret_type: resp.ret_type,
114            msg: resp.ret_msg.unwrap_or_default(),
115        });
116    }
117
118    resp.s2c
119        .ok_or(FutuError::Codec("missing s2c in GetRehab".into()))
120}
121
122// ===== 板块 =====
123
124/// 板块信息
125#[derive(Debug, Clone)]
126pub struct PlateInfo {
127    pub security: Security,
128    pub name: String,
129    pub plate_type: Option<i32>,
130}
131
132impl PlateInfo {
133    pub fn from_proto(p: &futu_proto::qot_common::PlateInfo) -> Self {
134        Self {
135            security: Security::from_proto(&p.plate),
136            name: p.name.clone(),
137            plate_type: p.plate_type,
138        }
139    }
140}
141
142/// 获取板块集合
143pub async fn get_plate_set(
144    client: &FutuClient,
145    market: QotMarket,
146    plate_set_type: i32,
147) -> Result<Vec<PlateInfo>> {
148    let req = futu_proto::qot_get_plate_set::Request {
149        c2s: futu_proto::qot_get_plate_set::C2s {
150            market: market as i32,
151            plate_set_type,
152        },
153    };
154
155    let body = prost::Message::encode_to_vec(&req);
156    let resp_frame = client.request(proto_id::QOT_GET_PLATE_SET, body).await?;
157    let resp: futu_proto::qot_get_plate_set::Response =
158        prost::Message::decode(resp_frame.body.as_ref()).map_err(FutuError::Proto)?;
159
160    if resp.ret_type != 0 {
161        return Err(FutuError::ServerError {
162            ret_type: resp.ret_type,
163            msg: resp.ret_msg.unwrap_or_default(),
164        });
165    }
166
167    let s2c = resp
168        .s2c
169        .ok_or(FutuError::Codec("missing s2c in GetPlateSet".into()))?;
170
171    Ok(s2c
172        .plate_info_list
173        .iter()
174        .map(PlateInfo::from_proto)
175        .collect())
176}
177
178/// 获取板块内股票列表
179pub async fn get_plate_security(
180    client: &FutuClient,
181    plate: &Security,
182) -> Result<Vec<crate::static_info::SecurityStaticInfo>> {
183    let req = futu_proto::qot_get_plate_security::Request {
184        c2s: futu_proto::qot_get_plate_security::C2s {
185            plate: plate.to_proto(),
186            sort_field: None,
187            ascend: None,
188        },
189    };
190
191    let body = prost::Message::encode_to_vec(&req);
192    let resp_frame = client
193        .request(proto_id::QOT_GET_PLATE_SECURITY, body)
194        .await?;
195    let resp: futu_proto::qot_get_plate_security::Response =
196        prost::Message::decode(resp_frame.body.as_ref()).map_err(FutuError::Proto)?;
197
198    if resp.ret_type != 0 {
199        return Err(FutuError::ServerError {
200            ret_type: resp.ret_type,
201            msg: resp.ret_msg.unwrap_or_default(),
202        });
203    }
204
205    let s2c = resp
206        .s2c
207        .ok_or(FutuError::Codec("missing s2c in GetPlateSecurity".into()))?;
208
209    Ok(s2c
210        .static_info_list
211        .iter()
212        .map(crate::static_info::SecurityStaticInfo::from_proto)
213        .collect())
214}
215
216/// 获取股票所属板块
217pub async fn get_owner_plate(
218    client: &FutuClient,
219    securities: &[Security],
220) -> Result<futu_proto::qot_get_owner_plate::S2c> {
221    let req = futu_proto::qot_get_owner_plate::Request {
222        c2s: futu_proto::qot_get_owner_plate::C2s {
223            security_list: securities.iter().map(|s| s.to_proto()).collect(),
224        },
225    };
226
227    let body = prost::Message::encode_to_vec(&req);
228    let resp_frame = client.request(proto_id::QOT_GET_OWNER_PLATE, body).await?;
229    let resp: futu_proto::qot_get_owner_plate::Response =
230        prost::Message::decode(resp_frame.body.as_ref()).map_err(FutuError::Proto)?;
231
232    if resp.ret_type != 0 {
233        return Err(FutuError::ServerError {
234            ret_type: resp.ret_type,
235            msg: resp.ret_msg.unwrap_or_default(),
236        });
237    }
238
239    resp.s2c
240        .ok_or(FutuError::Codec("missing s2c in GetOwnerPlate".into()))
241}
242
243/// 获取关联股票
244pub async fn get_reference(
245    client: &FutuClient,
246    security: &Security,
247    reference_type: i32,
248) -> Result<Vec<crate::static_info::SecurityStaticInfo>> {
249    let req = futu_proto::qot_get_reference::Request {
250        c2s: futu_proto::qot_get_reference::C2s {
251            security: security.to_proto(),
252            reference_type,
253        },
254    };
255
256    let body = prost::Message::encode_to_vec(&req);
257    let resp_frame = client.request(proto_id::QOT_GET_REFERENCE, body).await?;
258    let resp: futu_proto::qot_get_reference::Response =
259        prost::Message::decode(resp_frame.body.as_ref()).map_err(FutuError::Proto)?;
260
261    if resp.ret_type != 0 {
262        return Err(FutuError::ServerError {
263            ret_type: resp.ret_type,
264            msg: resp.ret_msg.unwrap_or_default(),
265        });
266    }
267
268    let s2c = resp
269        .s2c
270        .ok_or(FutuError::Codec("missing s2c in GetReference".into()))?;
271
272    Ok(s2c
273        .static_info_list
274        .iter()
275        .map(crate::static_info::SecurityStaticInfo::from_proto)
276        .collect())
277}
278
279// ===== 期权链 =====
280
281/// 获取期权链(原始 proto 响应)
282pub async fn get_option_chain(
283    client: &FutuClient,
284    owner: &Security,
285    begin_time: &str,
286    end_time: &str,
287    option_type: Option<i32>,
288    option_cond: Option<i32>,
289) -> Result<futu_proto::qot_get_option_chain::S2c> {
290    let req = futu_proto::qot_get_option_chain::Request {
291        c2s: futu_proto::qot_get_option_chain::C2s {
292            owner: owner.to_proto(),
293            index_option_type: None,
294            r#type: option_type,
295            condition: option_cond,
296            begin_time: begin_time.to_string(),
297            end_time: end_time.to_string(),
298            data_filter: None,
299        },
300    };
301
302    let body = prost::Message::encode_to_vec(&req);
303    let resp_frame = client.request(proto_id::QOT_GET_OPTION_CHAIN, body).await?;
304    let resp: futu_proto::qot_get_option_chain::Response =
305        prost::Message::decode(resp_frame.body.as_ref()).map_err(FutuError::Proto)?;
306
307    if resp.ret_type != 0 {
308        return Err(FutuError::ServerError {
309            ret_type: resp.ret_type,
310            msg: resp.ret_msg.unwrap_or_default(),
311        });
312    }
313
314    resp.s2c
315        .ok_or(FutuError::Codec("missing s2c in GetOptionChain".into()))
316}
317
318// ===== 持股变动 =====
319
320/// 获取持股变动(原始 proto 响应)
321pub async fn get_holding_change_list(
322    client: &FutuClient,
323    security: &Security,
324    holder_category: i32,
325    begin_time: Option<&str>,
326    end_time: Option<&str>,
327) -> Result<futu_proto::qot_get_holding_change_list::S2c> {
328    let req = futu_proto::qot_get_holding_change_list::Request {
329        c2s: futu_proto::qot_get_holding_change_list::C2s {
330            security: security.to_proto(),
331            holder_category,
332            begin_time: begin_time.map(|s| s.to_string()),
333            end_time: end_time.map(|s| s.to_string()),
334        },
335    };
336
337    let body = prost::Message::encode_to_vec(&req);
338    let resp_frame = client
339        .request(proto_id::QOT_GET_HOLDING_CHANGE_LIST, body)
340        .await?;
341    let resp: futu_proto::qot_get_holding_change_list::Response =
342        prost::Message::decode(resp_frame.body.as_ref()).map_err(FutuError::Proto)?;
343
344    if resp.ret_type != 0 {
345        return Err(FutuError::ServerError {
346            ret_type: resp.ret_type,
347            msg: resp.ret_msg.unwrap_or_default(),
348        });
349    }
350
351    resp.s2c.ok_or(FutuError::Codec(
352        "missing s2c in GetHoldingChangeList".into(),
353    ))
354}
355
356// ===== 历史K线点 =====
357// NOTE: qot_get_history_kl_points proto has been removed; this function is no longer available.
358
359// ===== 订阅查询与推送注册 =====
360
361/// 查询订阅信息
362pub async fn get_sub_info(
363    client: &FutuClient,
364    is_req_all_conn: bool,
365) -> Result<futu_proto::qot_get_sub_info::S2c> {
366    let req = futu_proto::qot_get_sub_info::Request {
367        c2s: futu_proto::qot_get_sub_info::C2s {
368            is_req_all_conn: Some(is_req_all_conn),
369        },
370    };
371
372    let body = prost::Message::encode_to_vec(&req);
373    let resp_frame = client.request(proto_id::QOT_GET_SUB_INFO, body).await?;
374    let resp: futu_proto::qot_get_sub_info::Response =
375        prost::Message::decode(resp_frame.body.as_ref()).map_err(FutuError::Proto)?;
376
377    if resp.ret_type != 0 {
378        return Err(FutuError::ServerError {
379            ret_type: resp.ret_type,
380            msg: resp.ret_msg.unwrap_or_default(),
381        });
382    }
383
384    resp.s2c
385        .ok_or(FutuError::Codec("missing s2c in GetSubInfo".into()))
386}
387
388/// 注册/取消行情推送
389pub async fn reg_qot_push(
390    client: &FutuClient,
391    securities: &[Security],
392    sub_types: &[crate::types::SubType],
393    is_reg: bool,
394    is_first_push: Option<bool>,
395) -> Result<()> {
396    let req = futu_proto::qot_reg_qot_push::Request {
397        c2s: futu_proto::qot_reg_qot_push::C2s {
398            security_list: securities.iter().map(|s| s.to_proto()).collect(),
399            sub_type_list: sub_types.iter().map(|t| *t as i32).collect(),
400            rehab_type_list: vec![],
401            is_reg_or_un_reg: is_reg,
402            is_first_push,
403        },
404    };
405
406    let body = prost::Message::encode_to_vec(&req);
407    let resp_frame = client.request(proto_id::QOT_REG_QOT_PUSH, body).await?;
408    let resp: futu_proto::qot_reg_qot_push::Response =
409        prost::Message::decode(resp_frame.body.as_ref()).map_err(FutuError::Proto)?;
410
411    if resp.ret_type != 0 {
412        return Err(FutuError::ServerError {
413            ret_type: resp.ret_type,
414            msg: resp.ret_msg.unwrap_or_default(),
415        });
416    }
417
418    Ok(())
419}