1use anyhow::{Result, anyhow, bail};
11use prost::Message;
12use serde::Serialize;
13use tabled::Tabled;
14
15use crate::common::connect_gateway;
16use crate::output::OutputFormat;
17
18mod quote_rights;
19mod subscription;
20#[cfg(test)]
21mod tests;
22mod user_info;
23
24pub use quote_rights::run_quote_rights;
25#[cfg(test)]
26use quote_rights::{quote_right_quota_rows, quote_right_rows, quote_right_user_rows};
27pub use subscription::{run_query_subscription, run_unsubscribe, run_used_quota};
28pub use user_info::run_user_info;
29#[cfg(test)]
30use user_info::user_attribution_region_label;
31
32#[derive(Tabled)]
37struct GlobalStateRow {
38 #[tabled(rename = "Field")]
39 field: String,
40 #[tabled(rename = "Value")]
41 value: String,
42}
43
44#[derive(Serialize)]
45struct GlobalStateJson {
46 market_hk: i32,
47 market_us: i32,
48 market_sh: i32,
49 market_sz: i32,
50 market_hk_future: i32,
51 market_us_future: Option<i32>,
52 market_sg_future: Option<i32>,
53 market_jp_future: Option<i32>,
54 market_sg: Option<i32>,
55 market_my: Option<i32>,
56 market_jp: Option<i32>,
57 qot_logined: bool,
58 trd_logined: bool,
59 server_ver: i32,
60 server_build_no: i32,
61 server_time: i64,
62 conn_id: Option<u64>,
63}
64
65use futu_qot::types::market_state_label;
68use futu_surface_spec::endpoints::get_delay_statistics::{
69 DEFAULT_QOT_PUSH_STAGE, default_segment_list_vec, default_type_list_vec,
70};
71
72pub async fn run_global_state(gateway: &str, format: OutputFormat) -> Result<()> {
73 let (client, _rx) = connect_gateway(gateway, "futucli-global-state").await?;
74 let req = futu_proto::get_global_state::Request {
75 c2s: futu_proto::get_global_state::C2s { user_id: 0 },
76 };
77 let body = req.encode_to_vec();
78 let frame = client
79 .request(futu_core::proto_id::GET_GLOBAL_STATE, body)
80 .await?;
81 let resp = futu_proto::get_global_state::Response::decode(frame.body.as_ref())
82 .map_err(|e| anyhow!("decode global_state: {e}"))?;
83 if resp.ret_type != 0 {
84 bail!(
85 "global_state ret_type={} msg={:?}",
86 resp.ret_type,
87 resp.ret_msg
88 );
89 }
90 let s = resp.s2c.ok_or_else(|| anyhow!("missing s2c"))?;
91 let rows = vec![
92 GlobalStateRow {
93 field: "market_hk".into(),
94 value: format!("{} ({})", market_state_label(s.market_hk), s.market_hk),
95 },
96 GlobalStateRow {
97 field: "market_us".into(),
98 value: format!("{} ({})", market_state_label(s.market_us), s.market_us),
99 },
100 GlobalStateRow {
101 field: "market_sh".into(),
102 value: format!("{} ({})", market_state_label(s.market_sh), s.market_sh),
103 },
104 GlobalStateRow {
105 field: "market_sz".into(),
106 value: format!("{} ({})", market_state_label(s.market_sz), s.market_sz),
107 },
108 GlobalStateRow {
109 field: "market_hk_future".into(),
110 value: format!(
111 "{} ({})",
112 market_state_label(s.market_hk_future),
113 s.market_hk_future
114 ),
115 },
116 GlobalStateRow {
117 field: "market_sg".into(),
118 value: s
119 .market_sg
120 .map(|v| format!("{} ({})", market_state_label(v), v))
121 .unwrap_or_else(|| "-".into()),
122 },
123 GlobalStateRow {
124 field: "market_my".into(),
125 value: s
126 .market_my
127 .map(|v| format!("{} ({})", market_state_label(v), v))
128 .unwrap_or_else(|| "-".into()),
129 },
130 GlobalStateRow {
131 field: "market_jp".into(),
132 value: s
133 .market_jp
134 .map(|v| format!("{} ({})", market_state_label(v), v))
135 .unwrap_or_else(|| "-".into()),
136 },
137 GlobalStateRow {
138 field: "qot_logined".into(),
139 value: s.qot_logined.to_string(),
140 },
141 GlobalStateRow {
142 field: "trd_logined".into(),
143 value: s.trd_logined.to_string(),
144 },
145 GlobalStateRow {
146 field: "server_ver".into(),
147 value: s.server_ver.to_string(),
148 },
149 GlobalStateRow {
150 field: "server_build_no".into(),
151 value: s.server_build_no.to_string(),
152 },
153 GlobalStateRow {
154 field: "server_time".into(),
155 value: s.time.to_string(),
156 },
157 GlobalStateRow {
158 field: "conn_id".into(),
159 value: s
160 .conn_id
161 .map(|c| c.to_string())
162 .unwrap_or_else(|| "-".into()),
163 },
164 ];
165 let json = GlobalStateJson {
166 market_hk: s.market_hk,
167 market_us: s.market_us,
168 market_sh: s.market_sh,
169 market_sz: s.market_sz,
170 market_hk_future: s.market_hk_future,
171 market_us_future: s.market_us_future,
172 market_sg_future: s.market_sg_future,
173 market_jp_future: s.market_jp_future,
174 market_sg: s.market_sg,
175 market_my: s.market_my,
176 market_jp: s.market_jp,
177 qot_logined: s.qot_logined,
178 trd_logined: s.trd_logined,
179 server_ver: s.server_ver,
180 server_build_no: s.server_build_no,
181 server_time: s.time,
182 conn_id: s.conn_id,
183 };
184 format.print_rows(&rows, &[json])?;
185 Ok(())
186}
187
188#[derive(Tabled)]
193struct DelayStatRow {
194 #[tabled(rename = "Category")]
195 category: String,
196 #[tabled(rename = "Samples")]
197 samples: usize,
198}
199
200#[derive(Serialize)]
201struct DelayStatJson {
202 qot_push_categories: usize,
203 req_reply_samples: usize,
204 place_order_samples: usize,
205}
206
207pub async fn run_delay_statistics(gateway: &str, format: OutputFormat) -> Result<()> {
208 let (client, _rx) = connect_gateway(gateway, "futucli-delay-statistics").await?;
209 let req = futu_proto::get_delay_statistics::Request {
210 c2s: futu_proto::get_delay_statistics::C2s {
211 type_list: default_type_list_vec(),
212 qot_push_stage: Some(DEFAULT_QOT_PUSH_STAGE),
213 segment_list: default_segment_list_vec(),
214 },
215 };
216 let body = req.encode_to_vec();
217 let frame = client
218 .request(futu_core::proto_id::GET_DELAY_STATISTICS, body)
219 .await?;
220 let resp = futu_proto::get_delay_statistics::Response::decode(frame.body.as_ref())
221 .map_err(|e| anyhow!("decode delay_statistics: {e}"))?;
222 if resp.ret_type != 0 {
223 bail!(
224 "delay_statistics 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 let rows = vec![
231 DelayStatRow {
232 category: "qot_push".into(),
233 samples: s.qot_push_statistics_list.len(),
234 },
235 DelayStatRow {
236 category: "req_reply".into(),
237 samples: s.req_reply_statistics_list.len(),
238 },
239 DelayStatRow {
240 category: "place_order".into(),
241 samples: s.place_order_statistics_list.len(),
242 },
243 ];
244 let json = DelayStatJson {
245 qot_push_categories: s.qot_push_statistics_list.len(),
246 req_reply_samples: s.req_reply_statistics_list.len(),
247 place_order_samples: s.place_order_statistics_list.len(),
248 };
249 format.print_rows(&rows, &[json])?;
250 Ok(())
251}
252
253#[derive(serde::Serialize, tabled::Tabled)]
259struct TokenStateRow {
260 brand: String,
262 bind: u32,
264 enable: u32,
266}
267
268#[derive(serde::Serialize)]
269struct TokenStateJson {
270 nn_token_enable: u32,
271 nn_token_bind: u32,
272 mm_token_enable: u32,
273 mm_token_bind: u32,
274}
275
276pub async fn run_token_state(gateway: &str, format: OutputFormat) -> Result<()> {
277 use futu_backend::proto_internal::futu_token_state;
278
279 let (client, _rx) = connect_gateway(gateway, "futucli-token-state").await?;
280 let req = futu_token_state::DaemonGetTokenStateReq {
281 c2s: futu_token_state::daemon_get_token_state_req::C2s { app_id: None },
282 };
283 let body = req.encode_to_vec();
284 let frame = client
285 .request(futu_core::proto_id::GET_TOKEN_STATE, body)
286 .await?;
287 let resp = futu_token_state::DaemonGetTokenStateRsp::decode(frame.body.as_ref())
288 .map_err(|e| anyhow!("decode token_state: {e}"))?;
289 if resp.ret_type != 0 {
290 bail!(
291 "token_state ret_type={} msg={:?}",
292 resp.ret_type,
293 resp.ret_msg
294 );
295 }
296 let s = resp.s2c.ok_or_else(|| anyhow!("missing s2c"))?;
297 let rows = vec![
298 TokenStateRow {
299 brand: "NN (Futu Token)".into(),
300 bind: s.nn_token_bind.unwrap_or(0),
301 enable: s.nn_token_enable.unwrap_or(0),
302 },
303 TokenStateRow {
304 brand: "MM (moomoo Token)".into(),
305 bind: s.mm_token_bind.unwrap_or(0),
306 enable: s.mm_token_enable.unwrap_or(0),
307 },
308 ];
309 let json = TokenStateJson {
310 nn_token_enable: s.nn_token_enable.unwrap_or(0),
311 nn_token_bind: s.nn_token_bind.unwrap_or(0),
312 mm_token_enable: s.mm_token_enable.unwrap_or(0),
313 mm_token_bind: s.mm_token_bind.unwrap_or(0),
314 };
315 format.print_rows(&rows, &[json])?;
316 Ok(())
317}
318
319#[derive(serde::Serialize, tabled::Tabled)]
325struct RiskFreeRateRow {
326 market: String,
327 rate_pct: String,
329 raw: u64,
331}
332
333#[derive(serde::Serialize)]
334struct RiskFreeRateJson {
335 hk_rate_pct: Option<f64>,
336 us_rate_pct: Option<f64>,
337 jp_rate_pct: Option<f64>,
338 update_time: Option<i64>,
339 hk_rate_raw: Option<u64>,
340 us_rate_raw: Option<u64>,
341 jp_rate_raw: Option<u64>,
342}
343
344pub async fn run_risk_free_rate(gateway: &str, format: OutputFormat) -> Result<()> {
345 use futu_backend::proto_internal::risk_free_rate;
346
347 let (client, _rx) = connect_gateway(gateway, "futucli-risk-free-rate").await?;
348 let req = risk_free_rate::DaemonGetRiskFreeRateReq {
349 c2s: risk_free_rate::daemon_get_risk_free_rate_req::C2s { rate_time: None },
350 };
351 let body = req.encode_to_vec();
352 let frame = client
353 .request(futu_core::proto_id::QOT_GET_RISK_FREE_RATE, body)
354 .await?;
355 let resp = risk_free_rate::DaemonGetRiskFreeRateRsp::decode(frame.body.as_ref())
356 .map_err(|e| anyhow!("decode risk_free_rate: {e}"))?;
357 if resp.ret_type != 0 {
358 bail!(
359 "risk_free_rate ret_type={} msg={:?}",
360 resp.ret_type,
361 resp.ret_msg
362 );
363 }
364 let s = resp.s2c.ok_or_else(|| anyhow!("missing s2c"))?;
365 let fmt_pct = |v: Option<f64>| -> String {
368 match v {
369 Some(p) => format!("{:.4}%", p),
370 None => "—".into(),
371 }
372 };
373 let rows = vec![
374 RiskFreeRateRow {
375 market: "HK".into(),
376 rate_pct: fmt_pct(s.hk_rate_pct),
377 raw: s.hk_rate_raw.unwrap_or(0),
378 },
379 RiskFreeRateRow {
380 market: "US".into(),
381 rate_pct: fmt_pct(s.us_rate_pct),
382 raw: s.us_rate_raw.unwrap_or(0),
383 },
384 RiskFreeRateRow {
385 market: "JP".into(),
386 rate_pct: fmt_pct(s.jp_rate_pct),
387 raw: s.jp_rate_raw.unwrap_or(0),
388 },
389 ];
390 let json = RiskFreeRateJson {
391 hk_rate_pct: s.hk_rate_pct,
392 us_rate_pct: s.us_rate_pct,
393 jp_rate_pct: s.jp_rate_pct,
394 update_time: s.update_time,
395 hk_rate_raw: s.hk_rate_raw,
396 us_rate_raw: s.us_rate_raw,
397 jp_rate_raw: s.jp_rate_raw,
398 };
399 format.print_rows(&rows, &[json])?;
400 Ok(())
401}
402
403pub async fn run_spread_table(gateway: &str, format: OutputFormat) -> Result<()> {
408 use futu_backend::proto_internal::spread_table_6503;
409
410 let (client, _rx) = connect_gateway(gateway, "futucli-spread-table").await?;
411 let req = spread_table_6503::DaemonGetSpreadTableReq {
412 c2s: spread_table_6503::daemon_get_spread_table_req::C2s { reserved: None },
413 };
414 let body = req.encode_to_vec();
415 let frame = client
416 .request(futu_core::proto_id::QOT_GET_SPREAD_TABLE, body)
417 .await?;
418 let resp = spread_table_6503::DaemonGetSpreadTableRsp::decode(frame.body.as_ref())
419 .map_err(|e| anyhow!("decode spread_table: {e}"))?;
420 if resp.ret_type != 0 {
421 bail!(
422 "spread_table ret_type={} msg={:?}",
423 resp.ret_type,
424 resp.ret_msg
425 );
426 }
427 let s = resp.s2c.ok_or_else(|| anyhow!("missing s2c"))?;
429 match format {
430 OutputFormat::Json => println!("{}", serde_json::to_string_pretty(&s)?),
434 OutputFormat::Jsonl => println!("{}", serde_json::to_string(&s)?),
435 OutputFormat::Table => {
436 println!(
438 "Spread tables: {} (use --output json for full data)",
439 s.spread_table_list.len()
440 );
441 for t in &s.spread_table_list {
442 println!(
443 " spread_code={:?} items={}",
444 t.spread_code,
445 t.spread_item_list.len()
446 );
447 }
448 }
449 }
450 Ok(())
451}
452
453pub async fn run_ticker_statistic(
458 gateway: &str,
459 symbol: &str,
460 ticker_type: Option<i32>,
461 stat_type: Option<u32>,
462 format: OutputFormat,
463) -> Result<()> {
464 use futu_backend::proto_internal::ticker_statistic_daemon;
465
466 let (client, _rx) = connect_gateway(gateway, "futucli-ticker-statistic").await?;
467 let sec = crate::common::parse_symbol(symbol)?;
471 let req = ticker_statistic_daemon::DaemonGetTickerStatisticReq {
472 c2s: ticker_statistic_daemon::daemon_get_ticker_statistic_req::C2s {
473 security: ticker_statistic_daemon::Security {
474 market: sec.market as i32,
475 code: sec.code,
476 },
477 ticker_type,
478 ticker_time: None,
479 stat_type,
480 },
481 };
482 let body = req.encode_to_vec();
483 let frame = client
484 .request(futu_core::proto_id::QOT_GET_TICKER_STATISTIC, body)
485 .await?;
486 let resp = ticker_statistic_daemon::DaemonGetTickerStatisticRsp::decode(frame.body.as_ref())
487 .map_err(|e| anyhow!("decode ticker_statistic: {e}"))?;
488 if resp.ret_type != 0 {
489 bail!(
490 "ticker_statistic ret_type={} msg={:?}",
491 resp.ret_type,
492 resp.ret_msg
493 );
494 }
495 let s = resp.s2c.ok_or_else(|| anyhow!("missing s2c"))?;
496 match format {
497 OutputFormat::Json => println!("{}", serde_json::to_string_pretty(&s)?),
500 OutputFormat::Jsonl => println!("{}", serde_json::to_string(&s)?),
501 OutputFormat::Table => {
502 println!("symbol: {}", symbol);
503 if let Some(stid) = s.resolved_stock_id {
504 println!("resolved_stock_id: {}", stid);
505 }
506 if let Some(tt) = s.ticker_time {
507 println!("ticker_time: {}", tt);
508 }
509 if !s.date_list.is_empty() {
510 println!("date_list (returned when ticker_time=0): {:?}", s.date_list);
511 }
512 if let Some(stat) = &s.stat {
513 println!("avg_price: {:?}", stat.avg_price);
514 println!("trade_volume: {:?}", stat.trade_volume);
515 println!("trade_num: {:?}", stat.trade_num);
516 println!("buy_volume: {:?}", stat.buy_volume);
517 println!("sell_volume: {:?}", stat.sell_volume);
518 println!("neutral_volume: {:?}", stat.neutral_volume);
519 println!("last_close_price: {:?}", stat.last_close_price);
520 }
521 }
522 }
523 Ok(())
524}
525
526pub struct TickerStatisticDetailCommand<'a> {
531 pub gateway: &'a str,
532 pub symbol: &'a str,
533 pub ticker_type: Option<i32>,
534 pub ticker_time: Option<u64>,
535 pub select_num: Option<u32>,
536 pub data_from: Option<u32>,
537 pub data_max_count: Option<u32>,
538 pub stat_type: Option<u32>,
539 pub format: OutputFormat,
540}
541
542pub async fn run_ticker_statistic_detail(input: TickerStatisticDetailCommand<'_>) -> Result<()> {
543 use futu_backend::proto_internal::ticker_statistic_daemon;
544
545 let (client, _rx) = connect_gateway(input.gateway, "futucli-ticker-statistic-detail").await?;
546 let sec = crate::common::parse_symbol(input.symbol)?;
547 let req = ticker_statistic_daemon::DaemonGetTickerStatisticDetailReq {
548 c2s: ticker_statistic_daemon::daemon_get_ticker_statistic_detail_req::C2s {
549 security: ticker_statistic_daemon::Security {
550 market: sec.market as i32,
551 code: sec.code,
552 },
553 ticker_type: input.ticker_type,
554 ticker_time: input.ticker_time,
555 select_num: input.select_num,
556 data_from: input.data_from,
557 data_max_count: input.data_max_count,
558 stat_type: input.stat_type,
559 },
560 };
561 let body = req.encode_to_vec();
562 let frame = client
563 .request(futu_core::proto_id::QOT_GET_TICKER_STATISTIC_DETAIL, body)
564 .await?;
565 let resp =
566 ticker_statistic_daemon::DaemonGetTickerStatisticDetailRsp::decode(frame.body.as_ref())
567 .map_err(|e| anyhow!("decode ticker_statistic_detail: {e}"))?;
568 if resp.ret_type != 0 {
569 bail!(
570 "ticker_statistic_detail ret_type={} msg={:?}",
571 resp.ret_type,
572 resp.ret_msg
573 );
574 }
575 let s = resp.s2c.ok_or_else(|| anyhow!("missing s2c"))?;
576 match input.format {
577 OutputFormat::Json => println!("{}", serde_json::to_string_pretty(&s)?),
578 OutputFormat::Jsonl => println!("{}", serde_json::to_string(&s)?),
579 OutputFormat::Table => {
580 println!("symbol: {}", input.symbol);
581 if let Some(stid) = s.resolved_stock_id {
582 println!("resolved_stock_id: {}", stid);
583 }
584 if let Some(tt) = s.ticker_time {
585 println!("ticker_time: {}", tt);
586 }
587 if let Some(have_more) = s.have_more {
588 println!("have_more: {}", have_more);
589 }
590 if let Some(mr) = s.max_ratio {
591 println!("max_ratio: {:.5}", mr);
592 }
593 println!("items ({}):", s.items.len());
594 for (i, item) in s.items.iter().enumerate() {
595 println!(
596 " [{:2}] price={:?} buy={:?} sell={:?} vol={:?} ratio={:?} neutral={:?}",
597 i,
598 item.price,
599 item.buy_volume,
600 item.sell_volume,
601 item.volume,
602 item.ratio,
603 item.neutral_volume
604 );
605 }
606 }
607 }
608 Ok(())
609}