1use axum::extract::{Json, State};
4use futu_qot::ipo_calendar::{
5 collect_ipo_calendar_events, is_supported_ipo_event_type, is_yyyymmdd,
6};
7use serde::Deserialize;
8use serde_json::Value;
9
10use futu_core::proto_id;
11
12use super::*;
13
14mod company;
15mod unusual;
16pub use company::{
17 get_company_executive_background, get_company_executives, get_company_operational_efficiency,
18 get_company_profile, get_financials_earnings_price_history, get_financials_earnings_price_move,
19 get_financials_revenue_breakdown, get_financials_statements, get_research_analyst_consensus,
20 get_research_morningstar_report, get_research_rating_summary, get_valuation_detail,
21 get_valuation_plate_stock_list,
22};
23pub use unusual::{get_derivative_unusual, get_financial_unusual, get_technical_unusual};
24
25#[cfg(test)]
26mod tests;
27
28pub async fn get_plate_set(
29 State(state): State<RestState>,
30 Json(body): Json<Value>,
31) -> RawApiResult {
32 adapter::proto_request_raw::<qot_get_plate_set::Request, qot_get_plate_set::Response>(
33 &state,
34 proto_id::QOT_GET_PLATE_SET,
35 Some(body),
36 )
37 .await
38}
39
40pub async fn get_plate_security(
42 State(state): State<RestState>,
43 Json(body): Json<Value>,
44) -> RawApiResult {
45 adapter::proto_request_raw::<qot_get_plate_security::Request, qot_get_plate_security::Response>(
46 &state,
47 proto_id::QOT_GET_PLATE_SECURITY,
48 Some(body),
49 )
50 .await
51}
52
53pub async fn get_reference(
55 State(state): State<RestState>,
56 Json(body): Json<Value>,
57) -> RawApiResult {
58 adapter::proto_request_raw::<qot_get_reference::Request, qot_get_reference::Response>(
59 &state,
60 proto_id::QOT_GET_REFERENCE,
61 Some(body),
62 )
63 .await
64}
65
66pub async fn get_owner_plate(
68 State(state): State<RestState>,
69 Json(body): Json<Value>,
70) -> RawApiResult {
71 adapter::proto_request_raw::<qot_get_owner_plate::Request, qot_get_owner_plate::Response>(
72 &state,
73 proto_id::QOT_GET_OWNER_PLATE,
74 Some(body),
75 )
76 .await
77}
78
79pub async fn get_option_chain(
81 State(state): State<RestState>,
82 Json(body): Json<Value>,
83) -> RawApiResult {
84 adapter::proto_request_raw::<qot_get_option_chain::Request, qot_get_option_chain::Response>(
85 &state,
86 proto_id::QOT_GET_OPTION_CHAIN,
87 Some(body),
88 )
89 .await
90}
91
92pub async fn get_option_quote(
94 State(state): State<RestState>,
95 Json(body): Json<Value>,
96) -> RawApiResult {
97 adapter::proto_request_raw::<qot_get_option_quote::Request, qot_get_option_quote::Response>(
98 &state,
99 proto_id::QOT_GET_OPTION_QUOTE,
100 Some(body),
101 )
102 .await
103}
104
105pub async fn get_option_strategy(
107 State(state): State<RestState>,
108 Json(body): Json<Value>,
109) -> RawApiResult {
110 adapter::proto_request_raw::<
111 qot_get_option_strategy::Request,
112 qot_get_option_strategy::Response,
113 >(&state, proto_id::QOT_GET_OPTION_STRATEGY, Some(body))
114 .await
115}
116
117pub async fn get_option_strategy_analysis(
119 State(state): State<RestState>,
120 Json(body): Json<Value>,
121) -> RawApiResult {
122 adapter::proto_request_raw::<
123 qot_get_option_strategy_analysis::Request,
124 qot_get_option_strategy_analysis::Response,
125 >(
126 &state,
127 proto_id::QOT_GET_OPTION_STRATEGY_ANALYSIS,
128 Some(body),
129 )
130 .await
131}
132
133pub async fn get_option_strategy_spread(
135 State(state): State<RestState>,
136 Json(body): Json<Value>,
137) -> RawApiResult {
138 adapter::proto_request_raw::<
139 qot_get_option_strategy_spread::Request,
140 qot_get_option_strategy_spread::Response,
141 >(&state, proto_id::QOT_GET_OPTION_STRATEGY_SPREAD, Some(body))
142 .await
143}
144
145pub async fn get_warrant(State(state): State<RestState>, Json(body): Json<Value>) -> RawApiResult {
147 adapter::proto_request_raw::<qot_get_warrant::Request, qot_get_warrant::Response>(
148 &state,
149 proto_id::QOT_GET_WARRANT,
150 Some(body),
151 )
152 .await
153}
154
155pub async fn get_capital_flow(
157 State(state): State<RestState>,
158 Json(body): Json<Value>,
159) -> RawApiResult {
160 adapter::proto_request_raw::<qot_get_capital_flow::Request, qot_get_capital_flow::Response>(
161 &state,
162 proto_id::QOT_GET_CAPITAL_FLOW,
163 Some(body),
164 )
165 .await
166}
167
168pub async fn get_capital_distribution(
170 State(state): State<RestState>,
171 Json(body): Json<Value>,
172) -> RawApiResult {
173 adapter::proto_request_raw::<
174 qot_get_capital_distribution::Request,
175 qot_get_capital_distribution::Response,
176 >(&state, proto_id::QOT_GET_CAPITAL_DISTRIBUTION, Some(body))
177 .await
178}
179
180pub async fn get_corporate_actions_buybacks(
182 State(state): State<RestState>,
183 Json(body): Json<Value>,
184) -> RawApiResult {
185 adapter::proto_request_raw::<
186 qot_get_corporate_actions_buybacks::Request,
187 qot_get_corporate_actions_buybacks::Response,
188 >(
189 &state,
190 proto_id::QOT_GET_CORPORATE_ACTIONS_BUYBACKS,
191 Some(body),
192 )
193 .await
194}
195
196pub async fn get_corporate_actions_dividends(
198 State(state): State<RestState>,
199 Json(body): Json<Value>,
200) -> RawApiResult {
201 adapter::proto_request_raw::<
202 qot_get_corporate_actions_dividends::Request,
203 qot_get_corporate_actions_dividends::Response,
204 >(
205 &state,
206 proto_id::QOT_GET_CORPORATE_ACTIONS_DIVIDENDS,
207 Some(body),
208 )
209 .await
210}
211
212pub async fn get_corporate_actions_stock_splits(
214 State(state): State<RestState>,
215 Json(body): Json<Value>,
216) -> RawApiResult {
217 adapter::proto_request_raw::<
218 qot_get_corporate_actions_stock_splits::Request,
219 qot_get_corporate_actions_stock_splits::Response,
220 >(
221 &state,
222 proto_id::QOT_GET_CORPORATE_ACTIONS_STOCK_SPLITS,
223 Some(body),
224 )
225 .await
226}
227
228pub async fn get_daily_short_volume(
230 State(state): State<RestState>,
231 Json(body): Json<Value>,
232) -> RawApiResult {
233 adapter::proto_request_raw::<
234 qot_get_daily_short_volume::Request,
235 qot_get_daily_short_volume::Response,
236 >(&state, proto_id::QOT_GET_DAILY_SHORT_VOLUME, Some(body))
237 .await
238}
239
240pub async fn get_short_interest(
242 State(state): State<RestState>,
243 Json(body): Json<Value>,
244) -> RawApiResult {
245 adapter::proto_request_raw::<qot_get_short_interest::Request, qot_get_short_interest::Response>(
246 &state,
247 proto_id::QOT_GET_SHORT_INTEREST,
248 Some(body),
249 )
250 .await
251}
252
253pub async fn get_top_ten_buy_sell_brokers(
255 State(state): State<RestState>,
256 Json(body): Json<Value>,
257) -> RawApiResult {
258 adapter::proto_request_raw::<
259 qot_get_top_ten_buy_sell_brokers::Request,
260 qot_get_top_ten_buy_sell_brokers::Response,
261 >(
262 &state,
263 proto_id::QOT_GET_TOP_TEN_BUY_SELL_BROKERS,
264 Some(body),
265 )
266 .await
267}
268
269pub async fn get_shareholders_overview(
271 State(state): State<RestState>,
272 Json(body): Json<Value>,
273) -> RawApiResult {
274 adapter::proto_request_raw::<
275 qot_get_shareholders_overview::Request,
276 qot_get_shareholders_overview::Response,
277 >(&state, proto_id::QOT_GET_SHAREHOLDERS_OVERVIEW, Some(body))
278 .await
279}
280
281pub async fn get_shareholders_holding_changes(
283 State(state): State<RestState>,
284 Json(body): Json<Value>,
285) -> RawApiResult {
286 adapter::proto_request_raw::<
287 qot_get_shareholders_holding_changes::Request,
288 qot_get_shareholders_holding_changes::Response,
289 >(
290 &state,
291 proto_id::QOT_GET_SHAREHOLDERS_HOLDING_CHANGES,
292 Some(body),
293 )
294 .await
295}
296
297pub async fn get_shareholders_holder_detail(
299 State(state): State<RestState>,
300 Json(body): Json<Value>,
301) -> RawApiResult {
302 adapter::proto_request_raw::<
303 qot_get_shareholders_holder_detail::Request,
304 qot_get_shareholders_holder_detail::Response,
305 >(
306 &state,
307 proto_id::QOT_GET_SHAREHOLDERS_HOLDER_DETAIL,
308 Some(body),
309 )
310 .await
311}
312
313pub async fn get_shareholders_institutional(
315 State(state): State<RestState>,
316 Json(body): Json<Value>,
317) -> RawApiResult {
318 adapter::proto_request_raw::<
319 qot_get_shareholders_institutional::Request,
320 qot_get_shareholders_institutional::Response,
321 >(
322 &state,
323 proto_id::QOT_GET_SHAREHOLDERS_INSTITUTIONAL,
324 Some(body),
325 )
326 .await
327}
328
329pub async fn get_insider_holder_list(
331 State(state): State<RestState>,
332 Json(body): Json<Value>,
333) -> RawApiResult {
334 adapter::proto_request_raw::<
335 qot_get_insider_holder_list::Request,
336 qot_get_insider_holder_list::Response,
337 >(&state, proto_id::QOT_GET_INSIDER_HOLDER_LIST, Some(body))
338 .await
339}
340
341pub async fn get_insider_trade_list(
343 State(state): State<RestState>,
344 Json(body): Json<Value>,
345) -> RawApiResult {
346 adapter::proto_request_raw::<
347 qot_get_insider_trade_list::Request,
348 qot_get_insider_trade_list::Response,
349 >(&state, proto_id::QOT_GET_INSIDER_TRADE_LIST, Some(body))
350 .await
351}
352
353pub async fn get_option_volatility(
355 State(state): State<RestState>,
356 Json(body): Json<Value>,
357) -> RawApiResult {
358 adapter::proto_request_raw::<
359 qot_get_option_volatility::Request,
360 qot_get_option_volatility::Response,
361 >(&state, proto_id::QOT_GET_OPTION_VOLATILITY, Some(body))
362 .await
363}
364
365pub async fn get_option_exercise_probability(
367 State(state): State<RestState>,
368 Json(body): Json<Value>,
369) -> RawApiResult {
370 adapter::proto_request_raw::<
371 qot_get_option_exercise_probability::Request,
372 qot_get_option_exercise_probability::Response,
373 >(
374 &state,
375 proto_id::QOT_GET_OPTION_EXERCISE_PROBABILITY,
376 Some(body),
377 )
378 .await
379}
380
381pub async fn stock_screen(State(state): State<RestState>, Json(body): Json<Value>) -> RawApiResult {
383 adapter::proto_request_raw::<qot_stock_screen::Request, qot_stock_screen::Response>(
384 &state,
385 proto_id::QOT_STOCK_SCREEN,
386 Some(body),
387 )
388 .await
389}
390
391pub async fn option_screen(
393 State(state): State<RestState>,
394 Json(body): Json<Value>,
395) -> RawApiResult {
396 adapter::proto_request_raw::<qot_option_screen::Request, qot_option_screen::Response>(
397 &state,
398 proto_id::QOT_OPTION_SCREEN,
399 Some(body),
400 )
401 .await
402}
403
404pub async fn warrant_screen(
406 State(state): State<RestState>,
407 Json(body): Json<Value>,
408) -> RawApiResult {
409 adapter::proto_request_raw::<qot_warrant_screen::Request, qot_warrant_screen::Response>(
410 &state,
411 proto_id::QOT_WARRANT_SCREEN,
412 Some(body),
413 )
414 .await
415}
416
417pub async fn get_user_security(
419 State(state): State<RestState>,
420 Json(body): Json<Value>,
421) -> RawApiResult {
422 adapter::proto_request_raw::<qot_get_user_security::Request, qot_get_user_security::Response>(
423 &state,
424 proto_id::QOT_GET_USER_SECURITY,
425 Some(body),
426 )
427 .await
428}
429
430pub async fn get_user_security_group(
432 State(state): State<RestState>,
433 Json(body): Json<Value>,
434) -> RawApiResult {
435 adapter::proto_request_raw::<
436 qot_get_user_security_group::Request,
437 qot_get_user_security_group::Response,
438 >(&state, proto_id::QOT_GET_USER_SECURITY_GROUP, Some(body))
439 .await
440}
441
442pub async fn stock_filter(State(state): State<RestState>, Json(body): Json<Value>) -> RawApiResult {
444 adapter::proto_request_raw::<qot_stock_filter::Request, qot_stock_filter::Response>(
445 &state,
446 proto_id::QOT_STOCK_FILTER,
447 Some(body),
448 )
449 .await
450}
451
452pub async fn get_ipo_list(State(state): State<RestState>, Json(body): Json<Value>) -> RawApiResult {
454 adapter::proto_request_raw::<qot_get_ipo_list::Request, qot_get_ipo_list::Response>(
455 &state,
456 proto_id::QOT_GET_IPO_LIST,
457 Some(body),
458 )
459 .await
460}
461
462#[derive(Debug, Deserialize)]
463#[serde(deny_unknown_fields)]
464pub struct IpoCalendarRestReq {
465 market: i32,
466 #[serde(default, alias = "eventTypes")]
467 event_types: Vec<String>,
468 #[serde(default, alias = "beginDate")]
469 begin_date: Option<String>,
470 #[serde(default, alias = "endDate")]
471 end_date: Option<String>,
472}
473
474fn validate_ipo_calendar_req(req: &IpoCalendarRestReq) -> Result<(), (StatusCode, Json<Value>)> {
475 if !matches!(req.market, 1 | 2 | 11 | 21 | 22 | 31 | 41 | 61) {
476 return Err((
477 StatusCode::BAD_REQUEST,
478 Json(serde_json::json!({
479 "error": "market must be 1, 2, 11, 21, 22, 31, 41, or 61 (HK/HK_Future/US/SH/SZ/SG/JP/MY)"
480 })),
481 ));
482 }
483 for (field, value) in [
484 ("begin_date", req.begin_date.as_deref()),
485 ("end_date", req.end_date.as_deref()),
486 ] {
487 if let Some(value) = value
488 && !is_yyyymmdd(value)
489 {
490 return Err((
491 StatusCode::BAD_REQUEST,
492 Json(serde_json::json!({
493 "error": format!("{field} must be YYYYMMDD")
494 })),
495 ));
496 }
497 }
498 if let (Some(begin), Some(end)) = (&req.begin_date, &req.end_date)
499 && begin > end
500 {
501 return Err((
502 StatusCode::BAD_REQUEST,
503 Json(serde_json::json!({
504 "error": "begin_date must be earlier than or equal to end_date"
505 })),
506 ));
507 }
508 if let Some(event) = req
509 .event_types
510 .iter()
511 .find(|event| !is_supported_ipo_event_type(event))
512 {
513 return Err((
514 StatusCode::BAD_REQUEST,
515 Json(serde_json::json!({
516 "error": format!("unsupported IPO calendar event_type {event:?}")
517 })),
518 ));
519 }
520 Ok(())
521}
522
523pub async fn get_ipo_calendar(
525 State(state): State<RestState>,
526 Json(req): Json<IpoCalendarRestReq>,
527) -> RawApiResult {
528 validate_ipo_calendar_req(&req)?;
529 let req_msg = futu_proto::qot_get_ipo_list::Request {
530 c2s: futu_proto::qot_get_ipo_list::C2s {
531 market: req.market,
532 header: None,
533 },
534 };
535 let conn_id = state.next_conn_id();
536 let serial_no = state.next_serial();
537 let incoming = IncomingRequest::builder(
538 conn_id,
539 proto_id::QOT_GET_IPO_LIST,
540 serial_no,
541 ProtoFmtType::Protobuf,
542 Bytes::from(req_msg.encode_to_vec()),
543 )
544 .build();
545 let resp_bytes = state
546 .router
547 .dispatch(incoming.conn_id, &incoming)
548 .await
549 .ok_or_else(|| {
550 (
551 StatusCode::INTERNAL_SERVER_ERROR,
552 Json(serde_json::json!({"error": "handler returned no response"})),
553 )
554 })?;
555 let resp =
556 futu_proto::qot_get_ipo_list::Response::decode(resp_bytes.as_slice()).map_err(|e| {
557 (
558 StatusCode::INTERNAL_SERVER_ERROR,
559 Json(serde_json::json!({"error": format!("failed to decode response: {e}")})),
560 )
561 })?;
562 if resp.ret_type != 0 {
563 let raw = serde_json::to_vec(&serde_json::json!({
564 "ret_type": resp.ret_type,
565 "ret_msg": resp.ret_msg,
566 "err_code": resp.err_code,
567 "events": []
568 }))
569 .map_err(|e| {
570 (
571 StatusCode::INTERNAL_SERVER_ERROR,
572 Json(serde_json::json!({"error": format!("failed to serialize response: {e}")})),
573 )
574 })?;
575 return Ok(adapter::RawJson::new(raw));
576 }
577 let s2c = resp.s2c.as_ref().ok_or_else(|| {
578 (
579 StatusCode::INTERNAL_SERVER_ERROR,
580 Json(serde_json::json!({"error": "missing s2c"})),
581 )
582 })?;
583 let events = collect_ipo_calendar_events(
584 s2c,
585 &req.event_types,
586 req.begin_date.as_deref(),
587 req.end_date.as_deref(),
588 );
589 let raw = serde_json::to_vec(&serde_json::json!({
590 "market": req.market,
591 "event_types": req.event_types,
592 "begin_date": req.begin_date,
593 "end_date": req.end_date,
594 "events": events
595 }))
596 .map_err(|e| {
597 (
598 StatusCode::INTERNAL_SERVER_ERROR,
599 Json(serde_json::json!({"error": format!("failed to serialize response: {e}")})),
600 )
601 })?;
602 Ok(adapter::RawJson::new(raw))
603}
604
605pub async fn get_future_info(
607 State(state): State<RestState>,
608 Json(body): Json<Value>,
609) -> RawApiResult {
610 adapter::proto_request_raw::<qot_get_future_info::Request, qot_get_future_info::Response>(
611 &state,
612 proto_id::QOT_GET_FUTURE_INFO,
613 Some(body),
614 )
615 .await
616}
617
618pub async fn get_market_state(
620 State(state): State<RestState>,
621 Json(body): Json<Value>,
622) -> RawApiResult {
623 adapter::proto_request_raw::<qot_get_market_state::Request, qot_get_market_state::Response>(
624 &state,
625 proto_id::QOT_GET_MARKET_STATE,
626 Some(body),
627 )
628 .await
629}
630
631pub async fn request_history_kl(
633 State(state): State<RestState>,
634 Json(body): Json<Value>,
635) -> RawApiResult {
636 adapter::proto_request_raw::<qot_request_history_kl::Request, qot_request_history_kl::Response>(
637 &state,
638 proto_id::QOT_REQUEST_HISTORY_KL,
639 Some(body),
640 )
641 .await
642}
643
644pub async fn request_trading_days(
648 State(state): State<RestState>,
649 Json(body): Json<Value>,
650) -> RawApiResult {
651 adapter::proto_request_raw::<qot_request_trade_date::Request, qot_request_trade_date::Response>(
652 &state,
653 proto_id::QOT_REQUEST_TRADE_DATE,
654 Some(body),
655 )
656 .await
657}
658
659pub async fn request_rehab(
661 State(state): State<RestState>,
662 Json(body): Json<Value>,
663) -> RawApiResult {
664 adapter::proto_request_raw::<qot_request_rehab::Request, qot_request_rehab::Response>(
665 &state,
666 proto_id::QOT_REQUEST_REHAB,
667 Some(body),
668 )
669 .await
670}
671
672pub async fn get_suspend(State(state): State<RestState>, Json(body): Json<Value>) -> RawApiResult {
674 adapter::proto_request_raw::<qot_get_suspend::Request, qot_get_suspend::Response>(
675 &state,
676 proto_id::QOT_GET_SUSPEND,
677 Some(body),
678 )
679 .await
680}
681
682pub async fn request_history_kl_quota(
686 State(state): State<RestState>,
687 Json(body): Json<Value>,
688) -> RawApiResult {
689 adapter::proto_request_raw::<
690 qot_request_history_kl_quota::Request,
691 qot_request_history_kl_quota::Response,
692 >(&state, proto_id::QOT_REQUEST_HISTORY_KL_QUOTA, Some(body))
693 .await
694}
695
696pub async fn get_used_quota(
698 State(state): State<RestState>,
699 Json(body): Json<Value>,
700) -> RawApiResult {
701 adapter::proto_request_raw::<used_quota::Request, used_quota::Response>(
702 &state,
703 proto_id::GET_USED_QUOTA,
704 Some(body),
705 )
706 .await
707}
708
709pub async fn get_holding_change(
711 State(state): State<RestState>,
712 Json(body): Json<Value>,
713) -> RawApiResult {
714 adapter::proto_request_raw::<
715 qot_get_holding_change_list::Request,
716 qot_get_holding_change_list::Response,
717 >(&state, proto_id::QOT_GET_HOLDING_CHANGE_LIST, Some(body))
718 .await
719}
720
721pub async fn modify_user_security(
723 State(state): State<RestState>,
724 Json(body): Json<Value>,
725) -> RawApiResult {
726 adapter::proto_request_raw::<
727 qot_modify_user_security::Request,
728 qot_modify_user_security::Response,
729 >(&state, proto_id::QOT_MODIFY_USER_SECURITY, Some(body))
730 .await
731}
732
733pub async fn get_code_change(
735 State(state): State<RestState>,
736 Json(body): Json<Value>,
737) -> RawApiResult {
738 adapter::proto_request_raw::<qot_get_code_change::Request, qot_get_code_change::Response>(
739 &state,
740 proto_id::QOT_GET_CODE_CHANGE,
741 Some(body),
742 )
743 .await
744}
745
746pub async fn set_price_reminder(
748 State(state): State<RestState>,
749 Json(body): Json<Value>,
750) -> RawApiResult {
751 adapter::proto_request_raw::<qot_set_price_reminder::Request, qot_set_price_reminder::Response>(
752 &state,
753 proto_id::QOT_SET_PRICE_REMINDER,
754 Some(body),
755 )
756 .await
757}
758
759pub async fn get_price_reminder(
761 State(state): State<RestState>,
762 Json(body): Json<Value>,
763) -> RawApiResult {
764 adapter::proto_request_raw::<qot_get_price_reminder::Request, qot_get_price_reminder::Response>(
765 &state,
766 proto_id::QOT_GET_PRICE_REMINDER,
767 Some(body),
768 )
769 .await
770}
771
772pub async fn get_option_expiration_date(
774 State(state): State<RestState>,
775 Json(body): Json<Value>,
776) -> RawApiResult {
777 adapter::proto_request_raw::<
778 qot_get_option_expiration_date::Request,
779 qot_get_option_expiration_date::Response,
780 >(&state, proto_id::QOT_GET_OPTION_EXPIRATION_DATE, Some(body))
781 .await
782}