Skip to main content

futucli/cmd/analysis/
modify.rs

1//! v1.4.110+ split (from cmd/analysis.rs): modify domain.
2//!
3//! pub items: run_holding_change, run_modify_user_security, run_code_change.
4
5use 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, // v1.4.110 codex Slice 1 schema 占位
57        },
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}