futucli/cmd/analysis/company/
research.rs1use 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}