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