1use anyhow::{Result, anyhow, bail};
6use prost::Message;
7use serde::Serialize;
8use tabled::Tabled;
9
10use crate::common::{connect_gateway, format_symbol, parse_symbol};
11use crate::output::OutputFormat;
12use crate::qot_sdk_adapter;
13
14#[derive(Tabled)]
19struct MarketStateRow {
20 #[tabled(rename = "Symbol")]
21 symbol: String,
22 #[tabled(rename = "Name")]
23 name: String,
24 #[tabled(rename = "State")]
25 state: String,
26}
27
28#[derive(Serialize)]
29struct MarketStateJson {
30 symbol: String,
31 name: String,
32 market_state: i32,
33}
34
35use futu_core::market::qot_market_state_label as market_state_label;
38
39fn market_state_symbol(sec: &futu_proto::qot_common::Security) -> String {
40 let sec = qot_sdk_adapter::security_from_proto(sec);
41 format_symbol(&sec)
42}
43
44pub async fn run_market_state(
45 gateway: &str,
46 symbols: &[String],
47 format: OutputFormat,
48) -> Result<()> {
49 if symbols.is_empty() {
50 bail!("no symbols");
51 }
52 let (client, _rx) = connect_gateway(gateway, "futucli-market-state").await?;
53
54 let secs: Vec<_> = symbols
55 .iter()
56 .map(|s| parse_symbol(s))
57 .collect::<Result<Vec<_>>>()?;
58 let sec_protos: Vec<_> = secs
59 .iter()
60 .map(|s| futu_proto::qot_common::Security {
61 market: s.market as i32,
62 code: s.code.clone(),
63 })
64 .collect();
65
66 let req = futu_proto::qot_get_market_state::Request {
67 c2s: futu_proto::qot_get_market_state::C2s {
68 security_list: sec_protos,
69 header: None,
70 },
71 };
72 let body = req.encode_to_vec();
73 let frame = client
74 .request(futu_core::proto_id::QOT_GET_MARKET_STATE, body)
75 .await?;
76 let resp = futu_proto::qot_get_market_state::Response::decode(frame.body.as_ref())?;
77 if resp.ret_type != 0 {
78 bail!("market_state ret_type={}", resp.ret_type);
79 }
80 let s = resp.s2c.ok_or_else(|| anyhow!("missing s2c"))?;
81
82 let rows: Vec<MarketStateRow> = s
83 .market_info_list
84 .iter()
85 .map(|m| MarketStateRow {
86 symbol: market_state_symbol(&m.security),
87 name: m.name.clone(),
88 state: format!(
89 "{} ({})",
90 market_state_label(m.market_state),
91 m.market_state
92 ),
93 })
94 .collect();
95
96 let jsons: Vec<MarketStateJson> = s
97 .market_info_list
98 .iter()
99 .map(|m| MarketStateJson {
100 symbol: market_state_symbol(&m.security),
101 name: m.name.clone(),
102 market_state: m.market_state,
103 })
104 .collect();
105
106 format.print_rows(&rows, &jsons)?;
107 Ok(())
108}
109
110#[cfg(test)]
111mod tests;
112
113#[derive(Tabled)]
118struct OwnerPlateRow {
119 #[tabled(rename = "Symbol")]
120 symbol: String,
121 #[tabled(rename = "Plate")]
122 plate: String,
123 #[tabled(rename = "Name")]
124 name: String,
125 #[tabled(rename = "Type")]
126 plate_type: String,
127}
128
129#[derive(Serialize)]
130struct OwnerPlateJson {
131 symbol: String,
132 plate_code: String,
133 plate_name: String,
134 plate_type: i32,
135}
136
137fn plate_type_label(t: i32) -> &'static str {
138 match t {
139 1 => "Industry",
140 2 => "Region",
141 3 => "Concept",
142 4 => "Other",
143 _ => "Unknown",
144 }
145}
146
147pub async fn run_owner_plate(
148 gateway: &str,
149 symbols: &[String],
150 format: OutputFormat,
151) -> Result<()> {
152 if symbols.is_empty() {
153 bail!("no symbols");
154 }
155 let (client, _rx) = connect_gateway(gateway, "futucli-owner-plate").await?;
156 let secs: Vec<_> = symbols
157 .iter()
158 .map(|s| parse_symbol(s))
159 .collect::<Result<Vec<_>>>()?;
160
161 let s2c = futu_qot::market_misc::get_owner_plate(&client, &secs).await?;
162
163 let mut rows: Vec<OwnerPlateRow> = Vec::new();
164 let mut jsons: Vec<OwnerPlateJson> = Vec::new();
165 for entry in &s2c.owner_plate_list {
166 let sym = format!("{}.{}", entry.security.market, entry.security.code);
167 for p in &entry.plate_info_list {
168 rows.push(OwnerPlateRow {
169 symbol: sym.clone(),
170 plate: p.plate.code.clone(),
171 name: p.name.clone(),
172 plate_type: format!(
173 "{} ({})",
174 plate_type_label(p.plate_type.unwrap_or(0)),
175 p.plate_type.unwrap_or(0)
176 ),
177 });
178 jsons.push(OwnerPlateJson {
179 symbol: sym.clone(),
180 plate_code: p.plate.code.clone(),
181 plate_name: p.name.clone(),
182 plate_type: p.plate_type.unwrap_or(0),
183 });
184 }
185 }
186
187 format.print_rows(&rows, &jsons)?;
188 Ok(())
189}
190
191#[derive(Tabled)]
196struct ReferenceRow {
197 #[tabled(rename = "Code")]
198 code: String,
199 #[tabled(rename = "Name")]
200 name: String,
201 #[tabled(rename = "Lot")]
202 lot_size: i32,
203 #[tabled(rename = "Type")]
204 sec_type: i32,
205}
206
207#[derive(Serialize)]
208struct ReferenceJson {
209 code: String,
210 name: String,
211 lot_size: i32,
212 sec_type: i32,
213}
214
215fn parse_reference_type(s: &str) -> Result<i32> {
216 match s.trim().to_ascii_lowercase().as_str() {
217 "warrant" => Ok(1),
218 "future" | "futures" => Ok(2),
219 "option" | "options" => bail!(
221 "reference type option is not supported by backend; use futucli option-chain instead"
222 ),
223 other => bail!("unknown reference type {other:?}; use warrant|future"),
224 }
225}
226
227pub async fn run_reference(
228 gateway: &str,
229 symbol: &str,
230 reference_type: &str,
231 format: OutputFormat,
232) -> Result<()> {
233 let sec = parse_symbol(symbol)?;
234 let ref_type = parse_reference_type(reference_type)?;
235 let (client, _rx) = connect_gateway(gateway, "futucli-reference").await?;
236 let list = futu_qot::market_misc::get_reference(&client, &sec, ref_type).await?;
237
238 let rows: Vec<ReferenceRow> = list
239 .iter()
240 .map(|s| ReferenceRow {
241 code: s.security.code.clone(),
242 name: s.name.clone(),
243 lot_size: s.lot_size,
244 sec_type: s.sec_type,
245 })
246 .collect();
247 let jsons: Vec<ReferenceJson> = list
248 .iter()
249 .map(|s| ReferenceJson {
250 code: s.security.code.clone(),
251 name: s.name.clone(),
252 lot_size: s.lot_size,
253 sec_type: s.sec_type,
254 })
255 .collect();
256
257 format.print_rows(&rows, &jsons)?;
258 Ok(())
259}