1use 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 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#[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}