futucli/cmd/analysis/short_info/
market_activity.rs1use anyhow::{Result, anyhow, bail};
2use prost::Message;
3use serde::Serialize;
4use tabled::Tabled;
5
6use crate::common::{connect_gateway, parse_symbol};
7use crate::output::OutputFormat;
8
9use super::{
10 display_bool, display_buy_sell_type, display_f64, display_i64, display_opt, display_u64,
11};
12
13#[derive(Tabled)]
14struct DailyShortVolumeRow {
15 #[tabled(rename = "Market")]
16 market: String,
17 #[tabled(rename = "Date")]
18 date: String,
19 #[tabled(rename = "Short Shares")]
20 short_shares: String,
21 #[tabled(rename = "Short %")]
22 short_percent: String,
23 #[tabled(rename = "Volume")]
24 volume: String,
25 #[tabled(rename = "Close")]
26 close: String,
27 #[tabled(rename = "Next Key")]
28 next_key: String,
29}
30
31#[derive(Serialize)]
32struct DailyShortVolumeJson {
33 symbol: String,
34 s2c: futu_proto::qot_get_daily_short_volume::S2c,
35}
36
37pub async fn run_daily_short_volume(
38 gateway: &str,
39 symbol: &str,
40 next_key: Option<&str>,
41 num: Option<i32>,
42 format: OutputFormat,
43) -> Result<()> {
44 let sec = parse_symbol(symbol)?;
45 let (client, _rx) = connect_gateway(gateway, "futucli-daily-short-volume").await?;
46 let req = futu_proto::qot_get_daily_short_volume::Request {
47 c2s: futu_proto::qot_get_daily_short_volume::C2s {
48 security: futu_proto::qot_common::Security {
49 market: sec.market as i32,
50 code: sec.code.clone(),
51 },
52 next_key: next_key.map(str::to_string),
53 num,
54 },
55 };
56 let frame = client
57 .request(
58 futu_core::proto_id::QOT_GET_DAILY_SHORT_VOLUME,
59 req.encode_to_vec(),
60 )
61 .await?;
62 let resp = futu_proto::qot_get_daily_short_volume::Response::decode(frame.body.as_ref())
63 .map_err(|e| anyhow!("decode daily_short_volume: {e}"))?;
64 if resp.ret_type != 0 {
65 bail!(
66 "daily_short_volume ret_type={} msg={:?}",
67 resp.ret_type,
68 resp.ret_msg
69 );
70 }
71 let s2c = resp.s2c.ok_or_else(|| anyhow!("missing s2c"))?;
72 let mut rows = Vec::new();
73 rows.extend(s2c.us_item_list.iter().map(|item| DailyShortVolumeRow {
74 market: "US".to_string(),
75 date: display_opt(&item.timestamp_str),
76 short_shares: display_u64(item.total_shares_short),
77 short_percent: display_f64(item.short_percent),
78 volume: display_u64(item.volume),
79 close: display_f64(item.close_price),
80 next_key: display_opt(&s2c.next_key),
81 }));
82 rows.extend(s2c.hk_item_list.iter().map(|item| DailyShortVolumeRow {
83 market: "HK".to_string(),
84 date: display_opt(&item.timestamp_str),
85 short_shares: display_u64(item.short_sell_shares_traded),
86 short_percent: display_f64(s2c.aggregated_short_ratio),
87 volume: display_u64(item.shares_traded),
88 close: display_f64(item.close_price),
89 next_key: display_opt(&s2c.next_key),
90 }));
91 let json = DailyShortVolumeJson {
92 symbol: symbol.to_string(),
93 s2c,
94 };
95 format.print_rows(&rows, &[json])?;
96 Ok(())
97}
98
99#[derive(Tabled)]
100struct ShortInterestRow {
101 #[tabled(rename = "Market")]
102 market: String,
103 #[tabled(rename = "Date")]
104 date: String,
105 #[tabled(rename = "Shares Short")]
106 shares_short: String,
107 #[tabled(rename = "Short %")]
108 short_percent: String,
109 #[tabled(rename = "Days To Cover")]
110 days_to_cover: String,
111 #[tabled(rename = "Close")]
112 close: String,
113 #[tabled(rename = "Next Key")]
114 next_key: String,
115}
116
117#[derive(Serialize)]
118struct ShortInterestJson {
119 symbol: String,
120 s2c: futu_proto::qot_get_short_interest::S2c,
121}
122
123pub async fn run_short_interest(
124 gateway: &str,
125 symbol: &str,
126 next_key: Option<&str>,
127 num: Option<i32>,
128 format: OutputFormat,
129) -> Result<()> {
130 let sec = parse_symbol(symbol)?;
131 let (client, _rx) = connect_gateway(gateway, "futucli-short-interest").await?;
132 let req = futu_proto::qot_get_short_interest::Request {
133 c2s: futu_proto::qot_get_short_interest::C2s {
134 security: futu_proto::qot_common::Security {
135 market: sec.market as i32,
136 code: sec.code.clone(),
137 },
138 next_key: next_key.map(str::to_string),
139 num,
140 },
141 };
142 let frame = client
143 .request(
144 futu_core::proto_id::QOT_GET_SHORT_INTEREST,
145 req.encode_to_vec(),
146 )
147 .await?;
148 let resp = futu_proto::qot_get_short_interest::Response::decode(frame.body.as_ref())
149 .map_err(|e| anyhow!("decode short_interest: {e}"))?;
150 if resp.ret_type != 0 {
151 bail!(
152 "short_interest ret_type={} msg={:?}",
153 resp.ret_type,
154 resp.ret_msg
155 );
156 }
157 let s2c = resp.s2c.ok_or_else(|| anyhow!("missing s2c"))?;
158 let mut rows = Vec::new();
159 rows.extend(s2c.us_item_list.iter().map(|item| ShortInterestRow {
160 market: "US".to_string(),
161 date: display_opt(&item.timestamp_str),
162 shares_short: display_u64(item.shares_short),
163 short_percent: display_f64(item.short_percent),
164 days_to_cover: display_f64(item.days_to_cover),
165 close: display_f64(item.close_price),
166 next_key: display_opt(&s2c.next_key),
167 }));
168 rows.extend(s2c.hk_item_list.iter().map(|item| ShortInterestRow {
169 market: "HK".to_string(),
170 date: display_opt(&item.timestamp_str),
171 shares_short: display_u64(item.aggregated_short),
172 short_percent: display_f64(item.aggregated_short_ratio),
173 days_to_cover: String::new(),
174 close: display_f64(item.close_price),
175 next_key: display_opt(&s2c.next_key),
176 }));
177 let json = ShortInterestJson {
178 symbol: symbol.to_string(),
179 s2c,
180 };
181 format.print_rows(&rows, &[json])?;
182 Ok(())
183}
184
185#[derive(Tabled)]
186struct TopTenBuySellBrokersRow {
187 #[tabled(rename = "Side")]
188 side: String,
189 #[tabled(rename = "Broker")]
190 broker: String,
191 #[tabled(rename = "Net Vol")]
192 net_vol: String,
193 #[tabled(rename = "Avg Price")]
194 avg_price: String,
195 #[tabled(rename = "Total Vol")]
196 total_vol: String,
197 #[tabled(rename = "Turnover")]
198 turnover: String,
199 #[tabled(rename = "Realtime")]
200 realtime: String,
201 #[tabled(rename = "Data Time")]
202 data_time: String,
203}
204
205#[derive(Serialize)]
206struct TopTenBuySellBrokersJson {
207 symbol: String,
208 s2c: futu_proto::qot_get_top_ten_buy_sell_brokers::S2c,
209}
210
211pub async fn run_top_ten_buy_sell_brokers(
212 gateway: &str,
213 symbol: &str,
214 days_before: Option<i32>,
215 format: OutputFormat,
216) -> Result<()> {
217 let sec = parse_symbol(symbol)?;
218 let (client, _rx) = connect_gateway(gateway, "futucli-top-ten-buy-sell-brokers").await?;
219 let req = futu_proto::qot_get_top_ten_buy_sell_brokers::Request {
220 c2s: futu_proto::qot_get_top_ten_buy_sell_brokers::C2s {
221 security: futu_proto::qot_common::Security {
222 market: sec.market as i32,
223 code: sec.code.clone(),
224 },
225 days_before,
226 },
227 };
228 let frame = client
229 .request(
230 futu_core::proto_id::QOT_GET_TOP_TEN_BUY_SELL_BROKERS,
231 req.encode_to_vec(),
232 )
233 .await?;
234 let resp = futu_proto::qot_get_top_ten_buy_sell_brokers::Response::decode(frame.body.as_ref())
235 .map_err(|e| anyhow!("decode top_ten_buy_sell_brokers: {e}"))?;
236 if resp.ret_type != 0 {
237 bail!(
238 "top_ten_buy_sell_brokers ret_type={} msg={:?}",
239 resp.ret_type,
240 resp.ret_msg
241 );
242 }
243 let s2c = resp.s2c.ok_or_else(|| anyhow!("missing s2c"))?;
244 let realtime = display_bool(s2c.is_real_time);
245 let data_time = display_opt(&s2c.data_time_str);
246 let rows: Vec<_> = s2c
247 .broker_list
248 .iter()
249 .map(|item| TopTenBuySellBrokersRow {
250 side: display_buy_sell_type(item.buy_sell_type),
251 broker: display_opt(&item.broker_name),
252 net_vol: display_i64(item.net_vol),
253 avg_price: display_f64(item.avg_price),
254 total_vol: display_f64(item.total_vol),
255 turnover: display_f64(item.total_turnover),
256 realtime: realtime.clone(),
257 data_time: data_time.clone(),
258 })
259 .collect();
260 let json = TopTenBuySellBrokersJson {
261 symbol: symbol.to_string(),
262 s2c,
263 };
264 format.print_rows(&rows, &[json])?;
265 Ok(())
266}