Skip to main content

futucli/cmd/analysis/
market_plate.rs

1//! v1.4.110+ split (from cmd/analysis.rs): market_plate domain.
2//!
3//! pub items: run_market_state,run_owner_plate,run_reference.
4
5use 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// ============================================================
15// market-state
16// ============================================================
17
18#[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
35// Market-state display labels live in futu-core; this command keeps only
36// surface formatting and backend calls.
37use 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// ============================================================
114// owner-plate
115// ============================================================
116
117#[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// ============================================================
192// reference
193// ============================================================
194
195#[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        // 保持和 MCP 当前能力边界一致:backend 对 option 不支持,option 用 option-chain。
220        "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}