1use anyhow::{Result, anyhow, bail};
6use prost::Message;
7use serde::Serialize;
8use tabled::Tabled;
9
10use crate::common::{connect_gateway, parse_symbol};
11use crate::output::OutputFormat;
12
13#[derive(Tabled)]
14struct HoldingRow {
15 #[tabled(rename = "Holder")]
16 holder: String,
17 #[tabled(rename = "Qty")]
18 qty: String,
19 #[tabled(rename = "Ratio%")]
20 ratio: String,
21 #[tabled(rename = "Change")]
22 change: String,
23 #[tabled(rename = "Time")]
24 time: String,
25}
26
27#[derive(Serialize)]
28struct HoldingJson {
29 holder_name: String,
30 holding_qty: f64,
31 holding_ratio: f64,
32 change_qty: f64,
33 change_ratio: f64,
34 time: String,
35}
36
37pub async fn run_holding_change(
38 gateway: &str,
39 symbol: &str,
40 category: i32,
41 begin: Option<&str>,
42 end: Option<&str>,
43 format: OutputFormat,
44) -> Result<()> {
45 let sec = parse_symbol(symbol)?;
46 let (client, _rx) = connect_gateway(gateway, "futucli-holding-change").await?;
47 let req = futu_proto::qot_get_holding_change_list::Request {
48 c2s: futu_proto::qot_get_holding_change_list::C2s {
49 security: futu_proto::qot_common::Security {
50 market: sec.market as i32,
51 code: sec.code,
52 },
53 holder_category: category,
54 begin_time: begin.map(String::from),
55 end_time: end.map(String::from),
56 header: None, },
58 };
59 let body = req.encode_to_vec();
60 let frame = client
61 .request(futu_core::proto_id::QOT_GET_HOLDING_CHANGE_LIST, body)
62 .await?;
63 let resp = futu_proto::qot_get_holding_change_list::Response::decode(frame.body.as_ref())
64 .map_err(|e| anyhow!("decode: {e}"))?;
65 if resp.ret_type != 0 {
66 bail!(
67 "holding_change ret_type={} msg={:?}",
68 resp.ret_type,
69 resp.ret_msg
70 );
71 }
72 let s = resp.s2c.ok_or_else(|| anyhow!("missing s2c"))?;
73 let mut rows = Vec::new();
74 let mut jsons = Vec::new();
75 for h in &s.holding_change_list {
76 rows.push(HoldingRow {
77 holder: h.holder_name.clone(),
78 qty: format!("{:.0}", h.holding_qty),
79 ratio: format!("{:.3}", h.holding_ratio),
80 change: format!("{:+.0}", h.change_qty),
81 time: h.time.clone(),
82 });
83 jsons.push(HoldingJson {
84 holder_name: h.holder_name.clone(),
85 holding_qty: h.holding_qty,
86 holding_ratio: h.holding_ratio,
87 change_qty: h.change_qty,
88 change_ratio: h.change_ratio,
89 time: h.time.clone(),
90 });
91 }
92 format.print_rows(&rows, &jsons)?;
93 Ok(())
94}
95
96pub async fn run_modify_user_security(
97 gateway: &str,
98 group_name: &str,
99 op: i32,
100 symbols: &[String],
101 _format: OutputFormat,
102) -> Result<()> {
103 let secs: Vec<_> = symbols
104 .iter()
105 .map(|s| parse_symbol(s))
106 .collect::<Result<Vec<_>>>()?;
107 let (client, _rx) = connect_gateway(gateway, "futucli-modify-user-sec").await?;
108 let req = futu_proto::qot_modify_user_security::Request {
109 c2s: futu_proto::qot_modify_user_security::C2s {
110 group_name: group_name.to_string(),
111 op,
112 security_list: secs
113 .iter()
114 .map(|s| futu_proto::qot_common::Security {
115 market: s.market as i32,
116 code: s.code.clone(),
117 })
118 .collect(),
119 header: None,
120 },
121 };
122 let body = req.encode_to_vec();
123 let frame = client
124 .request(futu_core::proto_id::QOT_MODIFY_USER_SECURITY, body)
125 .await?;
126 let resp = futu_proto::qot_modify_user_security::Response::decode(frame.body.as_ref())
127 .map_err(|e| anyhow!("decode: {e}"))?;
128 if resp.ret_type != 0 {
129 bail!(
130 "modify_user_security ret_type={} msg={:?}",
131 resp.ret_type,
132 resp.ret_msg
133 );
134 }
135 println!(
136 "✅ modify_user_security ok: group={group_name} op={op} count={}",
137 symbols.len()
138 );
139 Ok(())
140}
141
142#[derive(Tabled)]
143struct CodeChangeRow {
144 #[tabled(rename = "Type")]
145 change_type: i32,
146 #[tabled(rename = "Main")]
147 main_code: String,
148 #[tabled(rename = "Related")]
149 related_code: String,
150 #[tabled(rename = "Public")]
151 public_time: String,
152 #[tabled(rename = "Effective")]
153 effective_time: String,
154}
155
156#[derive(Serialize)]
157struct CodeChangeJson {
158 change_type: i32,
159 main_code: String,
160 related_code: String,
161 public_time: Option<String>,
162 effective_time: Option<String>,
163}
164
165pub async fn run_code_change(
166 gateway: &str,
167 symbols: &[String],
168 format: OutputFormat,
169) -> Result<()> {
170 let secs: Vec<_> = symbols
171 .iter()
172 .map(|s| parse_symbol(s))
173 .collect::<Result<Vec<_>>>()?;
174 let (client, _rx) = connect_gateway(gateway, "futucli-code-change").await?;
175 let req = futu_proto::qot_get_code_change::Request {
176 c2s: futu_proto::qot_get_code_change::C2s {
177 place_holder: None,
178 security_list: secs
179 .iter()
180 .map(|s| futu_proto::qot_common::Security {
181 market: s.market as i32,
182 code: s.code.clone(),
183 })
184 .collect(),
185 time_filter_list: vec![],
186 type_list: vec![],
187 header: None,
188 },
189 };
190 let body = req.encode_to_vec();
191 let frame = client
192 .request(futu_core::proto_id::QOT_GET_CODE_CHANGE, body)
193 .await?;
194 let resp = futu_proto::qot_get_code_change::Response::decode(frame.body.as_ref())
195 .map_err(|e| anyhow!("decode: {e}"))?;
196 if resp.ret_type != 0 {
197 bail!(
198 "code_change ret_type={} msg={:?}",
199 resp.ret_type,
200 resp.ret_msg
201 );
202 }
203 let s = resp.s2c.ok_or_else(|| anyhow!("missing s2c"))?;
204 let mut rows = Vec::new();
205 let mut jsons = Vec::new();
206 for c in &s.code_change_list {
207 rows.push(CodeChangeRow {
208 change_type: c.r#type,
209 main_code: c.security.code.clone(),
210 related_code: c.related_security.code.clone(),
211 public_time: c.public_time.clone().unwrap_or_default(),
212 effective_time: c.effective_time.clone().unwrap_or_default(),
213 });
214 jsons.push(CodeChangeJson {
215 change_type: c.r#type,
216 main_code: c.security.code.clone(),
217 related_code: c.related_security.code.clone(),
218 public_time: c.public_time.clone(),
219 effective_time: c.effective_time.clone(),
220 });
221 }
222 format.print_rows(&rows, &jsons)?;
223 Ok(())
224}