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