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")
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#[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}