Skip to main content

futucli/cmd/sys/
subscription.rs

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    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    // v1.4.98 eli BUG-005 fix (P2, 2026-04-27): table 模式打印 quota 文本头;
95    // JSON/JSONL 仅返合法 JSON (脚本解析).
96    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}