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
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}