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