Skip to main content

futucli/cmd/analysis/company/
research.rs

1use anyhow::{Result, anyhow, bail};
2use prost::Message;
3use tabled::Tabled;
4
5use crate::common::{connect_gateway, parse_symbol};
6use crate::output::OutputFormat;
7
8use super::{display_opt, display_opt_f64, display_opt_i32};
9
10#[derive(Tabled)]
11struct ResearchAnalystConsensusRow {
12    #[tabled(rename = "Highest")]
13    highest: String,
14    #[tabled(rename = "Average")]
15    average: String,
16    #[tabled(rename = "Lowest")]
17    lowest: String,
18    #[tabled(rename = "Rating")]
19    rating: String,
20    #[tabled(rename = "Total")]
21    total: String,
22    #[tabled(rename = "Buy %")]
23    buy: String,
24    #[tabled(rename = "Hold %")]
25    hold: String,
26    #[tabled(rename = "Sell %")]
27    sell: String,
28    #[tabled(rename = "Updated")]
29    updated: String,
30}
31
32pub async fn run_research_analyst_consensus(
33    gateway: &str,
34    symbol: &str,
35    format: OutputFormat,
36) -> Result<()> {
37    let sec = parse_symbol(symbol)?;
38    let (client, _rx) = connect_gateway(gateway, "futucli-research-analyst-consensus").await?;
39
40    let req = futu_proto::qot_get_research_analyst_consensus::Request {
41        c2s: futu_proto::qot_get_research_analyst_consensus::C2s {
42            security: futu_proto::qot_common::Security {
43                market: sec.market as i32,
44                code: sec.code.clone(),
45            },
46        },
47    };
48    let body = req.encode_to_vec();
49    let frame = client
50        .request(
51            futu_core::proto_id::QOT_GET_RESEARCH_ANALYST_CONSENSUS,
52            body,
53        )
54        .await?;
55    let resp =
56        futu_proto::qot_get_research_analyst_consensus::Response::decode(frame.body.as_ref())
57            .map_err(|e| anyhow!("decode research_analyst_consensus: {e}"))?;
58    if resp.ret_type != 0 {
59        bail!(
60            "research_analyst_consensus ret_type={} msg={:?}",
61            resp.ret_type,
62            resp.ret_msg
63        );
64    }
65    let s2c = resp.s2c.ok_or_else(|| anyhow!("missing s2c"))?;
66    let rows = [ResearchAnalystConsensusRow {
67        highest: display_opt_f64(s2c.highest),
68        average: display_opt_f64(s2c.average),
69        lowest: display_opt_f64(s2c.lowest),
70        rating: display_opt_i32(s2c.rating),
71        total: display_opt_i32(s2c.total),
72        buy: display_opt_f64(s2c.buy),
73        hold: display_opt_f64(s2c.hold),
74        sell: display_opt_f64(s2c.sell),
75        updated: display_opt(&s2c.update_time_str),
76    }];
77    let json = serde_json::json!({
78        "symbol": symbol,
79        "highest": s2c.highest,
80        "average": s2c.average,
81        "lowest": s2c.lowest,
82        "rating": s2c.rating,
83        "total": s2c.total,
84        "update_time": s2c.update_time,
85        "update_time_str": s2c.update_time_str,
86        "buy": s2c.buy,
87        "hold": s2c.hold,
88        "sell": s2c.sell,
89        "strong_buy": s2c.strong_buy,
90        "underperform": s2c.underperform,
91    });
92    format.print_rows(&rows, &[json])?;
93    Ok(())
94}
95
96#[derive(Tabled)]
97struct ResearchRatingSummaryRow {
98    #[tabled(rename = "Symbol")]
99    symbol: String,
100    #[tabled(rename = "Next Key")]
101    next_key: String,
102    #[tabled(rename = "Inst Summary")]
103    inst_summary: String,
104    #[tabled(rename = "Analyst Summary")]
105    analyst_summary: String,
106    #[tabled(rename = "Inst Detail Ratings")]
107    inst_detail_ratings: String,
108    #[tabled(rename = "Analyst Detail Ratings")]
109    analyst_detail_ratings: String,
110}
111
112pub async fn run_research_rating_summary(
113    gateway: &str,
114    symbol: &str,
115    rating_dimension_type: Option<i32>,
116    uid: Option<&str>,
117    next_key: Option<&str>,
118    num: Option<i32>,
119    format: OutputFormat,
120) -> Result<()> {
121    let sec = parse_symbol(symbol)?;
122    let (client, _rx) = connect_gateway(gateway, "futucli-research-rating-summary").await?;
123
124    let req = futu_proto::qot_get_research_rating_summary::Request {
125        c2s: futu_proto::qot_get_research_rating_summary::C2s {
126            security: futu_proto::qot_common::Security {
127                market: sec.market as i32,
128                code: sec.code.clone(),
129            },
130            rating_dimension_type,
131            uid: uid.map(str::to_string),
132            next_key: next_key.map(str::to_string),
133            num,
134        },
135    };
136    let body = req.encode_to_vec();
137    let frame = client
138        .request(futu_core::proto_id::QOT_GET_RESEARCH_RATING_SUMMARY, body)
139        .await?;
140    let resp = futu_proto::qot_get_research_rating_summary::Response::decode(frame.body.as_ref())
141        .map_err(|e| anyhow!("decode research_rating_summary: {e}"))?;
142    if resp.ret_type != 0 {
143        bail!(
144            "research_rating_summary ret_type={} msg={:?}",
145            resp.ret_type,
146            resp.ret_msg
147        );
148    }
149    let s2c = resp.s2c.ok_or_else(|| anyhow!("missing s2c"))?;
150    let inst_detail_count = s2c
151        .inst_rating_detail
152        .as_ref()
153        .map(|detail| detail.rating_item_list.len())
154        .unwrap_or_default();
155    let analyst_detail_count = s2c
156        .analyst_rating_detail
157        .as_ref()
158        .map(|detail| detail.rating_item_list.len())
159        .unwrap_or_default();
160    let rows = [ResearchRatingSummaryRow {
161        symbol: symbol.to_string(),
162        next_key: display_opt(&s2c.next_key),
163        inst_summary: s2c.inst_rating_summary_list.len().to_string(),
164        analyst_summary: s2c.analyst_rating_summary_list.len().to_string(),
165        inst_detail_ratings: inst_detail_count.to_string(),
166        analyst_detail_ratings: analyst_detail_count.to_string(),
167    }];
168    let json = serde_json::json!({
169        "symbol": symbol,
170        "inst_rating_summary_list": s2c.inst_rating_summary_list,
171        "analyst_rating_summary_list": s2c.analyst_rating_summary_list,
172        "inst_rating_detail": s2c.inst_rating_detail,
173        "analyst_rating_detail": s2c.analyst_rating_detail,
174        "next_key": s2c.next_key,
175    });
176    format.print_rows(&rows, &[json])?;
177    Ok(())
178}
179
180#[derive(Tabled)]
181struct ResearchMorningstarReportRow {
182    #[tabled(rename = "Symbol")]
183    symbol: String,
184    #[tabled(rename = "Rating Type")]
185    rating_type: String,
186    #[tabled(rename = "Stars")]
187    star_rating: String,
188    #[tabled(rename = "Fair Value")]
189    fair_value: String,
190    #[tabled(rename = "Moat")]
191    economic_moat: String,
192    #[tabled(rename = "Uncertainty")]
193    uncertainty: String,
194    #[tabled(rename = "PDF")]
195    pdf_url: String,
196}
197
198pub async fn run_research_morningstar_report(
199    gateway: &str,
200    symbol: &str,
201    format: OutputFormat,
202) -> Result<()> {
203    let sec = parse_symbol(symbol)?;
204    let (client, _rx) = connect_gateway(gateway, "futucli-research-morningstar-report").await?;
205
206    let req = futu_proto::qot_get_research_morningstar_report::Request {
207        c2s: futu_proto::qot_get_research_morningstar_report::C2s {
208            security: futu_proto::qot_common::Security {
209                market: sec.market as i32,
210                code: sec.code.clone(),
211            },
212        },
213    };
214    let body = req.encode_to_vec();
215    let frame = client
216        .request(
217            futu_core::proto_id::QOT_GET_RESEARCH_MORNINGSTAR_REPORT,
218            body,
219        )
220        .await?;
221    let resp =
222        futu_proto::qot_get_research_morningstar_report::Response::decode(frame.body.as_ref())
223            .map_err(|e| anyhow!("decode research_morningstar_report: {e}"))?;
224    if resp.ret_type != 0 {
225        bail!(
226            "research_morningstar_report ret_type={} msg={:?}",
227            resp.ret_type,
228            resp.ret_msg
229        );
230    }
231    let s2c = resp.s2c.ok_or_else(|| anyhow!("missing s2c"))?;
232    let rows = [ResearchMorningstarReportRow {
233        symbol: symbol.to_string(),
234        rating_type: display_opt_i32(s2c.rating_type),
235        star_rating: display_opt_i32(s2c.star_rating),
236        fair_value: display_opt_f64(s2c.fair_value),
237        economic_moat: display_opt(&s2c.economic_moat_label),
238        uncertainty: display_opt(&s2c.uncertainty_label),
239        pdf_url: display_opt(&s2c.pdf_url),
240    }];
241    let json = serde_json::json!({
242        "symbol": symbol,
243        "s2c": s2c,
244    });
245    format.print_rows(&rows, &[json])?;
246    Ok(())
247}