1use std::sync::Arc;
5
6use anyhow::{Result, anyhow, bail};
7use futu_net::client::FutuClient;
8use prost::Message;
9
10use crate::state::parse_symbol;
11
12pub async fn subscribe(
14 client: &Arc<FutuClient>,
15 symbols: &[String],
16 sub_types: &[i32],
17 is_first_push: bool,
18 is_reg_push: bool,
19 extended_time: Option<bool>,
20 session: Option<i32>,
21 is_sub_order_book_detail: Option<bool>,
22) -> Result<String> {
23 if let Err(err) = futu_qot::subscription_plan::SubscribeOptionsPlan::from_raw(
24 session,
25 extended_time,
26 is_sub_order_book_detail,
27 ) {
28 match err {
29 futu_qot::subscription_plan::SubscribePlanError::OvernightSessionUnsupported => {
30 bail!(
31 "subscribe: session=4 (OVERNIGHT) is not supported by QOT subscribe; \
32 use 2=ETH or 3=ALL"
33 );
34 }
35 }
36 }
37 let sec_list: Vec<_> = symbols
38 .iter()
39 .map(|s| parse_symbol(s))
40 .collect::<Result<Vec<_>>>()?;
41 let proto_secs: Vec<_> = sec_list
42 .iter()
43 .map(|s| futu_proto::qot_common::Security {
44 market: s.market as i32,
45 code: s.code.clone(),
46 })
47 .collect();
48 let req = futu_proto::qot_sub::Request {
49 c2s: futu_proto::qot_sub::C2s {
50 security_list: proto_secs,
51 sub_type_list: sub_types.to_vec(),
52 is_sub_or_un_sub: true, is_reg_or_un_reg_push: Some(is_reg_push),
54 reg_push_rehab_type_list: vec![],
55 is_first_push: Some(is_first_push),
56 is_unsub_all: None,
57 is_sub_order_book_detail,
58 extended_time,
59 session,
60 header: None,
61 },
62 };
63 let body = req.encode_to_vec();
64 let frame = client.request(futu_core::proto_id::QOT_SUB, body).await?;
65 let resp = futu_proto::qot_sub::Response::decode(frame.body.as_ref())
66 .map_err(|e| anyhow!("decode subscribe: {e}"))?;
67 if resp.ret_type != 0 {
68 bail!(
69 "subscribe ret_type={} msg={:?}",
70 resp.ret_type,
71 resp.ret_msg
72 );
73 }
74 Ok(serde_json::to_string_pretty(&serde_json::json!({
75 "ok": true,
76 "subscribed_symbols": symbols,
77 "sub_types": sub_types,
78 "is_first_push": is_first_push,
79 "is_reg_push": is_reg_push,
80 "extended_time": extended_time,
81 "session": session,
82 "is_sub_order_book_detail": is_sub_order_book_detail,
83 }))?)
84}
85
86pub async fn unsubscribe(
89 client: &Arc<FutuClient>,
90 symbols: &[String],
91 sub_types: &[i32],
92 unsub_all: bool,
93) -> Result<String> {
94 let sec_list: Vec<_> = if unsub_all {
95 Vec::new()
96 } else {
97 symbols
98 .iter()
99 .map(|s| parse_symbol(s))
100 .collect::<Result<Vec<_>>>()?
101 };
102 let proto_secs: Vec<_> = sec_list
103 .iter()
104 .map(|s| futu_proto::qot_common::Security {
105 market: s.market as i32,
106 code: s.code.clone(),
107 })
108 .collect();
109 let req = futu_proto::qot_sub::Request {
110 c2s: futu_proto::qot_sub::C2s {
111 security_list: proto_secs,
112 sub_type_list: sub_types.to_vec(),
113 is_sub_or_un_sub: false, is_reg_or_un_reg_push: Some(false),
115 reg_push_rehab_type_list: vec![],
116 is_first_push: None,
117 is_unsub_all: Some(unsub_all),
118 is_sub_order_book_detail: None,
119 extended_time: None,
120 session: None,
121 header: None,
122 },
123 };
124 let body = req.encode_to_vec();
125 let frame = client.request(futu_core::proto_id::QOT_SUB, body).await?;
126 let resp = futu_proto::qot_sub::Response::decode(frame.body.as_ref())
127 .map_err(|e| anyhow!("decode unsubscribe: {e}"))?;
128 if resp.ret_type != 0 {
129 bail!(
130 "unsubscribe ret_type={} msg={:?}",
131 resp.ret_type,
132 resp.ret_msg
133 );
134 }
135 Ok(serde_json::to_string_pretty(&serde_json::json!({
136 "ok": true,
137 "unsub_all": unsub_all,
138 "count": symbols.len(),
139 }))?)
140}
141
142pub async fn get_token_state(client: &Arc<FutuClient>, app_id: Option<&str>) -> Result<String> {
148 use futu_backend::proto_internal::futu_token_state;
149 let req = futu_token_state::DaemonGetTokenStateReq {
150 c2s: futu_token_state::daemon_get_token_state_req::C2s {
151 app_id: app_id.map(|s| s.to_string()),
152 },
153 };
154 let body = req.encode_to_vec();
155 let frame = client
156 .request(futu_core::proto_id::GET_TOKEN_STATE, body)
157 .await?;
158 let resp = futu_token_state::DaemonGetTokenStateRsp::decode(frame.body.as_ref())
159 .map_err(|e| anyhow!("decode token_state: {e}"))?;
160 if resp.ret_type != 0 {
161 bail!(
162 "token_state ret_type={} msg={:?}",
163 resp.ret_type,
164 resp.ret_msg
165 );
166 }
167 let s = resp.s2c.ok_or_else(|| anyhow!("missing s2c"))?;
168 Ok(serde_json::to_string_pretty(&serde_json::json!({
169 "nn_token_enable": s.nn_token_enable,
170 "nn_token_bind": s.nn_token_bind,
171 "mm_token_enable": s.mm_token_enable,
172 "mm_token_bind": s.mm_token_bind,
173 }))?)
174}
175
176pub async fn get_risk_free_rate(client: &Arc<FutuClient>) -> Result<String> {
181 use futu_backend::proto_internal::risk_free_rate;
182 let req = risk_free_rate::DaemonGetRiskFreeRateReq {
183 c2s: risk_free_rate::daemon_get_risk_free_rate_req::C2s { rate_time: None },
184 };
185 let body = req.encode_to_vec();
186 let frame = client
187 .request(futu_core::proto_id::QOT_GET_RISK_FREE_RATE, body)
188 .await?;
189 let resp = risk_free_rate::DaemonGetRiskFreeRateRsp::decode(frame.body.as_ref())
190 .map_err(|e| anyhow!("decode risk_free_rate: {e}"))?;
191 if resp.ret_type != 0 {
192 bail!(
193 "risk_free_rate ret_type={} msg={:?}",
194 resp.ret_type,
195 resp.ret_msg
196 );
197 }
198 let s = resp.s2c.ok_or_else(|| anyhow!("missing s2c"))?;
199 Ok(serde_json::to_string_pretty(&serde_json::json!({
200 "hk_rate_pct": s.hk_rate_pct,
201 "us_rate_pct": s.us_rate_pct,
202 "jp_rate_pct": s.jp_rate_pct,
203 "update_time": s.update_time,
204 "hk_rate_raw": s.hk_rate_raw,
205 "us_rate_raw": s.us_rate_raw,
206 "jp_rate_raw": s.jp_rate_raw,
207 }))?)
208}
209
210pub async fn get_spread_table(client: &Arc<FutuClient>) -> Result<String> {
212 use futu_backend::proto_internal::spread_table_6503;
213 let req = spread_table_6503::DaemonGetSpreadTableReq {
214 c2s: spread_table_6503::daemon_get_spread_table_req::C2s { reserved: None },
215 };
216 let body = req.encode_to_vec();
217 let frame = client
218 .request(futu_core::proto_id::QOT_GET_SPREAD_TABLE, body)
219 .await?;
220 let resp = spread_table_6503::DaemonGetSpreadTableRsp::decode(frame.body.as_ref())
221 .map_err(|e| anyhow!("decode spread_table: {e}"))?;
222 if resp.ret_type != 0 {
223 bail!(
224 "spread_table ret_type={} msg={:?}",
225 resp.ret_type,
226 resp.ret_msg
227 );
228 }
229 let s = resp.s2c.ok_or_else(|| anyhow!("missing s2c"))?;
230 Ok(serde_json::to_string_pretty(&s)?)
231}
232
233pub async fn get_ticker_statistic(
235 client: &Arc<FutuClient>,
236 symbol: &str,
237 ticker_type: Option<i32>,
238 stat_type: Option<u32>,
239) -> Result<String> {
240 use futu_backend::proto_internal::ticker_statistic_daemon;
241 let sec = parse_symbol(symbol)?;
242 let req = ticker_statistic_daemon::DaemonGetTickerStatisticReq {
243 c2s: ticker_statistic_daemon::daemon_get_ticker_statistic_req::C2s {
244 security: ticker_statistic_daemon::Security {
245 market: sec.market as i32,
246 code: sec.code,
247 },
248 ticker_type,
249 ticker_time: None,
250 stat_type,
251 },
254 };
255 let body = req.encode_to_vec();
256 let frame = client
257 .request(futu_core::proto_id::QOT_GET_TICKER_STATISTIC, body)
258 .await?;
259 let resp = ticker_statistic_daemon::DaemonGetTickerStatisticRsp::decode(frame.body.as_ref())
260 .map_err(|e| anyhow!("decode ticker_statistic: {e}"))?;
261 if resp.ret_type != 0 {
262 bail!(
263 "ticker_statistic ret_type={} msg={:?}",
264 resp.ret_type,
265 resp.ret_msg
266 );
267 }
268 let s = resp.s2c.ok_or_else(|| anyhow!("missing s2c"))?;
269 Ok(serde_json::to_string_pretty(&s)?)
270}
271
272pub struct TickerStatisticDetailInput<'a> {
279 pub symbol: &'a str,
280 pub ticker_type: Option<i32>,
281 pub ticker_time: Option<u64>,
282 pub select_num: Option<u32>,
283 pub data_from: Option<u32>,
284 pub data_max_count: Option<u32>,
285 pub stat_type: Option<u32>,
286}
287
288pub async fn get_ticker_statistic_detail(
289 client: &Arc<FutuClient>,
290 input: TickerStatisticDetailInput<'_>,
291) -> Result<String> {
292 use futu_backend::proto_internal::ticker_statistic_daemon;
293 let sec = parse_symbol(input.symbol)?;
294 let req = ticker_statistic_daemon::DaemonGetTickerStatisticDetailReq {
295 c2s: ticker_statistic_daemon::daemon_get_ticker_statistic_detail_req::C2s {
296 security: ticker_statistic_daemon::Security {
297 market: sec.market as i32,
298 code: sec.code,
299 },
300 ticker_type: input.ticker_type,
301 ticker_time: input.ticker_time,
302 select_num: input.select_num,
303 data_from: input.data_from,
304 data_max_count: input.data_max_count,
305 stat_type: input.stat_type,
306 },
307 };
308 let body = req.encode_to_vec();
309 let frame = client
310 .request(futu_core::proto_id::QOT_GET_TICKER_STATISTIC_DETAIL, body)
311 .await?;
312 let resp =
313 ticker_statistic_daemon::DaemonGetTickerStatisticDetailRsp::decode(frame.body.as_ref())
314 .map_err(|e| anyhow!("decode ticker_statistic_detail: {e}"))?;
315 if resp.ret_type != 0 {
316 bail!(
317 "ticker_statistic_detail ret_type={} msg={:?}",
318 resp.ret_type,
319 resp.ret_msg
320 );
321 }
322 let s = resp.s2c.ok_or_else(|| anyhow!("missing s2c"))?;
323 Ok(serde_json::to_string_pretty(&s)?)
324}