Skip to main content

futucli/cmd/analysis/company/
financials.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, 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}