Skip to main content

futucli/cmd/analysis/short_info/
shareholders.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::{display_bool, display_f64, display_i64, display_opt, display_u64};
10
11#[derive(Tabled)]
12struct ShareholdersOverviewRow {
13    #[tabled(rename = "Group")]
14    group: String,
15    #[tabled(rename = "Static Date")]
16    static_date: String,
17    #[tabled(rename = "Name")]
18    name: String,
19    #[tabled(rename = "Holder %")]
20    holder_pct: String,
21    #[tabled(rename = "Holder ID")]
22    holder_id: String,
23}
24
25#[derive(Serialize)]
26struct ShareholdersOverviewJson {
27    symbol: String,
28    s2c: futu_proto::qot_get_shareholders_overview::S2c,
29}
30
31pub async fn run_shareholders_overview(
32    gateway: &str,
33    symbol: &str,
34    period_id: Option<i32>,
35    format: OutputFormat,
36) -> Result<()> {
37    let sec = parse_symbol(symbol)?;
38    let (client, _rx) = connect_gateway(gateway, "futucli-shareholders-overview").await?;
39    let req = futu_proto::qot_get_shareholders_overview::Request {
40        c2s: futu_proto::qot_get_shareholders_overview::C2s {
41            security: futu_proto::qot_common::Security {
42                market: sec.market as i32,
43                code: sec.code.clone(),
44            },
45            period_id,
46        },
47    };
48    let frame = client
49        .request(
50            futu_core::proto_id::QOT_GET_SHAREHOLDERS_OVERVIEW,
51            req.encode_to_vec(),
52        )
53        .await?;
54    let resp = futu_proto::qot_get_shareholders_overview::Response::decode(frame.body.as_ref())
55        .map_err(|e| anyhow!("decode shareholders_overview: {e}"))?;
56    if resp.ret_type != 0 {
57        bail!(
58            "shareholders_overview ret_type={} msg={:?}",
59            resp.ret_type,
60            resp.ret_msg
61        );
62    }
63    let s2c = resp.s2c.ok_or_else(|| anyhow!("missing s2c"))?;
64    let mut rows = Vec::new();
65    for info in &s2c.main_holder_info_list {
66        for item in &info.item_list {
67            rows.push(ShareholdersOverviewRow {
68                group: "main_holder".to_string(),
69                static_date: display_opt(&info.static_date_str),
70                name: display_opt(&item.name),
71                holder_pct: display_f64(item.holder_pct),
72                holder_id: item.holder_id.map(|v| v.to_string()).unwrap_or_default(),
73            });
74        }
75    }
76    for info in &s2c.holder_type_info_list {
77        for item in &info.item_list {
78            rows.push(ShareholdersOverviewRow {
79                group: "holder_type".to_string(),
80                static_date: display_opt(&info.static_date_str),
81                name: display_opt(&item.name),
82                holder_pct: display_f64(item.holder_pct),
83                holder_id: item.holder_id.map(|v| v.to_string()).unwrap_or_default(),
84            });
85        }
86    }
87    let json = ShareholdersOverviewJson {
88        symbol: symbol.to_string(),
89        s2c,
90    };
91    format.print_rows(&rows, &[json])?;
92    Ok(())
93}
94
95#[derive(Tabled)]
96struct ShareholdersHoldingChangesRow {
97    #[tabled(rename = "Period")]
98    period: String,
99    #[tabled(rename = "Name")]
100    name: String,
101    #[tabled(rename = "Holder ID")]
102    holder_id: String,
103    #[tabled(rename = "Change")]
104    change_num: String,
105    #[tabled(rename = "Holder %")]
106    holder_pct: String,
107    #[tabled(rename = "Change %")]
108    holder_pct_change: String,
109    #[tabled(rename = "Holding Date")]
110    holding_date: String,
111    #[tabled(rename = "Type")]
112    holder_type: String,
113    #[tabled(rename = "Next Key")]
114    next_key: String,
115}
116
117#[derive(Serialize)]
118struct ShareholdersHoldingChangesJson {
119    symbol: String,
120    s2c: futu_proto::qot_get_shareholders_holding_changes::S2c,
121}
122
123pub struct ShareholdersHoldingChangesCommand<'a> {
124    pub gateway: &'a str,
125    pub symbol: &'a str,
126    pub next_key: Option<String>,
127    pub num: Option<i32>,
128    pub sort_type: Option<i32>,
129    pub sort_column: Option<i32>,
130    pub filter_type: Option<i32>,
131    pub format: OutputFormat,
132}
133
134pub async fn run_shareholders_holding_changes(
135    input: ShareholdersHoldingChangesCommand<'_>,
136) -> Result<()> {
137    let sec = parse_symbol(input.symbol)?;
138    let (client, _rx) =
139        connect_gateway(input.gateway, "futucli-shareholders-holding-changes").await?;
140    let req = futu_proto::qot_get_shareholders_holding_changes::Request {
141        c2s: futu_proto::qot_get_shareholders_holding_changes::C2s {
142            security: futu_proto::qot_common::Security {
143                market: sec.market as i32,
144                code: sec.code.clone(),
145            },
146            next_key: input.next_key,
147            num: input.num,
148            sort_type: input.sort_type,
149            sort_column: input.sort_column,
150            filter_type: input.filter_type,
151        },
152    };
153    let frame = client
154        .request(
155            futu_core::proto_id::QOT_GET_SHAREHOLDERS_HOLDING_CHANGES,
156            req.encode_to_vec(),
157        )
158        .await?;
159    let resp =
160        futu_proto::qot_get_shareholders_holding_changes::Response::decode(frame.body.as_ref())
161            .map_err(|e| anyhow!("decode shareholders_holding_changes: {e}"))?;
162    if resp.ret_type != 0 {
163        bail!(
164            "shareholders_holding_changes ret_type={} msg={:?}",
165            resp.ret_type,
166            resp.ret_msg
167        );
168    }
169    let s2c = resp.s2c.ok_or_else(|| anyhow!("missing s2c"))?;
170    let rows: Vec<_> = s2c
171        .item_list
172        .iter()
173        .map(|item| ShareholdersHoldingChangesRow {
174            period: display_opt(&item.period_text),
175            name: display_opt(&item.name),
176            holder_id: item.holder_id.map(|v| v.to_string()).unwrap_or_default(),
177            change_num: display_i64(item.share_change_num),
178            holder_pct: display_f64(item.holder_pct),
179            holder_pct_change: display_f64(item.holder_pct_change),
180            holding_date: display_opt(&item.holding_date_str),
181            holder_type: display_opt(&item.holder_type),
182            next_key: display_opt(&s2c.next_key),
183        })
184        .collect();
185    let json = ShareholdersHoldingChangesJson {
186        symbol: input.symbol.to_string(),
187        s2c,
188    };
189    input.format.print_rows(&rows, &[json])?;
190    Ok(())
191}
192
193#[derive(Tabled)]
194struct ShareholdersHolderDetailRow {
195    #[tabled(rename = "Period")]
196    period: String,
197    #[tabled(rename = "Holder")]
198    name: String,
199    #[tabled(rename = "Holder ID")]
200    holder_id: String,
201    #[tabled(rename = "Holding Qty")]
202    holder_quantity: String,
203    #[tabled(rename = "Holding Change")]
204    holder_quantity_change: String,
205    #[tabled(rename = "Holder %")]
206    holder_pct: String,
207    #[tabled(rename = "Change %")]
208    holder_pct_change: String,
209    #[tabled(rename = "Holding Date")]
210    holding_date: String,
211    #[tabled(rename = "Close")]
212    close_price: String,
213    #[tabled(rename = "Price Change %")]
214    price_change_pct: String,
215    #[tabled(rename = "Source")]
216    source: String,
217    #[tabled(rename = "Next Key")]
218    next_key: String,
219}
220
221#[derive(Serialize)]
222struct ShareholdersHolderDetailJson {
223    symbol: String,
224    s2c: futu_proto::qot_get_shareholders_holder_detail::S2c,
225}
226
227pub struct ShareholdersHolderDetailCommand<'a> {
228    pub gateway: &'a str,
229    pub symbol: &'a str,
230    pub request_type: Option<i32>,
231    pub next_key: Option<String>,
232    pub num: Option<i32>,
233    pub sort_column: Option<i32>,
234    pub sort_type: Option<i32>,
235    pub period_id: Option<i32>,
236    pub holder_id: Option<i32>,
237    pub format: OutputFormat,
238}
239
240pub async fn run_shareholders_holder_detail(
241    input: ShareholdersHolderDetailCommand<'_>,
242) -> Result<()> {
243    let sec = parse_symbol(input.symbol)?;
244    let (client, _rx) =
245        connect_gateway(input.gateway, "futucli-shareholders-holder-detail").await?;
246    let req = futu_proto::qot_get_shareholders_holder_detail::Request {
247        c2s: futu_proto::qot_get_shareholders_holder_detail::C2s {
248            security: futu_proto::qot_common::Security {
249                market: sec.market as i32,
250                code: sec.code.clone(),
251            },
252            request_type: input.request_type,
253            next_key: input.next_key,
254            num: input.num,
255            sort_column: input.sort_column,
256            sort_type: input.sort_type,
257            period_id: input.period_id,
258            holder_id: input.holder_id,
259        },
260    };
261    let frame = client
262        .request(
263            futu_core::proto_id::QOT_GET_SHAREHOLDERS_HOLDER_DETAIL,
264            req.encode_to_vec(),
265        )
266        .await?;
267    let resp =
268        futu_proto::qot_get_shareholders_holder_detail::Response::decode(frame.body.as_ref())
269            .map_err(|e| anyhow!("decode shareholders_holder_detail: {e}"))?;
270    if resp.ret_type != 0 {
271        bail!(
272            "shareholders_holder_detail ret_type={} msg={:?}",
273            resp.ret_type,
274            resp.ret_msg
275        );
276    }
277    let s2c = resp.s2c.ok_or_else(|| anyhow!("missing s2c"))?;
278    let rows: Vec<_> = s2c
279        .item_list
280        .iter()
281        .map(|item| ShareholdersHolderDetailRow {
282            period: display_opt(&item.period_text),
283            name: display_opt(&item.name),
284            holder_id: display_u64(item.holder_id),
285            holder_quantity: display_i64(item.holder_quantity),
286            holder_quantity_change: display_i64(item.holder_quantity_change),
287            holder_pct: display_f64(item.holder_pct),
288            holder_pct_change: display_f64(item.holder_pct_change),
289            holding_date: display_opt(&item.holding_date_str),
290            close_price: display_f64(item.close_price),
291            price_change_pct: display_f64(item.price_change_pct),
292            source: display_opt(&item.source_group_name),
293            next_key: display_opt(&s2c.next_key),
294        })
295        .collect();
296    let json = ShareholdersHolderDetailJson {
297        symbol: input.symbol.to_string(),
298        s2c,
299    };
300    input.format.print_rows(&rows, &[json])?;
301    Ok(())
302}
303
304#[derive(Tabled)]
305struct ShareholdersInstitutionalRow {
306    #[tabled(rename = "Period")]
307    period: String,
308    #[tabled(rename = "Institutions")]
309    institutions: String,
310    #[tabled(rename = "Inst Change")]
311    institution_change: String,
312    #[tabled(rename = "Holding Qty")]
313    holder_quantity: String,
314    #[tabled(rename = "Holding Change")]
315    holder_quantity_change: String,
316    #[tabled(rename = "Holder %")]
317    holder_pct: String,
318    #[tabled(rename = "Change %")]
319    holder_pct_change: String,
320    #[tabled(rename = "Next Key")]
321    next_key: String,
322}
323
324#[derive(Serialize)]
325struct ShareholdersInstitutionalJson {
326    symbol: String,
327    s2c: futu_proto::qot_get_shareholders_institutional::S2c,
328}
329
330pub async fn run_shareholders_institutional(
331    gateway: &str,
332    symbol: &str,
333    next_key: Option<String>,
334    num: Option<i32>,
335    format: OutputFormat,
336) -> Result<()> {
337    let sec = parse_symbol(symbol)?;
338    let (client, _rx) = connect_gateway(gateway, "futucli-shareholders-institutional").await?;
339    let req = futu_proto::qot_get_shareholders_institutional::Request {
340        c2s: futu_proto::qot_get_shareholders_institutional::C2s {
341            security: futu_proto::qot_common::Security {
342                market: sec.market as i32,
343                code: sec.code.clone(),
344            },
345            next_key,
346            num,
347        },
348    };
349    let frame = client
350        .request(
351            futu_core::proto_id::QOT_GET_SHAREHOLDERS_INSTITUTIONAL,
352            req.encode_to_vec(),
353        )
354        .await?;
355    let resp =
356        futu_proto::qot_get_shareholders_institutional::Response::decode(frame.body.as_ref())
357            .map_err(|e| anyhow!("decode shareholders_institutional: {e}"))?;
358    if resp.ret_type != 0 {
359        bail!(
360            "shareholders_institutional ret_type={} msg={:?}",
361            resp.ret_type,
362            resp.ret_msg
363        );
364    }
365    let s2c = resp.s2c.ok_or_else(|| anyhow!("missing s2c"))?;
366    let rows: Vec<_> = s2c
367        .item_list
368        .iter()
369        .map(|item| ShareholdersInstitutionalRow {
370            period: display_opt(&item.period_text),
371            institutions: display_i64(item.institution_quantity),
372            institution_change: display_i64(item.institution_quantity_change),
373            holder_quantity: display_i64(item.holder_quantity),
374            holder_quantity_change: display_i64(item.holder_quantity_change),
375            holder_pct: display_f64(item.holder_pct),
376            holder_pct_change: display_f64(item.holder_pct_change),
377            next_key: display_opt(&s2c.next_key),
378        })
379        .collect();
380    let json = ShareholdersInstitutionalJson {
381        symbol: symbol.to_string(),
382        s2c,
383    };
384    format.print_rows(&rows, &[json])?;
385    Ok(())
386}
387
388#[derive(Tabled)]
389struct InsiderHolderListRow {
390    #[tabled(rename = "Holder")]
391    name: String,
392    #[tabled(rename = "Title")]
393    title: String,
394    #[tabled(rename = "Holder ID")]
395    holder_id: String,
396    #[tabled(rename = "Holding Qty")]
397    holder_quantity: String,
398    #[tabled(rename = "Holder %")]
399    holder_pct: String,
400    #[tabled(rename = "All Count")]
401    all_count: String,
402    #[tabled(rename = "Insider Total")]
403    insider_total: String,
404    #[tabled(rename = "Bought")]
405    bought: String,
406    #[tabled(rename = "Sold")]
407    sold: String,
408    #[tabled(rename = "Next Key")]
409    next_key: String,
410}
411
412#[derive(Serialize)]
413struct InsiderHolderListJson {
414    symbol: String,
415    s2c: futu_proto::qot_get_insider_holder_list::S2c,
416}
417
418pub async fn run_insider_holder_list(
419    gateway: &str,
420    symbol: &str,
421    next_key: Option<String>,
422    num: Option<i32>,
423    format: OutputFormat,
424) -> Result<()> {
425    let sec = parse_symbol(symbol)?;
426    let (client, _rx) = connect_gateway(gateway, "futucli-insider-holder-list").await?;
427    let req = futu_proto::qot_get_insider_holder_list::Request {
428        c2s: futu_proto::qot_get_insider_holder_list::C2s {
429            security: futu_proto::qot_common::Security {
430                market: sec.market as i32,
431                code: sec.code.clone(),
432            },
433            next_key,
434            num,
435        },
436    };
437    let frame = client
438        .request(
439            futu_core::proto_id::QOT_GET_INSIDER_HOLDER_LIST,
440            req.encode_to_vec(),
441        )
442        .await?;
443    let resp = futu_proto::qot_get_insider_holder_list::Response::decode(frame.body.as_ref())
444        .map_err(|e| anyhow!("decode insider_holder_list: {e}"))?;
445    if resp.ret_type != 0 {
446        bail!(
447            "insider_holder_list ret_type={} msg={:?}",
448            resp.ret_type,
449            resp.ret_msg
450        );
451    }
452    let s2c = resp.s2c.ok_or_else(|| anyhow!("missing s2c"))?;
453    let rows: Vec<_> = s2c
454        .item_list
455        .iter()
456        .map(|item| InsiderHolderListRow {
457            name: display_opt(&item.name),
458            title: display_opt(&item.title),
459            holder_id: display_i64(item.holder_id),
460            holder_quantity: display_i64(item.holder_quantity),
461            holder_pct: display_f64(item.holder_pct),
462            all_count: s2c.all_count.map(|v| v.to_string()).unwrap_or_default(),
463            insider_total: s2c
464                .insider_total_count
465                .map(|v| v.to_string())
466                .unwrap_or_default(),
467            bought: s2c
468                .insider_bought_count
469                .map(|v| v.to_string())
470                .unwrap_or_default(),
471            sold: s2c
472                .insider_sold_count
473                .map(|v| v.to_string())
474                .unwrap_or_default(),
475            next_key: display_opt(&s2c.next_key),
476        })
477        .collect();
478    let json = InsiderHolderListJson {
479        symbol: symbol.to_string(),
480        s2c,
481    };
482    format.print_rows(&rows, &[json])?;
483    Ok(())
484}
485
486#[derive(Tabled)]
487struct InsiderTradeListRow {
488    #[tabled(rename = "Holder")]
489    name: String,
490    #[tabled(rename = "Title")]
491    title: String,
492    #[tabled(rename = "Holder ID")]
493    holder_id: String,
494    #[tabled(rename = "Shares")]
495    trade_shares: String,
496    #[tabled(rename = "Min Date")]
497    min_trade_date: String,
498    #[tabled(rename = "Max Date")]
499    max_trade_date: String,
500    #[tabled(rename = "Min Price")]
501    min_price: String,
502    #[tabled(rename = "Max Price")]
503    max_price: String,
504    #[tabled(rename = "Security Qty")]
505    security_holder_quantity: String,
506    #[tabled(rename = "Security")]
507    security_description: String,
508    #[tabled(rename = "Type")]
509    transaction_type: String,
510    #[tabled(rename = "Source")]
511    source_group_name: String,
512    #[tabled(rename = "Proposed Sale")]
513    proposed_sale: String,
514    #[tabled(rename = "All Count")]
515    all_count: String,
516    #[tabled(rename = "Next Key")]
517    next_key: String,
518}
519
520#[derive(Serialize)]
521struct InsiderTradeListJson {
522    symbol: String,
523    s2c: futu_proto::qot_get_insider_trade_list::S2c,
524}
525
526pub async fn run_insider_trade_list(
527    gateway: &str,
528    symbol: &str,
529    holder_id: Option<i64>,
530    next_key: Option<String>,
531    num: Option<i32>,
532    format: OutputFormat,
533) -> Result<()> {
534    let sec = parse_symbol(symbol)?;
535    let (client, _rx) = connect_gateway(gateway, "futucli-insider-trade-list").await?;
536    let req = futu_proto::qot_get_insider_trade_list::Request {
537        c2s: futu_proto::qot_get_insider_trade_list::C2s {
538            security: futu_proto::qot_common::Security {
539                market: sec.market as i32,
540                code: sec.code.clone(),
541            },
542            holder_id,
543            next_key,
544            num,
545        },
546    };
547    let frame = client
548        .request(
549            futu_core::proto_id::QOT_GET_INSIDER_TRADE_LIST,
550            req.encode_to_vec(),
551        )
552        .await?;
553    let resp = futu_proto::qot_get_insider_trade_list::Response::decode(frame.body.as_ref())
554        .map_err(|e| anyhow!("decode insider_trade_list: {e}"))?;
555    if resp.ret_type != 0 {
556        bail!(
557            "insider_trade_list ret_type={} msg={:?}",
558            resp.ret_type,
559            resp.ret_msg
560        );
561    }
562    let s2c = resp.s2c.ok_or_else(|| anyhow!("missing s2c"))?;
563    let rows: Vec<_> = s2c
564        .item_list
565        .iter()
566        .map(|item| InsiderTradeListRow {
567            name: display_opt(&item.name),
568            title: display_opt(&item.title),
569            holder_id: display_i64(item.holder_id),
570            trade_shares: display_i64(item.trade_shares),
571            min_trade_date: display_opt(&item.min_trade_date_str),
572            max_trade_date: display_opt(&item.max_trade_date_str),
573            min_price: display_f64(item.min_price),
574            max_price: display_f64(item.max_price),
575            security_holder_quantity: display_i64(item.security_holder_quantity),
576            security_description: display_opt(&item.security_description),
577            transaction_type: display_opt(&item.transaction_type),
578            source_group_name: display_opt(&item.source_group_name),
579            proposed_sale: display_bool(item.is_proposed_sale_of_securities),
580            all_count: s2c.all_count.map(|v| v.to_string()).unwrap_or_default(),
581            next_key: display_opt(&s2c.next_key),
582        })
583        .collect();
584    let json = InsiderTradeListJson {
585        symbol: symbol.to_string(),
586        s2c,
587    };
588    format.print_rows(&rows, &[json])?;
589    Ok(())
590}