1use std::time::Duration;
4
5use anyhow::Result;
6use serde::Serialize;
7use tabled::Tabled;
8
9use crate::common::{connect_gateway, format_symbol, parse_symbol};
10use crate::output::OutputFormat;
11use crate::qot_sdk_adapter;
12use futu_qot::types::BasicQot;
13
14#[derive(Tabled)]
15struct QuoteRow {
16 #[tabled(rename = "Symbol")]
17 symbol: String,
18 #[tabled(rename = "Price")]
19 price: String,
20 #[tabled(rename = "Change")]
21 change: String,
22 #[tabled(rename = "Change%")]
23 change_pct: String,
24 #[tabled(rename = "Volume")]
25 volume: String,
26 #[tabled(rename = "Update")]
27 update: String,
28}
29
30#[derive(Serialize)]
31struct QuoteJson {
32 symbol: String,
33 cur_price: f64,
34 last_close_price: f64,
35 change: f64,
36 change_pct: f64,
37 open_price: f64,
38 high_price: f64,
39 low_price: f64,
40 volume: i64,
41 turnover: f64,
42 update_time: String,
43 is_suspended: bool,
44}
45
46impl QuoteJson {
47 fn from(q: &BasicQot) -> Self {
48 let change = q.cur_price - q.last_close_price;
49 let change_pct = if q.last_close_price != 0.0 {
50 change / q.last_close_price * 100.0
51 } else {
52 0.0
53 };
54 Self {
55 symbol: format_symbol(&q.security),
56 cur_price: q.cur_price,
57 last_close_price: q.last_close_price,
58 change,
59 change_pct,
60 open_price: q.open_price,
61 high_price: q.high_price,
62 low_price: q.low_price,
63 volume: q.volume,
64 turnover: q.turnover,
65 update_time: q.update_time.clone(),
66 is_suspended: q.is_suspended,
67 }
68 }
69}
70
71fn to_row(j: &QuoteJson) -> QuoteRow {
72 let sign = if j.change >= 0.0 { "+" } else { "" };
73 QuoteRow {
74 symbol: j.symbol.clone(),
75 price: format!("{:.3}", j.cur_price),
76 change: format!("{sign}{:.3}", j.change),
77 change_pct: format!("{sign}{:.2}%", j.change_pct),
78 volume: format_volume(j.volume),
79 update: j.update_time.clone(),
80 }
81}
82
83fn format_volume(v: i64) -> String {
84 let s = v.abs().to_string();
86 let bytes = s.as_bytes();
87 let mut out = String::new();
88 for (i, b) in bytes.iter().enumerate() {
89 if i > 0 && (bytes.len() - i).is_multiple_of(3) {
90 out.push(',');
91 }
92 out.push(*b as char);
93 }
94 if v < 0 { format!("-{out}") } else { out }
95}
96
97pub async fn run(gateway: &str, symbols: &[String], format: OutputFormat) -> Result<()> {
98 let secs: Vec<_> = symbols
99 .iter()
100 .map(|s| parse_symbol(s))
101 .collect::<Result<_>>()?;
102
103 let (client, _push_rx) = connect_gateway(gateway, "futucli-quote").await?;
104
105 futu_qot::sub::subscribe(
107 &client,
108 &secs,
109 &[qot_sdk_adapter::basic_sub_type()],
110 true,
111 true,
112 )
113 .await?;
114 tokio::time::sleep(Duration::from_millis(200)).await;
116
117 let quotes = futu_qot::basic_qot::get_basic_qot(&client, &secs).await?;
118
119 let jsons: Vec<QuoteJson> = quotes.iter().map(QuoteJson::from).collect();
120 let rows: Vec<QuoteRow> = jsons.iter().map(to_row).collect();
121
122 format.print_rows(&rows, &jsons)?;
123 Ok(())
124}