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