Skip to main content

futucli/cmd/analysis/short_info/
market_activity.rs

1use 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}