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}