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