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