1use anyhow::Result;
4use serde::Serialize;
5use tabled::Tabled;
6
7use crate::common::{connect_gateway, parse_symbol};
8use crate::output::OutputFormat;
9
10#[derive(Tabled)]
11struct SnapshotRow {
12 #[tabled(rename = "Symbol")]
13 symbol: String,
14 #[tabled(rename = "Name")]
15 name: String,
16 #[tabled(rename = "Price")]
17 price: String,
18 #[tabled(rename = "Change%")]
19 change_pct: String,
20 #[tabled(rename = "Open")]
21 open: String,
22 #[tabled(rename = "High")]
23 high: String,
24 #[tabled(rename = "Low")]
25 low: String,
26 #[tabled(rename = "Volume")]
27 volume: String,
28 #[tabled(rename = "Turnover")]
29 turnover: String,
30 #[tabled(rename = "Exchange")]
32 exchange_code: String,
33}
34
35#[derive(Serialize)]
36struct SnapshotJson {
37 symbol: String,
38 market: i32,
39 code: String,
40 name: String,
41 sec_type: i32,
42 lot_size: i32,
43 is_suspend: bool,
44 list_time: String,
45 update_time: String,
46 cur_price: f64,
47 last_close_price: f64,
48 change_pct: f64,
49 open_price: f64,
50 high_price: f64,
51 low_price: f64,
52 volume: i64,
53 turnover: f64,
54 turnover_rate: f64,
55 ask_price: Option<f64>,
56 bid_price: Option<f64>,
57 ask_vol: Option<i64>,
58 bid_vol: Option<i64>,
59 amplitude: Option<f64>,
60 avg_price: Option<f64>,
61 volume_ratio: Option<f64>,
62 highest52_weeks_price: Option<f64>,
63 lowest52_weeks_price: Option<f64>,
64 #[serde(skip_serializing_if = "Option::is_none")]
70 exchange_code: Option<String>,
71}
72
73pub async fn run(gateway: &str, symbols: &[String], format: OutputFormat) -> Result<()> {
74 let secs: Vec<_> = symbols
75 .iter()
76 .map(|s| parse_symbol(s))
77 .collect::<Result<_>>()?;
78
79 let (client, _push_rx) = connect_gateway(gateway, "futucli-snapshot").await?;
80 let s2c = futu_qot::snapshot::get_security_snapshot(&client, &secs).await?;
81
82 let exch_code_by_key: std::collections::HashMap<(i32, String), &'static str> =
88 match futu_qot::static_info::get_static_info(&client, &secs).await {
89 Ok(infos) => infos
90 .iter()
91 .filter_map(|i| {
92 i.exchange_code()
93 .map(|s| ((i.security.market as i32, i.security.code.clone()), s))
94 })
95 .collect(),
96 Err(e) => {
97 tracing::warn!(
98 "get_static_info for exchange_code lookup failed: {e}; exchange_code 字段省略"
99 );
100 std::collections::HashMap::new()
101 }
102 };
103
104 let mut rows = Vec::new();
105 let mut jsons = Vec::new();
106
107 for snap in &s2c.snapshot_list {
108 let basic = &snap.basic;
109 let cur = basic.cur_price;
110 let last = basic.last_close_price;
111 let change_pct = if last != 0.0 {
112 (cur - last) / last * 100.0
113 } else {
114 0.0
115 };
116 let market_prefix = market_prefix_from_i32(basic.security.market);
117 let symbol = format!("{}.{}", market_prefix, basic.security.code);
118 let sign = if cur >= last { "+" } else { "" };
119 let name = basic.name.clone().unwrap_or_default();
120 let exchange_code: Option<&'static str> = exch_code_by_key
121 .get(&(basic.security.market, basic.security.code.clone()))
122 .copied();
123
124 rows.push(SnapshotRow {
125 symbol: symbol.clone(),
126 name: name.clone(),
127 price: format!("{cur:.3}"),
128 change_pct: format!("{sign}{change_pct:.2}%"),
129 open: format!("{:.3}", basic.open_price),
130 high: format!("{:.3}", basic.high_price),
131 low: format!("{:.3}", basic.low_price),
132 volume: basic.volume.to_string(),
133 turnover: format!("{:.0}", basic.turnover),
134 exchange_code: exchange_code.unwrap_or("-").to_string(),
136 });
137
138 jsons.push(SnapshotJson {
139 symbol,
140 market: basic.security.market,
141 code: basic.security.code.clone(),
142 name,
143 sec_type: basic.r#type,
144 lot_size: basic.lot_size,
145 is_suspend: basic.is_suspend,
146 list_time: basic.list_time.clone(),
147 update_time: basic.update_time.clone(),
148 cur_price: cur,
149 last_close_price: last,
150 change_pct,
151 open_price: basic.open_price,
152 high_price: basic.high_price,
153 low_price: basic.low_price,
154 volume: basic.volume,
155 turnover: basic.turnover,
156 turnover_rate: basic.turnover_rate,
157 ask_price: basic.ask_price,
158 bid_price: basic.bid_price,
159 ask_vol: basic.ask_vol,
160 bid_vol: basic.bid_vol,
161 amplitude: basic.amplitude,
162 avg_price: basic.avg_price,
163 volume_ratio: basic.volume_ratio,
164 highest52_weeks_price: basic.highest52_weeks_price,
165 lowest52_weeks_price: basic.lowest52_weeks_price,
166 exchange_code: exchange_code.map(String::from),
168 });
169 }
170
171 format.print_rows(&rows, &jsons)?;
172 Ok(())
173}
174
175fn market_prefix_from_i32(m: i32) -> &'static str {
176 match m {
177 1 => "HK",
178 2 => "HK_FUTURE",
179 11 => "US",
180 21 => "SH",
181 22 => "SZ",
182 _ => "UNKNOWN",
183 }
184}