Skip to main content

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