1use std::sync::Arc;
4
5use anyhow::{Result, anyhow, bail};
6use futu_net::client::FutuClient;
7use prost::Message;
8use serde::Serialize;
9
10use crate::state::parse_symbol;
11
12mod financials;
13pub use financials::*;
14
15#[derive(Serialize)]
16struct CompanyProfileOut {
17 symbol: String,
18 item_list: Vec<CompanyProfileItemOut>,
19}
20
21#[derive(Serialize)]
22struct CompanyProfileItemOut {
23 name: Option<String>,
24 value: Option<String>,
25 field_type: Option<i32>,
26}
27
28pub async fn get_company_profile(client: &Arc<FutuClient>, symbol: &str) -> Result<String> {
29 let sec = parse_symbol(symbol)?;
30 let req = futu_proto::qot_get_company_profile::Request {
31 c2s: futu_proto::qot_get_company_profile::C2s {
32 security: futu_proto::qot_common::Security {
33 market: sec.market as i32,
34 code: sec.code.clone(),
35 },
36 },
37 };
38 let body = req.encode_to_vec();
39 let frame = client
40 .request(futu_core::proto_id::QOT_GET_COMPANY_PROFILE, body)
41 .await?;
42 let resp = futu_proto::qot_get_company_profile::Response::decode(frame.body.as_ref())
43 .map_err(|e| anyhow!("decode company_profile: {e}"))?;
44 if resp.ret_type != 0 {
45 bail!(
46 "company_profile ret_type={} msg={:?}",
47 resp.ret_type,
48 resp.ret_msg
49 );
50 }
51 let s2c = resp.s2c.ok_or_else(|| anyhow!("missing s2c"))?;
52 let out = CompanyProfileOut {
53 symbol: symbol.to_string(),
54 item_list: s2c
55 .item_list
56 .iter()
57 .map(|item| CompanyProfileItemOut {
58 name: item.name.clone(),
59 value: item.value.clone(),
60 field_type: item.field_type,
61 })
62 .collect(),
63 };
64 Ok(serde_json::to_string_pretty(&out)?)
65}
66
67#[derive(Serialize)]
68struct CompanyExecutivesOut {
69 symbol: String,
70 director_list: Vec<DirectorOut>,
71}
72
73#[derive(Serialize)]
74struct DirectorOut {
75 display_leader_name: Option<String>,
76 leader_name: Option<String>,
77 position_name: Option<String>,
78 begin_date: Option<u64>,
79 begin_date_str: Option<String>,
80 leader_gender: Option<String>,
81 leader_age: Option<String>,
82 highest_education: Option<String>,
83 annual_salary: Option<u64>,
84 issue_date: Option<u64>,
85 issue_date_str: Option<String>,
86}
87
88pub async fn get_company_executives(client: &Arc<FutuClient>, symbol: &str) -> Result<String> {
89 let sec = parse_symbol(symbol)?;
90 let req = futu_proto::qot_get_company_executives::Request {
91 c2s: futu_proto::qot_get_company_executives::C2s {
92 security: futu_proto::qot_common::Security {
93 market: sec.market as i32,
94 code: sec.code.clone(),
95 },
96 },
97 };
98 let body = req.encode_to_vec();
99 let frame = client
100 .request(futu_core::proto_id::QOT_GET_COMPANY_EXECUTIVES, body)
101 .await?;
102 let resp = futu_proto::qot_get_company_executives::Response::decode(frame.body.as_ref())
103 .map_err(|e| anyhow!("decode company_executives: {e}"))?;
104 if resp.ret_type != 0 {
105 bail!(
106 "company_executives ret_type={} msg={:?}",
107 resp.ret_type,
108 resp.ret_msg
109 );
110 }
111 let s2c = resp.s2c.ok_or_else(|| anyhow!("missing s2c"))?;
112 let out = CompanyExecutivesOut {
113 symbol: symbol.to_string(),
114 director_list: s2c
115 .director_list
116 .iter()
117 .map(|item| DirectorOut {
118 display_leader_name: item.display_leader_name.clone(),
119 leader_name: item.leader_name.clone(),
120 position_name: item.position_name.clone(),
121 begin_date: item.begin_date,
122 begin_date_str: item.begin_date_str.clone(),
123 leader_gender: item.leader_gender.clone(),
124 leader_age: item.leader_age.clone(),
125 highest_education: item.highest_education.clone(),
126 annual_salary: item.annual_salary,
127 issue_date: item.issue_date,
128 issue_date_str: item.issue_date_str.clone(),
129 })
130 .collect(),
131 };
132 Ok(serde_json::to_string_pretty(&out)?)
133}
134
135#[derive(Serialize)]
136struct CompanyExecutiveBackgroundOut {
137 symbol: String,
138 leader_name: String,
139 brief_background: Option<String>,
140}
141
142pub async fn get_company_executive_background(
143 client: &Arc<FutuClient>,
144 symbol: &str,
145 leader_name: &str,
146) -> Result<String> {
147 let sec = parse_symbol(symbol)?;
148 let req = futu_proto::qot_get_company_executive_background::Request {
149 c2s: futu_proto::qot_get_company_executive_background::C2s {
150 security: futu_proto::qot_common::Security {
151 market: sec.market as i32,
152 code: sec.code.clone(),
153 },
154 leader_name: Some(leader_name.to_string()),
155 },
156 };
157 let body = req.encode_to_vec();
158 let frame = client
159 .request(
160 futu_core::proto_id::QOT_GET_COMPANY_EXECUTIVE_BACKGROUND,
161 body,
162 )
163 .await?;
164 let resp =
165 futu_proto::qot_get_company_executive_background::Response::decode(frame.body.as_ref())
166 .map_err(|e| anyhow!("decode company_executive_background: {e}"))?;
167 if resp.ret_type != 0 {
168 bail!(
169 "company_executive_background ret_type={} msg={:?}",
170 resp.ret_type,
171 resp.ret_msg
172 );
173 }
174 let s2c = resp.s2c.ok_or_else(|| anyhow!("missing s2c"))?;
175 let out = CompanyExecutiveBackgroundOut {
176 symbol: symbol.to_string(),
177 leader_name: leader_name.to_string(),
178 brief_background: s2c.brief_background,
179 };
180 Ok(serde_json::to_string_pretty(&out)?)
181}
182
183#[derive(Serialize)]
184struct CompanyOperationalEfficiencyOut {
185 symbol: String,
186 item_list: Vec<OperationalEfficiencyItemOut>,
187 next_key: Option<String>,
188 currency_code: Option<String>,
189}
190
191#[derive(Serialize)]
192struct OperationalEfficiencyItemOut {
193 fiscal_year: Option<i32>,
194 financial_type: Option<i32>,
195 period_text: Option<String>,
196 end_date: Option<i64>,
197 end_date_str: Option<String>,
198 employee_num: Option<i64>,
199 employee_num_yoy: Option<f64>,
200 income_per_capita: Option<f64>,
201 income_per_capita_yoy: Option<f64>,
202 profit_per_capita: Option<f64>,
203 profit_per_capita_yoy: Option<f64>,
204 net_profit_per_capita: Option<f64>,
205 net_profit_per_capita_yoy: Option<f64>,
206}
207
208pub async fn get_company_operational_efficiency(
209 client: &Arc<FutuClient>,
210 symbol: &str,
211 next_key: Option<&str>,
212 num: Option<i32>,
213 currency_code: Option<&str>,
214 financial_type: Option<i32>,
215) -> Result<String> {
216 let sec = parse_symbol(symbol)?;
217 let req = futu_proto::qot_get_company_operational_efficiency::Request {
218 c2s: futu_proto::qot_get_company_operational_efficiency::C2s {
219 security: futu_proto::qot_common::Security {
220 market: sec.market as i32,
221 code: sec.code.clone(),
222 },
223 next_key: next_key.map(str::to_string),
224 num,
225 currency_code: currency_code.map(str::to_string),
226 financial_type,
227 },
228 };
229 let body = req.encode_to_vec();
230 let frame = client
231 .request(
232 futu_core::proto_id::QOT_GET_COMPANY_OPERATIONAL_EFFICIENCY,
233 body,
234 )
235 .await?;
236 let resp =
237 futu_proto::qot_get_company_operational_efficiency::Response::decode(frame.body.as_ref())
238 .map_err(|e| anyhow!("decode company_operational_efficiency: {e}"))?;
239 if resp.ret_type != 0 {
240 bail!(
241 "company_operational_efficiency ret_type={} msg={:?}",
242 resp.ret_type,
243 resp.ret_msg
244 );
245 }
246 let s2c = resp.s2c.ok_or_else(|| anyhow!("missing s2c"))?;
247 let out = CompanyOperationalEfficiencyOut {
248 symbol: symbol.to_string(),
249 item_list: s2c
250 .item_list
251 .iter()
252 .map(|item| OperationalEfficiencyItemOut {
253 fiscal_year: item.fiscal_year,
254 financial_type: item.financial_type,
255 period_text: item.period_text.clone(),
256 end_date: item.end_date,
257 end_date_str: item.end_date_str.clone(),
258 employee_num: item.employee_num,
259 employee_num_yoy: item.employee_num_yoy,
260 income_per_capita: item.income_per_capita,
261 income_per_capita_yoy: item.income_per_capita_yoy,
262 profit_per_capita: item.profit_per_capita,
263 profit_per_capita_yoy: item.profit_per_capita_yoy,
264 net_profit_per_capita: item.net_profit_per_capita,
265 net_profit_per_capita_yoy: item.net_profit_per_capita_yoy,
266 })
267 .collect(),
268 next_key: s2c.next_key,
269 currency_code: s2c.currency_code,
270 };
271 Ok(serde_json::to_string_pretty(&out)?)
272}
273
274#[derive(Serialize)]
275struct ResearchAnalystConsensusOut {
276 symbol: String,
277 highest: Option<f64>,
278 average: Option<f64>,
279 lowest: Option<f64>,
280 rating: Option<i32>,
281 total: Option<i32>,
282 update_time: Option<i64>,
283 update_time_str: Option<String>,
284 buy: Option<f64>,
285 hold: Option<f64>,
286 sell: Option<f64>,
287 strong_buy: Option<f64>,
288 underperform: Option<f64>,
289}
290
291pub async fn get_research_analyst_consensus(
292 client: &Arc<FutuClient>,
293 symbol: &str,
294) -> Result<String> {
295 let sec = parse_symbol(symbol)?;
296 let req = futu_proto::qot_get_research_analyst_consensus::Request {
297 c2s: futu_proto::qot_get_research_analyst_consensus::C2s {
298 security: futu_proto::qot_common::Security {
299 market: sec.market as i32,
300 code: sec.code.clone(),
301 },
302 },
303 };
304 let body = req.encode_to_vec();
305 let frame = client
306 .request(
307 futu_core::proto_id::QOT_GET_RESEARCH_ANALYST_CONSENSUS,
308 body,
309 )
310 .await?;
311 let resp =
312 futu_proto::qot_get_research_analyst_consensus::Response::decode(frame.body.as_ref())
313 .map_err(|e| anyhow!("decode research_analyst_consensus: {e}"))?;
314 if resp.ret_type != 0 {
315 bail!(
316 "research_analyst_consensus ret_type={} msg={:?}",
317 resp.ret_type,
318 resp.ret_msg
319 );
320 }
321 let s2c = resp.s2c.ok_or_else(|| anyhow!("missing s2c"))?;
322 let out = ResearchAnalystConsensusOut {
323 symbol: symbol.to_string(),
324 highest: s2c.highest,
325 average: s2c.average,
326 lowest: s2c.lowest,
327 rating: s2c.rating,
328 total: s2c.total,
329 update_time: s2c.update_time,
330 update_time_str: s2c.update_time_str,
331 buy: s2c.buy,
332 hold: s2c.hold,
333 sell: s2c.sell,
334 strong_buy: s2c.strong_buy,
335 underperform: s2c.underperform,
336 };
337 Ok(serde_json::to_string_pretty(&out)?)
338}
339
340#[derive(Serialize)]
341struct ResearchRatingSummaryOut {
342 symbol: String,
343 s2c: futu_proto::qot_get_research_rating_summary::S2c,
344}
345
346pub async fn get_research_rating_summary(
347 client: &Arc<FutuClient>,
348 symbol: &str,
349 rating_dimension_type: Option<i32>,
350 uid: Option<&str>,
351 next_key: Option<&str>,
352 num: Option<i32>,
353) -> Result<String> {
354 let sec = parse_symbol(symbol)?;
355 let req = futu_proto::qot_get_research_rating_summary::Request {
356 c2s: futu_proto::qot_get_research_rating_summary::C2s {
357 security: futu_proto::qot_common::Security {
358 market: sec.market as i32,
359 code: sec.code.clone(),
360 },
361 rating_dimension_type,
362 uid: uid.map(str::to_string),
363 next_key: next_key.map(str::to_string),
364 num,
365 },
366 };
367 let body = req.encode_to_vec();
368 let frame = client
369 .request(futu_core::proto_id::QOT_GET_RESEARCH_RATING_SUMMARY, body)
370 .await?;
371 let resp = futu_proto::qot_get_research_rating_summary::Response::decode(frame.body.as_ref())
372 .map_err(|e| anyhow!("decode research_rating_summary: {e}"))?;
373 if resp.ret_type != 0 {
374 bail!(
375 "research_rating_summary ret_type={} msg={:?}",
376 resp.ret_type,
377 resp.ret_msg
378 );
379 }
380 let s2c = resp.s2c.ok_or_else(|| anyhow!("missing s2c"))?;
381 let out = ResearchRatingSummaryOut {
382 symbol: symbol.to_string(),
383 s2c,
384 };
385 Ok(serde_json::to_string_pretty(&out)?)
386}
387
388#[derive(Serialize)]
389struct ResearchMorningstarReportOut {
390 symbol: String,
391 s2c: futu_proto::qot_get_research_morningstar_report::S2c,
392}
393
394pub async fn get_research_morningstar_report(
395 client: &Arc<FutuClient>,
396 symbol: &str,
397) -> Result<String> {
398 let sec = parse_symbol(symbol)?;
399 let req = futu_proto::qot_get_research_morningstar_report::Request {
400 c2s: futu_proto::qot_get_research_morningstar_report::C2s {
401 security: futu_proto::qot_common::Security {
402 market: sec.market as i32,
403 code: sec.code.clone(),
404 },
405 },
406 };
407 let body = req.encode_to_vec();
408 let frame = client
409 .request(
410 futu_core::proto_id::QOT_GET_RESEARCH_MORNINGSTAR_REPORT,
411 body,
412 )
413 .await?;
414 let resp =
415 futu_proto::qot_get_research_morningstar_report::Response::decode(frame.body.as_ref())
416 .map_err(|e| anyhow!("decode research_morningstar_report: {e}"))?;
417 if resp.ret_type != 0 {
418 bail!(
419 "research_morningstar_report ret_type={} msg={:?}",
420 resp.ret_type,
421 resp.ret_msg
422 );
423 }
424 let s2c = resp.s2c.ok_or_else(|| anyhow!("missing s2c"))?;
425 let out = ResearchMorningstarReportOut {
426 symbol: symbol.to_string(),
427 s2c,
428 };
429 Ok(serde_json::to_string_pretty(&out)?)
430}
431
432#[derive(Serialize)]
433struct ValuationDetailOut {
434 symbol: String,
435 s2c: futu_proto::qot_get_valuation_detail::S2c,
436}
437
438pub async fn get_valuation_detail(
439 client: &Arc<FutuClient>,
440 symbol: &str,
441 valuation_type: Option<i32>,
442 interval_type: Option<i32>,
443) -> Result<String> {
444 let sec = parse_symbol(symbol)?;
445 let req = futu_proto::qot_get_valuation_detail::Request {
446 c2s: futu_proto::qot_get_valuation_detail::C2s {
447 security: futu_proto::qot_common::Security {
448 market: sec.market as i32,
449 code: sec.code.clone(),
450 },
451 valuation_type,
452 interval_type,
453 },
454 };
455 let body = req.encode_to_vec();
456 let frame = client
457 .request(futu_core::proto_id::QOT_GET_VALUATION_DETAIL, body)
458 .await?;
459 let resp = futu_proto::qot_get_valuation_detail::Response::decode(frame.body.as_ref())
460 .map_err(|e| anyhow!("decode valuation_detail: {e}"))?;
461 if resp.ret_type != 0 {
462 bail!(
463 "valuation_detail ret_type={} msg={:?}",
464 resp.ret_type,
465 resp.ret_msg
466 );
467 }
468 let s2c = resp.s2c.ok_or_else(|| anyhow!("missing s2c"))?;
469 let out = ValuationDetailOut {
470 symbol: symbol.to_string(),
471 s2c,
472 };
473 Ok(serde_json::to_string_pretty(&out)?)
474}
475
476#[derive(Serialize)]
477struct ValuationPlateStockListOut {
478 symbol: String,
479 s2c: futu_proto::qot_get_valuation_plate_stock_list::S2c,
480}
481
482pub async fn get_valuation_plate_stock_list(
483 client: &Arc<FutuClient>,
484 symbol: &str,
485 valuation_type: Option<i32>,
486 next_key: Option<&str>,
487 num: Option<i32>,
488 sort_type: Option<i32>,
489 sort_id: Option<i32>,
490 filter_security: Option<&str>,
491) -> Result<String> {
492 let sec = parse_symbol(symbol)?;
493 let filter_security = match filter_security {
494 Some(symbol) if !symbol.trim().is_empty() => {
495 let sec = parse_symbol(symbol)?;
496 Some(futu_proto::qot_common::Security {
497 market: sec.market as i32,
498 code: sec.code,
499 })
500 }
501 _ => None,
502 };
503 let req = futu_proto::qot_get_valuation_plate_stock_list::Request {
504 c2s: futu_proto::qot_get_valuation_plate_stock_list::C2s {
505 security: futu_proto::qot_common::Security {
506 market: sec.market as i32,
507 code: sec.code.clone(),
508 },
509 valuation_type,
510 next_key: next_key.map(str::to_string),
511 num,
512 sort_type,
513 sort_id,
514 filter_security,
515 },
516 };
517 let body = req.encode_to_vec();
518 let frame = client
519 .request(
520 futu_core::proto_id::QOT_GET_VALUATION_PLATE_STOCK_LIST,
521 body,
522 )
523 .await?;
524 let resp =
525 futu_proto::qot_get_valuation_plate_stock_list::Response::decode(frame.body.as_ref())
526 .map_err(|e| anyhow!("decode valuation_plate_stock_list: {e}"))?;
527 if resp.ret_type != 0 {
528 bail!(
529 "valuation_plate_stock_list ret_type={} msg={:?}",
530 resp.ret_type,
531 resp.ret_msg
532 );
533 }
534 let s2c = resp.s2c.ok_or_else(|| anyhow!("missing s2c"))?;
535 let out = ValuationPlateStockListOut {
536 symbol: symbol.to_string(),
537 s2c,
538 };
539 Ok(serde_json::to_string_pretty(&out)?)
540}