Skip to main content

futucli/cmd/analysis/
screen.rs

1//! Stock screener CLI adapter.
2
3use anyhow::{Result, anyhow, bail};
4use prost::Message;
5use serde::Serialize;
6use tabled::Tabled;
7
8use crate::common::connect_gateway;
9use crate::output::OutputFormat;
10
11#[derive(Tabled)]
12struct StockScreenRow {
13    #[tabled(rename = "Stock ID")]
14    stock_id: String,
15    #[tabled(rename = "Results")]
16    result_count: usize,
17}
18
19#[derive(Serialize)]
20struct StockScreenJson {
21    data_list: Vec<futu_proto::qot_stock_screen::StockScreenItem>,
22    last_page: Option<i32>,
23    all_count: Option<i32>,
24}
25
26#[derive(Tabled)]
27struct OptionScreenRow {
28    #[tabled(rename = "Security")]
29    security: String,
30    #[tabled(rename = "Name")]
31    name: String,
32    #[tabled(rename = "Price")]
33    price: String,
34}
35
36#[derive(Serialize)]
37struct OptionScreenJson {
38    data_list: Vec<futu_proto::qot_option_screen::OptionScreenItem>,
39    last_page: bool,
40    all_count: i32,
41}
42
43#[derive(Tabled)]
44struct WarrantScreenRow {
45    #[tabled(rename = "Stock ID")]
46    stock_id: String,
47    #[tabled(rename = "Owner")]
48    stock_owner: String,
49    #[tabled(rename = "Price")]
50    current_price: String,
51}
52
53#[derive(Serialize)]
54struct WarrantScreenJson {
55    warrants: Vec<futu_proto::qot_warrant_screen::WarrantItem>,
56    last_page: Option<bool>,
57    all_count: Option<i32>,
58}
59
60fn parse_stock_screen_c2s(json: &str) -> Result<futu_proto::qot_stock_screen::C2s> {
61    let value: serde_json::Value =
62        serde_json::from_str(json).map_err(|e| anyhow!("stock-screen c2s json: {e}"))?;
63    serde_json::from_value(value).map_err(|e| anyhow!("stock-screen c2s json: {e}"))
64}
65
66fn parse_option_screen_c2s(json: &str) -> Result<futu_proto::qot_option_screen::C2s> {
67    let value: serde_json::Value =
68        serde_json::from_str(json).map_err(|e| anyhow!("option-screen c2s json: {e}"))?;
69    serde_json::from_value(value).map_err(|e| anyhow!("option-screen c2s json: {e}"))
70}
71
72fn parse_warrant_screen_c2s(json: &str) -> Result<futu_proto::qot_warrant_screen::C2s> {
73    let value: serde_json::Value =
74        serde_json::from_str(json).map_err(|e| anyhow!("warrant-screen c2s json: {e}"))?;
75    serde_json::from_value(value).map_err(|e| anyhow!("warrant-screen c2s json: {e}"))
76}
77
78pub async fn run_stock_screen(gateway: &str, c2s_json: &str, format: OutputFormat) -> Result<()> {
79    let c2s = parse_stock_screen_c2s(c2s_json)?;
80    let (client, _rx) = connect_gateway(gateway, "futucli-stock-screen").await?;
81    let req = futu_proto::qot_stock_screen::Request { c2s };
82    let frame = client
83        .request(futu_core::proto_id::QOT_STOCK_SCREEN, req.encode_to_vec())
84        .await?;
85    let resp = futu_proto::qot_stock_screen::Response::decode(frame.body.as_ref())
86        .map_err(|e| anyhow!("decode stock_screen: {e}"))?;
87    if resp.ret_type != 0 {
88        bail!(
89            "stock_screen ret_type={} msg={:?}",
90            resp.ret_type,
91            resp.ret_msg
92        );
93    }
94    let s2c = resp.s2c.ok_or_else(|| anyhow!("missing s2c"))?;
95    let rows: Vec<StockScreenRow> = s2c
96        .data_list
97        .iter()
98        .map(|item| StockScreenRow {
99            stock_id: item.stock_id.map(|id| id.to_string()).unwrap_or_default(),
100            result_count: item.results.len(),
101        })
102        .collect();
103    let json = StockScreenJson {
104        data_list: s2c.data_list,
105        last_page: s2c.last_page,
106        all_count: s2c.all_count,
107    };
108    format.print_rows(&rows, &[json])?;
109    Ok(())
110}
111
112pub async fn run_option_screen(gateway: &str, c2s_json: &str, format: OutputFormat) -> Result<()> {
113    let c2s = parse_option_screen_c2s(c2s_json)?;
114    let (client, _rx) = connect_gateway(gateway, "futucli-option-screen").await?;
115    let req = futu_proto::qot_option_screen::Request { c2s };
116    let frame = client
117        .request(futu_core::proto_id::QOT_OPTION_SCREEN, req.encode_to_vec())
118        .await?;
119    let resp = futu_proto::qot_option_screen::Response::decode(frame.body.as_ref())
120        .map_err(|e| anyhow!("decode option_screen: {e}"))?;
121    if resp.ret_type != 0 {
122        bail!(
123            "option_screen ret_type={} msg={:?}",
124            resp.ret_type,
125            resp.ret_msg
126        );
127    }
128    let s2c = resp.s2c.ok_or_else(|| anyhow!("missing s2c"))?;
129    let rows: Vec<OptionScreenRow> = s2c
130        .data_list
131        .iter()
132        .map(|item| {
133            let security = item
134                .security
135                .as_ref()
136                .map(|sec| format!("{}.{}", sec.market, sec.code))
137                .unwrap_or_default();
138            OptionScreenRow {
139                security,
140                name: item.option_name.clone().unwrap_or_default(),
141                price: item.price.map(|v| v.to_string()).unwrap_or_default(),
142            }
143        })
144        .collect();
145    let json = OptionScreenJson {
146        data_list: s2c.data_list,
147        last_page: s2c.last_page,
148        all_count: s2c.all_count,
149    };
150    format.print_rows(&rows, &[json])?;
151    Ok(())
152}
153
154pub async fn run_warrant_screen(gateway: &str, c2s_json: &str, format: OutputFormat) -> Result<()> {
155    let c2s = parse_warrant_screen_c2s(c2s_json)?;
156    let (client, _rx) = connect_gateway(gateway, "futucli-warrant-screen").await?;
157    let req = futu_proto::qot_warrant_screen::Request { c2s };
158    let frame = client
159        .request(futu_core::proto_id::QOT_WARRANT_SCREEN, req.encode_to_vec())
160        .await?;
161    let resp = futu_proto::qot_warrant_screen::Response::decode(frame.body.as_ref())
162        .map_err(|e| anyhow!("decode warrant_screen: {e}"))?;
163    if resp.ret_type != 0 {
164        bail!(
165            "warrant_screen ret_type={} msg={:?}",
166            resp.ret_type,
167            resp.ret_msg
168        );
169    }
170    let s2c = resp.s2c.ok_or_else(|| anyhow!("missing s2c"))?;
171    let rows: Vec<WarrantScreenRow> = s2c
172        .warrants
173        .iter()
174        .map(|item| WarrantScreenRow {
175            stock_id: item.stock_id.map(|id| id.to_string()).unwrap_or_default(),
176            stock_owner: item
177                .stock_owner
178                .map(|id| id.to_string())
179                .unwrap_or_default(),
180            current_price: item
181                .current_price
182                .map(|price| price.to_string())
183                .unwrap_or_default(),
184        })
185        .collect();
186    let json = WarrantScreenJson {
187        warrants: s2c.warrants,
188        last_page: s2c.last_page,
189        all_count: s2c.all_count,
190    };
191    format.print_rows(&rows, &[json])?;
192    Ok(())
193}