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.106 codex 0635 ζ36 F3+F4: 不再静默 clamp num. 越界
123    // (begin<0 / num∉[1, 200]) 走 Err 让用户看到清晰错误. num=0 4 surface
124    // 一致 loud reject.
125    let bounds = futu_qot::page_bounds::validate_begin_num(begin, num, 200, "stock_filter")
126        .map_err(|e| anyhow!("{}", e))?;
127    let m = parse_qot_market(market)?;
128    let (client, _rx) = connect_gateway(gateway, "futucli-stock-filter").await?;
129    let req = futu_proto::qot_stock_filter::Request {
130        c2s: futu_proto::qot_stock_filter::C2s {
131            begin: bounds.begin,
132            num: bounds.num,
133            market: m,
134            plate: None,
135            base_filter_list: vec![],
136            accumulate_filter_list: vec![],
137            financial_filter_list: vec![],
138            pattern_filter_list: vec![],
139            custom_indicator_filter_list: vec![],
140            header: None,
141        },
142    };
143    let body = req.encode_to_vec();
144    let frame = client
145        .request(futu_core::proto_id::QOT_STOCK_FILTER, body)
146        .await?;
147    let resp = futu_proto::qot_stock_filter::Response::decode(frame.body.as_ref())
148        .map_err(|e| anyhow!("decode stock_filter: {e}"))?;
149    if resp.ret_type != 0 {
150        bail!(
151            "stock_filter ret_type={} msg={:?}",
152            resp.ret_type,
153            resp.ret_msg
154        );
155    }
156    let s2c = resp.s2c.ok_or_else(|| anyhow!("missing s2c"))?;
157    let mut rows = Vec::new();
158    let mut jsons = Vec::new();
159    for d in &s2c.data_list {
160        rows.push(StockFilterRow {
161            code: d.security.code.clone(),
162            name: d.name.clone(),
163        });
164        jsons.push(StockFilterJson {
165            code: d.security.code.clone(),
166            name: d.name.clone(),
167        });
168    }
169    format.print_rows(&rows, &jsons)?;
170    Ok(())
171}
172
173pub async fn run_option_chain(
174    gateway: &str,
175    owner: &str,
176    begin: &str,
177    end: &str,
178    option_type_str: &str,
179    greek_filter: OptionChainGreekFilterArgs,
180    format: OutputFormat,
181) -> Result<()> {
182    let owner_sec = parse_symbol(owner)?;
183    let option_type = match option_type_str.trim().to_ascii_lowercase().as_str() {
184        "all" => Some(0),
185        "call" => Some(1),
186        "put" => Some(2),
187        other => bail!("unknown option_type {other:?} (all|call|put)"),
188    };
189    let (client, _rx) = connect_gateway(gateway, "futucli-option-chain").await?;
190
191    let s2c = futu_qot::market_misc::get_option_chain(
192        &client,
193        &owner_sec,
194        begin,
195        end,
196        option_type,
197        None,
198        greek_filter.into_data_filter(),
199    )
200    .await?;
201
202    let mut rows: Vec<OptionChainRow> = Vec::new();
203    let mut jsons: Vec<OptionChainJson> = Vec::new();
204    for entry in &s2c.option_chain {
205        let mut calls: Vec<String> = Vec::new();
206        let mut puts: Vec<String> = Vec::new();
207        for item in &entry.option {
208            if let Some(c) = &item.call {
209                calls.push(c.basic.security.code.clone());
210            }
211            if let Some(p) = &item.put {
212                puts.push(p.basic.security.code.clone());
213            }
214        }
215        rows.push(OptionChainRow {
216            strike_time: entry.strike_time.clone(),
217            call_count: calls.len(),
218            put_count: puts.len(),
219            example_call: calls.first().cloned().unwrap_or_default(),
220            example_put: puts.first().cloned().unwrap_or_default(),
221        });
222        jsons.push(OptionChainJson {
223            strike_time: entry.strike_time.clone(),
224            call_symbols: calls,
225            put_symbols: puts,
226        });
227    }
228
229    format.print_rows(&rows, &jsons)?;
230    Ok(())
231}
232
233// ============================================================
234// v1.4.30 P2(100% 覆盖): history-kl-quota / holding-change /
235//           modify-user-security / code-change /
236//           set-price-reminder / price-reminder /
237//           option-expiration-date
238// ============================================================
239
240// v1.4.98 eli BUG-005 fix (P2, 2026-04-27): 之前 _format 标 unused, 用户传
241// `-o json` 仍打 "used: X / remain: Y" 文本. 加 format-aware output.
242#[derive(serde::Serialize, tabled::Tabled)]
243struct HistoryKlQuotaRow {
244    field: String,
245    value: String,
246}
247
248#[derive(serde::Serialize)]
249struct HistoryKlQuotaJson {
250    used_quota: i32,
251    remain_quota: i32,
252    detail_count: usize,
253}
254
255pub async fn run_history_kl_quota(gateway: &str, detail: bool, format: OutputFormat) -> Result<()> {
256    let (client, _rx) = connect_gateway(gateway, "futucli-hist-kl-quota").await?;
257    let req = futu_proto::qot_request_history_kl_quota::Request {
258        c2s: futu_proto::qot_request_history_kl_quota::C2s {
259            b_get_detail: Some(detail),
260            header: None,
261        },
262    };
263    let body = req.encode_to_vec();
264    let frame = client
265        .request(futu_core::proto_id::QOT_REQUEST_HISTORY_KL_QUOTA, body)
266        .await?;
267    let resp = futu_proto::qot_request_history_kl_quota::Response::decode(frame.body.as_ref())
268        .map_err(|e| anyhow!("decode: {e}"))?;
269    if resp.ret_type != 0 {
270        bail!(
271            "history_kl_quota ret_type={} msg={:?}",
272            resp.ret_type,
273            resp.ret_msg
274        );
275    }
276    let s = resp.s2c.ok_or_else(|| anyhow!("missing s2c"))?;
277    let rows = vec![
278        HistoryKlQuotaRow {
279            field: "used_quota".into(),
280            value: s.used_quota.to_string(),
281        },
282        HistoryKlQuotaRow {
283            field: "remain_quota".into(),
284            value: s.remain_quota.to_string(),
285        },
286        HistoryKlQuotaRow {
287            field: "detail_count".into(),
288            value: s.detail_list.len().to_string(),
289        },
290    ];
291    let json = HistoryKlQuotaJson {
292        used_quota: s.used_quota,
293        remain_quota: s.remain_quota,
294        detail_count: s.detail_list.len(),
295    };
296    format.print_rows(&rows, &[json])?;
297    Ok(())
298}