Skip to main content

futucli/cmd/analysis/
info.rs

1//! v1.4.110+ split (from cmd/analysis.rs): info domain.
2//!
3//! pub items: run_future_info, run_stock_filter, run_option_chain, run_history_kl_quota.
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
13use super::option_args::{OptionChainGreekFilterArgs, OptionChainJson, OptionChainRow};
14use super::trading::parse_qot_market;
15
16#[derive(Tabled)]
17struct FutureRow {
18    #[tabled(rename = "Code")]
19    code: String,
20    #[tabled(rename = "Name")]
21    name: String,
22    #[tabled(rename = "Type")]
23    contract_type: String,
24    #[tabled(rename = "Size")]
25    size: String,
26    #[tabled(rename = "Last Trade")]
27    last_trade: String,
28}
29
30#[derive(Serialize)]
31struct FutureJson {
32    code: String,
33    name: String,
34    contract_type: String,
35    contract_size: f64,
36    last_trade_time: String,
37}
38
39pub async fn run_future_info(
40    gateway: &str,
41    symbols: &[String],
42    format: OutputFormat,
43) -> Result<()> {
44    if symbols.is_empty() {
45        bail!("no symbols");
46    }
47    let secs: Vec<_> = symbols
48        .iter()
49        .map(|s| parse_symbol(s))
50        .collect::<Result<Vec<_>>>()?;
51    let (client, _rx) = connect_gateway(gateway, "futucli-future-info").await?;
52    let proto_secs: Vec<_> = secs
53        .iter()
54        .map(|s| futu_proto::qot_common::Security {
55            market: s.market as i32,
56            code: s.code.clone(),
57        })
58        .collect();
59    let req = futu_proto::qot_get_future_info::Request {
60        c2s: futu_proto::qot_get_future_info::C2s {
61            security_list: proto_secs,
62            header: None,
63        },
64    };
65    let body = req.encode_to_vec();
66    let frame = client
67        .request(futu_core::proto_id::QOT_GET_FUTURE_INFO, body)
68        .await?;
69    let resp = futu_proto::qot_get_future_info::Response::decode(frame.body.as_ref())
70        .map_err(|e| anyhow!("decode future_info: {e}"))?;
71    if resp.ret_type != 0 {
72        bail!(
73            "future_info ret_type={} msg={:?}",
74            resp.ret_type,
75            resp.ret_msg
76        );
77    }
78    let s2c = resp.s2c.ok_or_else(|| anyhow!("missing s2c"))?;
79    let mut rows = Vec::new();
80    let mut jsons = Vec::new();
81    for f in &s2c.future_info_list {
82        rows.push(FutureRow {
83            code: f.security.code.clone(),
84            name: f.name.clone(),
85            contract_type: f.contract_type.clone(),
86            size: format!("{:.2}", f.contract_size),
87            last_trade: f.last_trade_time.clone(),
88        });
89        jsons.push(FutureJson {
90            code: f.security.code.clone(),
91            name: f.name.clone(),
92            contract_type: f.contract_type.clone(),
93            contract_size: f.contract_size,
94            last_trade_time: f.last_trade_time.clone(),
95        });
96    }
97    format.print_rows(&rows, &jsons)?;
98    Ok(())
99}
100
101#[derive(Tabled)]
102struct StockFilterRow {
103    #[tabled(rename = "Code")]
104    code: String,
105    #[tabled(rename = "Name")]
106    name: String,
107}
108
109#[derive(Serialize)]
110struct StockFilterJson {
111    code: String,
112    name: String,
113}
114
115pub async fn run_stock_filter(
116    gateway: &str,
117    market: &str,
118    begin: i32,
119    num: i32,
120    format: OutputFormat,
121) -> Result<()> {
122    // v1.4.111 codex: 不静默 clamp num. 越界 (begin<0 / num∉[0, 200])
123    // 走 Err; num=0 对齐 C++ 作为合法空页请求.
124    let bounds = futu_qot::page_bounds::validate_begin_num(begin, num, 200, "stock_filter")
125        .map_err(|e| anyhow!("{}", e))?;
126    let m = parse_qot_market(market)?;
127    let (client, _rx) = connect_gateway(gateway, "futucli-stock-filter").await?;
128    let req = futu_proto::qot_stock_filter::Request {
129        c2s: futu_proto::qot_stock_filter::C2s {
130            begin: bounds.begin,
131            num: bounds.num,
132            market: m,
133            plate: None,
134            base_filter_list: vec![],
135            accumulate_filter_list: vec![],
136            financial_filter_list: vec![],
137            pattern_filter_list: vec![],
138            custom_indicator_filter_list: vec![],
139            header: None,
140        },
141    };
142    let body = req.encode_to_vec();
143    let frame = client
144        .request(futu_core::proto_id::QOT_STOCK_FILTER, body)
145        .await?;
146    let resp = futu_proto::qot_stock_filter::Response::decode(frame.body.as_ref())
147        .map_err(|e| anyhow!("decode stock_filter: {e}"))?;
148    if resp.ret_type != 0 {
149        bail!(
150            "stock_filter ret_type={} msg={:?}",
151            resp.ret_type,
152            resp.ret_msg
153        );
154    }
155    let s2c = resp.s2c.ok_or_else(|| anyhow!("missing s2c"))?;
156    let mut rows = Vec::new();
157    let mut jsons = Vec::new();
158    for d in &s2c.data_list {
159        rows.push(StockFilterRow {
160            code: d.security.code.clone(),
161            name: d.name.clone(),
162        });
163        jsons.push(StockFilterJson {
164            code: d.security.code.clone(),
165            name: d.name.clone(),
166        });
167    }
168    format.print_rows(&rows, &jsons)?;
169    Ok(())
170}
171
172pub async fn run_option_chain(
173    gateway: &str,
174    owner: &str,
175    begin: &str,
176    end: &str,
177    option_type_str: &str,
178    greek_filter: OptionChainGreekFilterArgs,
179    format: OutputFormat,
180) -> Result<()> {
181    let owner_sec = parse_symbol(owner)?;
182    let option_type = match option_type_str.trim().to_ascii_lowercase().as_str() {
183        "all" => Some(0),
184        "call" => Some(1),
185        "put" => Some(2),
186        other => bail!("unknown option_type {other:?} (all|call|put)"),
187    };
188    let (client, _rx) = connect_gateway(gateway, "futucli-option-chain").await?;
189
190    let s2c = futu_qot::market_misc::get_option_chain(
191        &client,
192        &owner_sec,
193        begin,
194        end,
195        option_type,
196        None,
197        greek_filter.into_data_filter(),
198    )
199    .await?;
200
201    let mut rows: Vec<OptionChainRow> = Vec::new();
202    let mut jsons: Vec<OptionChainJson> = Vec::new();
203    for entry in &s2c.option_chain {
204        let mut calls: Vec<String> = Vec::new();
205        let mut puts: Vec<String> = Vec::new();
206        for item in &entry.option {
207            if let Some(c) = &item.call {
208                calls.push(c.basic.security.code.clone());
209            }
210            if let Some(p) = &item.put {
211                puts.push(p.basic.security.code.clone());
212            }
213        }
214        rows.push(OptionChainRow {
215            strike_time: entry.strike_time.clone(),
216            call_count: calls.len(),
217            put_count: puts.len(),
218            example_call: calls.first().cloned().unwrap_or_default(),
219            example_put: puts.first().cloned().unwrap_or_default(),
220        });
221        jsons.push(OptionChainJson {
222            strike_time: entry.strike_time.clone(),
223            call_symbols: calls,
224            put_symbols: puts,
225        });
226    }
227
228    format.print_rows(&rows, &jsons)?;
229    Ok(())
230}
231
232// ============================================================
233// v1.4.30 P2(100% 覆盖): history-kl-quota / holding-change /
234//           modify-user-security / code-change /
235//           set-price-reminder / price-reminder /
236//           option-expiration-date
237// ============================================================
238
239// v1.4.98 external reviewer BUG-005 fix (P2, 2026-04-27): 之前 _format 标 unused, 用户传
240// `-o json` 仍打 "used: X / remain: Y" 文本. 加 format-aware output.
241#[derive(serde::Serialize, tabled::Tabled)]
242struct HistoryKlQuotaRow {
243    field: String,
244    value: String,
245}
246
247#[derive(serde::Serialize)]
248struct HistoryKlQuotaJson {
249    used_quota: i32,
250    remain_quota: i32,
251    detail_count: usize,
252}
253
254pub async fn run_history_kl_quota(gateway: &str, detail: bool, format: OutputFormat) -> Result<()> {
255    let (client, _rx) = connect_gateway(gateway, "futucli-hist-kl-quota").await?;
256    let req = futu_proto::qot_request_history_kl_quota::Request {
257        c2s: futu_proto::qot_request_history_kl_quota::C2s {
258            b_get_detail: Some(detail),
259            header: None,
260        },
261    };
262    let body = req.encode_to_vec();
263    let frame = client
264        .request(futu_core::proto_id::QOT_REQUEST_HISTORY_KL_QUOTA, body)
265        .await?;
266    let resp = futu_proto::qot_request_history_kl_quota::Response::decode(frame.body.as_ref())
267        .map_err(|e| anyhow!("decode: {e}"))?;
268    if resp.ret_type != 0 {
269        bail!(
270            "history_kl_quota ret_type={} msg={:?}",
271            resp.ret_type,
272            resp.ret_msg
273        );
274    }
275    let s = resp.s2c.ok_or_else(|| anyhow!("missing s2c"))?;
276    let rows = vec![
277        HistoryKlQuotaRow {
278            field: "used_quota".into(),
279            value: s.used_quota.to_string(),
280        },
281        HistoryKlQuotaRow {
282            field: "remain_quota".into(),
283            value: s.remain_quota.to_string(),
284        },
285        HistoryKlQuotaRow {
286            field: "detail_count".into(),
287            value: s.detail_list.len().to_string(),
288        },
289    ];
290    let json = HistoryKlQuotaJson {
291        used_quota: s.used_quota,
292        remain_quota: s.remain_quota,
293        detail_count: s.detail_list.len(),
294    };
295    format.print_rows(&rows, &[json])?;
296    Ok(())
297}