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
9#[derive(Tabled)]
10struct SubInfoRow {
11 #[tabled(rename = "SubType")]
12 sub_type: i32,
13 #[tabled(rename = "Symbols")]
14 symbols: String,
15}
16
17#[derive(Serialize)]
18struct SubInfoJson {
19 total_used_quota: i32,
20 remain_quota: i32,
21 option_used_quota: Option<i32>,
22 option_remain_quota: Option<i32>,
23 entries: Vec<SubInfoJsonEntry>,
24}
25
26#[derive(Serialize)]
27struct SubInfoJsonEntry {
28 sub_type: i32,
29 symbols: Vec<String>,
30}
31
32#[derive(Tabled)]
33struct UsedQuotaRow {
34 #[tabled(rename = "Field")]
35 field: String,
36 #[tabled(rename = "Value")]
37 value: i32,
38}
39
40#[derive(Serialize)]
41struct UsedQuotaJson {
42 used_sub_quota: i32,
43 used_k_line_quota: i32,
44}
45
46pub async fn run_query_subscription(
47 gateway: &str,
48 all_conn: bool,
49 format: OutputFormat,
50) -> Result<()> {
51 let (client, _rx) = connect_gateway(gateway, "futucli-query-sub").await?;
52 let req = futu_proto::qot_get_sub_info::Request {
53 c2s: futu_proto::qot_get_sub_info::C2s {
54 is_req_all_conn: Some(all_conn),
55 header: None,
56 },
57 };
58 let body = req.encode_to_vec();
59 let frame = client
60 .request(futu_core::proto_id::QOT_GET_SUB_INFO, body)
61 .await?;
62 let resp = futu_proto::qot_get_sub_info::Response::decode(frame.body.as_ref())
63 .map_err(|e| anyhow!("decode sub_info: {e}"))?;
64 if resp.ret_type != 0 {
65 bail!("sub_info ret_type={} msg={:?}", resp.ret_type, resp.ret_msg);
66 }
67 let s = resp.s2c.ok_or_else(|| anyhow!("missing s2c"))?;
68 let mut rows = Vec::new();
69 let mut entries = Vec::new();
70 for conn in &s.conn_sub_info_list {
71 for si in &conn.sub_info_list {
72 let syms: Vec<String> = si
73 .security_list
74 .iter()
75 .map(|sec| format!("{}.{}", sec.market, sec.code))
76 .collect();
77 rows.push(SubInfoRow {
78 sub_type: si.sub_type,
79 symbols: if syms.is_empty() {
80 "-".into()
81 } else {
82 syms.join(", ")
83 },
84 });
85 entries.push(SubInfoJsonEntry {
86 sub_type: si.sub_type,
87 symbols: syms,
88 });
89 }
90 }
91 let json = SubInfoJson {
92 total_used_quota: s.total_used_quota,
93 remain_quota: s.remain_quota,
94 option_used_quota: s.option_used_quota,
95 option_remain_quota: s.option_remain_quota,
96 entries,
97 };
98 if matches!(format, OutputFormat::Table) {
101 println!(
102 "quota: stock_used={} stock_remain={} option_used={} option_remain={}",
103 s.total_used_quota,
104 s.remain_quota,
105 s.option_used_quota.unwrap_or(0),
106 s.option_remain_quota.unwrap_or(0)
107 );
108 }
109 format.print_rows(&rows, &[json])?;
110 Ok(())
111}
112
113pub async fn run_used_quota(gateway: &str, format: OutputFormat) -> Result<()> {
114 let (client, _rx) = connect_gateway(gateway, "futucli-used-quota").await?;
115 let req = futu_proto::used_quota::Request {
116 c2s: futu_proto::used_quota::C2s {},
117 };
118 let body = req.encode_to_vec();
119 let frame = client
120 .request(futu_core::proto_id::GET_USED_QUOTA, body)
121 .await?;
122 let resp = futu_proto::used_quota::Response::decode(frame.body.as_ref())
123 .map_err(|e| anyhow!("decode used_quota: {e}"))?;
124 if resp.ret_type != 0 {
125 bail!(
126 "used_quota ret_type={} msg={:?}",
127 resp.ret_type,
128 resp.ret_msg
129 );
130 }
131 let s = resp.s2c.ok_or_else(|| anyhow!("missing s2c"))?;
132 let used_sub_quota = s
133 .used_sub_quota
134 .ok_or_else(|| anyhow!("missing used_sub_quota"))?;
135 let used_k_line_quota = s
136 .used_k_line_quota
137 .ok_or_else(|| anyhow!("missing used_k_line_quota"))?;
138 let json = UsedQuotaJson {
139 used_sub_quota,
140 used_k_line_quota,
141 };
142 let rows = vec![
143 UsedQuotaRow {
144 field: "used_sub_quota".into(),
145 value: json.used_sub_quota,
146 },
147 UsedQuotaRow {
148 field: "used_k_line_quota".into(),
149 value: json.used_k_line_quota,
150 },
151 ];
152 format.print_rows(&rows, &[json])?;
153 Ok(())
154}
155
156pub async fn run_unsubscribe(
157 gateway: &str,
158 symbols: &[String],
159 sub_types: &[i32],
160 unsub_all: bool,
161 _format: OutputFormat,
162) -> Result<()> {
163 let (client, _rx) = connect_gateway(gateway, "futucli-unsubscribe").await?;
164 let proto_secs: Vec<_> = if unsub_all {
165 vec![]
166 } else {
167 symbols
168 .iter()
169 .map(|s| {
170 let sec = parse_symbol(s)?;
171 Ok::<_, anyhow::Error>(futu_proto::qot_common::Security {
172 market: sec.market as i32,
173 code: sec.code,
174 })
175 })
176 .collect::<Result<Vec<_>>>()?
177 };
178 let req = futu_proto::qot_sub::Request {
179 c2s: futu_proto::qot_sub::C2s {
180 security_list: proto_secs,
181 sub_type_list: sub_types.to_vec(),
182 is_sub_or_un_sub: false,
183 is_reg_or_un_reg_push: Some(false),
184 reg_push_rehab_type_list: vec![],
185 is_first_push: None,
186 is_unsub_all: Some(unsub_all),
187 is_sub_order_book_detail: None,
188 extended_time: None,
189 session: None,
190 header: None,
191 },
192 };
193 let body = req.encode_to_vec();
194 let frame = client.request(futu_core::proto_id::QOT_SUB, body).await?;
195 let resp = futu_proto::qot_sub::Response::decode(frame.body.as_ref())
196 .map_err(|e| anyhow!("decode unsubscribe: {e}"))?;
197 if resp.ret_type != 0 {
198 bail!(
199 "unsubscribe ret_type={} msg={:?}",
200 resp.ret_type,
201 resp.ret_msg
202 );
203 }
204 println!(
205 "✅ unsubscribe ok: unsub_all={} count={}",
206 unsub_all,
207 symbols.len()
208 );
209 Ok(())
210}