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, display_opt_i64};
9
10#[derive(Tabled)]
11struct FinancialsEarningsPriceMoveRow {
12 #[tabled(rename = "Fiscal Year")]
13 fiscal_year: String,
14 #[tabled(rename = "Period")]
15 period: String,
16 #[tabled(rename = "Pub Day")]
17 pub_day: String,
18 #[tabled(rename = "Trading Day")]
19 trading_day: String,
20 #[tabled(rename = "Close")]
21 close_price: String,
22 #[tabled(rename = "Open")]
23 open_price: String,
24 #[tabled(rename = "High")]
25 highest_price: String,
26 #[tabled(rename = "Low")]
27 lowest_price: String,
28 #[tabled(rename = "Last Close")]
29 last_close_price: String,
30 #[tabled(rename = "Option IV")]
31 option_iv: String,
32 #[tabled(rename = "Option HV")]
33 option_hv: String,
34}
35
36pub async fn run_financials_earnings_price_move(
37 gateway: &str,
38 symbol: &str,
39 period_count: Option<i32>,
40 format: OutputFormat,
41) -> Result<()> {
42 let sec = parse_symbol(symbol)?;
43 let (client, _rx) = connect_gateway(gateway, "futucli-financials-earnings-price-move").await?;
44
45 let req = futu_proto::qot_get_financials_earnings_price_move::Request {
46 c2s: futu_proto::qot_get_financials_earnings_price_move::C2s {
47 security: futu_proto::qot_common::Security {
48 market: sec.market as i32,
49 code: sec.code.clone(),
50 },
51 period_count,
52 },
53 };
54 let body = req.encode_to_vec();
55 let frame = client
56 .request(
57 futu_core::proto_id::QOT_GET_FINANCIALS_EARNINGS_PRICE_MOVE,
58 body,
59 )
60 .await?;
61 let resp =
62 futu_proto::qot_get_financials_earnings_price_move::Response::decode(frame.body.as_ref())
63 .map_err(|e| anyhow!("decode financials_earnings_price_move: {e}"))?;
64 if resp.ret_type != 0 {
65 bail!(
66 "financials_earnings_price_move ret_type={} msg={:?}",
67 resp.ret_type,
68 resp.ret_msg
69 );
70 }
71 let s2c = resp.s2c.ok_or_else(|| anyhow!("missing s2c"))?;
72
73 let rows: Vec<FinancialsEarningsPriceMoveRow> = s2c
74 .detail_list
75 .iter()
76 .flat_map(|cycle| {
77 cycle
78 .item_list
79 .iter()
80 .map(move |row| FinancialsEarningsPriceMoveRow {
81 fiscal_year: display_opt_i32(cycle.fiscal_year),
82 period: display_opt(&cycle.period_text),
83 pub_day: display_opt(&cycle.pub_trading_day_str),
84 trading_day: display_opt(&row.trading_day_str),
85 close_price: display_opt_f64(row.close_price),
86 open_price: display_opt_f64(row.open_price),
87 highest_price: display_opt_f64(row.highest_price),
88 lowest_price: display_opt_f64(row.lowest_price),
89 last_close_price: display_opt_f64(row.last_close_price),
90 option_iv: display_opt_f64(row.option_iv),
91 option_hv: display_opt_f64(row.option_hv),
92 })
93 })
94 .collect();
95 let json = serde_json::json!({
96 "symbol": symbol,
97 "detail_list": s2c.detail_list,
98 });
99 format.print_rows(&rows, &[json])?;
100 Ok(())
101}
102
103#[derive(Tabled)]
104struct FinancialsEarningsPriceHistoryRow {
105 #[tabled(rename = "Fiscal Year")]
106 fiscal_year: String,
107 #[tabled(rename = "Period")]
108 period: String,
109 #[tabled(rename = "Current")]
110 is_current: String,
111 #[tabled(rename = "Pub Day")]
112 pub_day: String,
113 #[tabled(rename = "Pub Time")]
114 pub_time: String,
115 #[tabled(rename = "Close")]
116 close_price: String,
117 #[tabled(rename = "Volume")]
118 volume: String,
119 #[tabled(rename = "Vola Ratio")]
120 predict_vola_ratio: String,
121 #[tabled(rename = "IV Crush")]
122 option_iv_crush: String,
123}
124
125pub async fn run_financials_earnings_price_history(
126 gateway: &str,
127 symbol: &str,
128 format: OutputFormat,
129) -> Result<()> {
130 let sec = parse_symbol(symbol)?;
131 let (client, _rx) =
132 connect_gateway(gateway, "futucli-financials-earnings-price-history").await?;
133
134 let req = futu_proto::qot_get_financials_earnings_price_history::Request {
135 c2s: futu_proto::qot_get_financials_earnings_price_history::C2s {
136 security: futu_proto::qot_common::Security {
137 market: sec.market as i32,
138 code: sec.code.clone(),
139 },
140 },
141 };
142 let body = req.encode_to_vec();
143 let frame = client
144 .request(
145 futu_core::proto_id::QOT_GET_FINANCIALS_EARNINGS_PRICE_HISTORY,
146 body,
147 )
148 .await?;
149 let resp = futu_proto::qot_get_financials_earnings_price_history::Response::decode(
150 frame.body.as_ref(),
151 )
152 .map_err(|e| anyhow!("decode financials_earnings_price_history: {e}"))?;
153 if resp.ret_type != 0 {
154 bail!(
155 "financials_earnings_price_history ret_type={} msg={:?}",
156 resp.ret_type,
157 resp.ret_msg
158 );
159 }
160 let s2c = resp.s2c.ok_or_else(|| anyhow!("missing s2c"))?;
161
162 let rows: Vec<FinancialsEarningsPriceHistoryRow> = s2c
163 .detail_list
164 .iter()
165 .map(|detail| FinancialsEarningsPriceHistoryRow {
166 fiscal_year: display_opt_i32(detail.fiscal_year),
167 period: display_opt(&detail.period_text),
168 is_current: detail.is_current.map(|v| v.to_string()).unwrap_or_default(),
169 pub_day: display_opt(&detail.pub_trading_day_str),
170 pub_time: display_opt(&detail.pub_time_str),
171 close_price: detail
172 .price_info
173 .as_ref()
174 .map(|price| display_opt_f64(price.close_price))
175 .unwrap_or_default(),
176 volume: detail
177 .price_info
178 .as_ref()
179 .map(|price| display_opt_f64(price.volume))
180 .unwrap_or_default(),
181 predict_vola_ratio: display_opt_f64(detail.predict_vola_ratio_newest),
182 option_iv_crush: display_opt_f64(detail.option_iv_crush),
183 })
184 .collect();
185 let json = serde_json::json!({
186 "symbol": symbol,
187 "detail_list": s2c.detail_list,
188 });
189 format.print_rows(&rows, &[json])?;
190 Ok(())
191}
192
193#[derive(Tabled)]
194struct FinancialsStatementsRow {
195 #[tabled(rename = "Fiscal Year")]
196 fiscal_year: String,
197 #[tabled(rename = "Financial Type")]
198 financial_type: String,
199 #[tabled(rename = "Period")]
200 period: String,
201 #[tabled(rename = "Date")]
202 date: String,
203 #[tabled(rename = "Field ID")]
204 field_id: String,
205 #[tabled(rename = "Data")]
206 data: String,
207 #[tabled(rename = "YoY")]
208 yoy: String,
209 #[tabled(rename = "QoQ")]
210 qoq: String,
211 #[tabled(rename = "Currency")]
212 currency: String,
213}
214
215pub async fn run_financials_statements(
216 gateway: &str,
217 symbol: &str,
218 statement_type: Option<i32>,
219 financial_type: Option<i32>,
220 currency_code: Option<&str>,
221 next_key: Option<&str>,
222 num: Option<i32>,
223 format: OutputFormat,
224) -> Result<()> {
225 let sec = parse_symbol(symbol)?;
226 let (client, _rx) = connect_gateway(gateway, "futucli-financials-statements").await?;
227
228 let req = futu_proto::qot_get_financials_statements::Request {
229 c2s: futu_proto::qot_get_financials_statements::C2s {
230 security: futu_proto::qot_common::Security {
231 market: sec.market as i32,
232 code: sec.code.clone(),
233 },
234 statement_type,
235 financial_type,
236 currency_code: currency_code.map(str::to_string),
237 next_key: next_key.map(str::to_string),
238 num,
239 },
240 };
241 let body = req.encode_to_vec();
242 let frame = client
243 .request(futu_core::proto_id::QOT_GET_FINANCIALS_STATEMENTS, body)
244 .await?;
245 let resp = futu_proto::qot_get_financials_statements::Response::decode(frame.body.as_ref())
246 .map_err(|e| anyhow!("decode financials_statements: {e}"))?;
247 if resp.ret_type != 0 {
248 bail!(
249 "financials_statements ret_type={} msg={:?}",
250 resp.ret_type,
251 resp.ret_msg
252 );
253 }
254 let s2c = resp.s2c.ok_or_else(|| anyhow!("missing s2c"))?;
255
256 let rows: Vec<FinancialsStatementsRow> = s2c
257 .report_list
258 .iter()
259 .flat_map(|report| {
260 report
261 .item_list
262 .iter()
263 .map(move |item| FinancialsStatementsRow {
264 fiscal_year: display_opt_i32(report.fiscal_year),
265 financial_type: display_opt_i32(report.financial_type),
266 period: display_opt(&report.period_text),
267 date: display_opt(&report.date_time_str),
268 field_id: display_opt_i64(item.field_id),
269 data: display_opt_f64(item.data),
270 yoy: display_opt_f64(item.yoy),
271 qoq: display_opt_f64(item.qoq),
272 currency: display_opt(&report.currency_code),
273 })
274 })
275 .collect();
276 let json = serde_json::json!({
277 "symbol": symbol,
278 "structure_list": s2c.structure_list,
279 "report_list": s2c.report_list,
280 "next_key": s2c.next_key,
281 });
282 format.print_rows(&rows, &[json])?;
283 Ok(())
284}
285
286#[derive(Tabled)]
287struct FinancialsRevenueBreakdownRow {
288 #[tabled(rename = "Group Type")]
289 group_type: String,
290 #[tabled(rename = "Name")]
291 name: String,
292 #[tabled(rename = "Revenue")]
293 revenue: String,
294 #[tabled(rename = "Ratio")]
295 ratio: String,
296 #[tabled(rename = "Period")]
297 period: String,
298 #[tabled(rename = "Currency")]
299 currency: String,
300}
301
302pub async fn run_financials_revenue_breakdown(
303 gateway: &str,
304 symbol: &str,
305 date: Option<u32>,
306 financial_type: Option<i32>,
307 currency_code: Option<&str>,
308 format: OutputFormat,
309) -> Result<()> {
310 let sec = parse_symbol(symbol)?;
311 let (client, _rx) = connect_gateway(gateway, "futucli-financials-revenue-breakdown").await?;
312
313 let req = futu_proto::qot_get_financials_revenue_breakdown::Request {
314 c2s: futu_proto::qot_get_financials_revenue_breakdown::C2s {
315 security: futu_proto::qot_common::Security {
316 market: sec.market as i32,
317 code: sec.code.clone(),
318 },
319 date,
320 financial_type,
321 currency_code: currency_code.map(str::to_string),
322 },
323 };
324 let body = req.encode_to_vec();
325 let frame = client
326 .request(
327 futu_core::proto_id::QOT_GET_FINANCIALS_REVENUE_BREAKDOWN,
328 body,
329 )
330 .await?;
331 let resp =
332 futu_proto::qot_get_financials_revenue_breakdown::Response::decode(frame.body.as_ref())
333 .map_err(|e| anyhow!("decode financials_revenue_breakdown: {e}"))?;
334 if resp.ret_type != 0 {
335 bail!(
336 "financials_revenue_breakdown ret_type={} msg={:?}",
337 resp.ret_type,
338 resp.ret_msg
339 );
340 }
341 let s2c = resp.s2c.ok_or_else(|| anyhow!("missing s2c"))?;
342
343 let rows: Vec<FinancialsRevenueBreakdownRow> = s2c
344 .breakdown_list
345 .iter()
346 .flat_map(|group| {
347 group
348 .item_list
349 .iter()
350 .map(|item| FinancialsRevenueBreakdownRow {
351 group_type: display_opt_i32(group.r#type),
352 name: display_opt(&item.name),
353 revenue: display_opt_f64(item.main_oper_income),
354 ratio: display_opt_f64(item.ratio),
355 period: display_opt(&s2c.period),
356 currency: display_opt(&s2c.currency_code),
357 })
358 })
359 .collect();
360 let json = serde_json::json!({
361 "symbol": symbol,
362 "period": s2c.period,
363 "breakdown_list": s2c.breakdown_list,
364 "currency_code": s2c.currency_code,
365 "screen_date_list": s2c.screen_date_list,
366 });
367 format.print_rows(&rows, &[json])?;
368 Ok(())
369}