1use std::collections::hash_map::DefaultHasher;
4use std::hash::{Hash, Hasher};
5use std::sync::Arc;
6use std::sync::atomic::{AtomicU32, Ordering};
7
8use anyhow::{Result, anyhow, bail};
9use futu_core::trade_market;
10use futu_net::client::FutuClient;
11use prost::Message;
12use serde::Serialize;
13use serde::de::DeserializeOwned;
14
15use crate::tool_enums::{ToolEnum, TrdMarketEnum};
16
17mod kline_pattern;
18pub use kline_pattern::{
19 kline_pattern, kline_pattern_catalog, kline_pattern_performance, kline_pattern_statistics,
20 kline_pattern_stocks,
21};
22
23static PACKET_SERIAL: AtomicU32 = AtomicU32::new(1);
24
25#[derive(Debug, Clone)]
26pub struct ComboTradeContext {
27 pub env: &'static str,
28 pub acc_id: u64,
29 pub market: String,
30 pub order_value: Option<f64>,
31}
32
33pub fn parse_c2s_json<T>(label: &str, json: &str) -> Result<T>
34where
35 T: DeserializeOwned,
36{
37 let mut value: serde_json::Value =
38 serde_json::from_str(json).map_err(|err| anyhow!("{label} c2s_json: {err}"))?;
39 validate_proto_json_contract(label, &mut value)?;
40 serde_json::from_value(value).map_err(|err| anyhow!("{label} c2s_json: {err}"))
41}
42
43fn validate_proto_json_contract(label: &str, value: &mut serde_json::Value) -> Result<()> {
44 let Some(spec) = futu_surface_spec::lookup_endpoint_by_cli_subcommand(label) else {
45 return Ok(());
46 };
47 futu_surface_spec::validate_and_normalize(spec, value)
48 .map_err(|err| anyhow!("{label} c2s_json: {err}"))
49}
50
51pub fn parse_combo_max_c2s_json(json: &str) -> Result<futu_proto::trd_get_combo_max_trd_qtys::C2s> {
52 parse_combo_c2s_json("combo-max-trd-qtys", json)
53}
54
55pub fn parse_place_combo_c2s_json(json: &str) -> Result<futu_proto::trd_place_combo_order::C2s> {
56 parse_combo_c2s_json("combo-order", json)
57}
58
59fn parse_combo_c2s_json<T>(label: &str, json: &str) -> Result<T>
60where
61 T: DeserializeOwned,
62{
63 parse_c2s_json_with_required_paths(
64 label,
65 json,
66 &[
67 (["header", "trd_env"].as_slice(), "header.trd_env"),
68 (["header", "acc_id"].as_slice(), "header.acc_id"),
69 (["header", "trd_market"].as_slice(), "header.trd_market"),
70 (["combo_legs"].as_slice(), "combo_legs"),
71 (["qty"].as_slice(), "qty"),
72 (["order_type"].as_slice(), "order_type"),
73 ],
74 Some(validate_combo_trade_contract),
75 )
76}
77
78fn parse_c2s_json_with_required_paths<T>(
79 label: &str,
80 json: &str,
81 required_paths: &[(&[&str], &'static str)],
82 extra_validator: Option<fn(&str, &serde_json::Value) -> Result<()>>,
83) -> Result<T>
84where
85 T: DeserializeOwned,
86{
87 let value: serde_json::Value =
88 serde_json::from_str(json).map_err(|err| anyhow!("{label} c2s_json: {err}"))?;
89 for (path, name) in required_paths {
90 if json_path(&value, path).is_none() {
91 bail!("{label} c2s_json missing required field {name}");
92 }
93 }
94 if let Some(validate) = extra_validator {
95 validate(label, &value)?;
96 }
97 serde_json::from_value(value).map_err(|err| anyhow!("{label} c2s_json: {err}"))
98}
99
100fn json_path<'a>(value: &'a serde_json::Value, path: &[&str]) -> Option<&'a serde_json::Value> {
101 let mut current = value;
102 for segment in path {
103 current = current.get(*segment)?;
104 }
105 Some(current)
106}
107
108fn validate_combo_trade_contract(label: &str, value: &serde_json::Value) -> Result<()> {
109 match json_path(value, &["header", "acc_id"]).and_then(serde_json::Value::as_u64) {
110 Some(acc_id) if acc_id > 0 => {}
111 _ => bail!("{label} c2s_json header.acc_id must be a positive integer"),
112 }
113
114 let legs = json_path(value, &["combo_legs"])
115 .and_then(serde_json::Value::as_array)
116 .ok_or_else(|| anyhow!("{label} c2s_json combo_legs must be an array"))?;
117 if legs.len() < 2 {
118 bail!("{label} c2s_json combo_legs must contain at least two legs");
119 }
120
121 match json_path(value, &["qty"]).and_then(serde_json::Value::as_f64) {
122 Some(qty) if qty > 0.0 => {}
123 _ => bail!("{label} c2s_json qty must be positive"),
124 }
125
126 let order_type = json_path(value, &["order_type"])
127 .and_then(serde_json::Value::as_i64)
128 .ok_or_else(|| anyhow!("{label} c2s_json order_type must be an integer"))?;
129 if i32::try_from(order_type).is_err() {
130 bail!("{label} c2s_json order_type={order_type} is out of range");
131 }
132
133 Ok(())
134}
135
136pub fn combo_max_context(c2s: &futu_proto::trd_get_combo_max_trd_qtys::C2s) -> Result<u64> {
137 if c2s.header.acc_id == 0 {
138 bail!("combo-max-trd-qtys header.acc_id is required");
139 }
140 trd_env_label(c2s.header.trd_env)?;
141 trd_write_market_label("combo-max-trd-qtys", c2s.header.trd_market)?;
142 Ok(c2s.header.acc_id)
143}
144
145pub fn place_combo_context(
146 c2s: &futu_proto::trd_place_combo_order::C2s,
147) -> Result<ComboTradeContext> {
148 if c2s.header.acc_id == 0 {
149 bail!("combo-order header.acc_id is required");
150 }
151 Ok(ComboTradeContext {
152 env: trd_env_label(c2s.header.trd_env)?,
153 acc_id: c2s.header.acc_id,
154 market: trd_write_market_label("combo-order", c2s.header.trd_market)?,
155 order_value: c2s.price.map(|price| price * c2s.qty),
156 })
157}
158
159pub async fn option_quote(
160 client: &Arc<FutuClient>,
161 c2s: futu_proto::qot_get_option_quote::C2s,
162) -> Result<String> {
163 let response: futu_proto::qot_get_option_quote::Response = send_proto(
164 client,
165 futu_core::proto_id::QOT_GET_OPTION_QUOTE,
166 futu_proto::qot_get_option_quote::Request { c2s },
167 )
168 .await?;
169 finish_response(
170 "option-quote",
171 response.ret_type,
172 response.ret_msg.as_deref(),
173 response.err_code,
174 &response,
175 )
176}
177
178pub async fn option_strategy(
179 client: &Arc<FutuClient>,
180 c2s: futu_proto::qot_get_option_strategy::C2s,
181) -> Result<String> {
182 let response: futu_proto::qot_get_option_strategy::Response = send_proto(
183 client,
184 futu_core::proto_id::QOT_GET_OPTION_STRATEGY,
185 futu_proto::qot_get_option_strategy::Request { c2s },
186 )
187 .await?;
188 finish_response(
189 "option-strategy",
190 response.ret_type,
191 response.ret_msg.as_deref(),
192 response.err_code,
193 &response,
194 )
195}
196
197pub async fn option_strategy_analysis(
198 client: &Arc<FutuClient>,
199 c2s: futu_proto::qot_get_option_strategy_analysis::C2s,
200) -> Result<String> {
201 let response: futu_proto::qot_get_option_strategy_analysis::Response = send_proto(
202 client,
203 futu_core::proto_id::QOT_GET_OPTION_STRATEGY_ANALYSIS,
204 futu_proto::qot_get_option_strategy_analysis::Request { c2s },
205 )
206 .await?;
207 finish_response(
208 "option-strategy-analysis",
209 response.ret_type,
210 response.ret_msg.as_deref(),
211 response.err_code,
212 &response,
213 )
214}
215
216pub async fn option_strategy_spread(
217 client: &Arc<FutuClient>,
218 c2s: futu_proto::qot_get_option_strategy_spread::C2s,
219) -> Result<String> {
220 let response: futu_proto::qot_get_option_strategy_spread::Response = send_proto(
221 client,
222 futu_core::proto_id::QOT_GET_OPTION_STRATEGY_SPREAD,
223 futu_proto::qot_get_option_strategy_spread::Request { c2s },
224 )
225 .await?;
226 finish_response(
227 "option-strategy-spread",
228 response.ret_type,
229 response.ret_msg.as_deref(),
230 response.err_code,
231 &response,
232 )
233}
234
235macro_rules! qot_proto_json_handler {
236 ($fn_name:ident, $label:literal, $proto_const:ident, $module:ident) => {
237 pub async fn $fn_name(
238 client: &Arc<FutuClient>,
239 c2s: futu_proto::$module::C2s,
240 ) -> Result<String> {
241 let response: futu_proto::$module::Response = send_proto(
242 client,
243 futu_core::proto_id::$proto_const,
244 futu_proto::$module::Request { c2s },
245 )
246 .await?;
247 finish_response(
248 $label,
249 response.ret_type,
250 response.ret_msg.as_deref(),
251 response.err_code,
252 &response,
253 )
254 }
255 };
256}
257
258qot_proto_json_handler!(
259 earnings_calendar,
260 "earnings-calendar",
261 QOT_GET_EARNINGS_CALENDAR,
262 qot_get_earnings_calendar
263);
264qot_proto_json_handler!(
265 macro_indicator_list,
266 "macro-indicator-list",
267 QOT_GET_MACRO_INDICATOR_LIST,
268 qot_get_macro_indicator_list
269);
270qot_proto_json_handler!(
271 security_trading_sessions,
272 "security-trading-sessions",
273 QOT_GET_SECURITY_TRADING_SESSIONS,
274 qot_get_security_trading_sessions
275);
276qot_proto_json_handler!(
277 market_trading_sessions,
278 "market-trading-sessions",
279 QOT_GET_MARKET_TRADING_SESSIONS,
280 qot_get_market_trading_sessions
281);
282qot_proto_json_handler!(hot_news, "hot-news", QOT_GET_HOT_NEWS, qot_get_hot_news);
283qot_proto_json_handler!(
284 latest_news,
285 "latest-news",
286 QOT_GET_LATEST_NEWS,
287 qot_get_latest_news
288);
289qot_proto_json_handler!(
290 watchlist_news,
291 "watchlist-news",
292 QOT_GET_WATCHLIST_NEWS,
293 qot_get_watchlist_news
294);
295qot_proto_json_handler!(
296 watchlist_announcement,
297 "watchlist-announcement",
298 QOT_GET_WATCHLIST_ANNOUNCEMENT,
299 qot_get_watchlist_announcement
300);
301qot_proto_json_handler!(
302 watchlist_rating,
303 "watchlist-rating",
304 QOT_GET_WATCHLIST_RATING,
305 qot_get_watchlist_rating
306);
307qot_proto_json_handler!(
308 stock_news,
309 "stock-news",
310 QOT_GET_STOCK_NEWS,
311 qot_get_stock_news
312);
313qot_proto_json_handler!(etf_screen, "etf-screen", QOT_ETF_SCREEN, qot_get_etf_screen);
314qot_proto_json_handler!(
315 fund_screen,
316 "fund-screen",
317 QOT_FUND_SCREEN,
318 qot_get_fund_screen
319);
320qot_proto_json_handler!(
321 future_screen,
322 "future-screen",
323 QOT_FUTURE_SCREEN,
324 qot_get_future_screen
325);
326qot_proto_json_handler!(
327 bond_screen,
328 "bond-screen",
329 QOT_BOND_SCREEN,
330 qot_get_bond_screen
331);
332qot_proto_json_handler!(
333 preview_order_impact,
334 "preview-order-impact",
335 TRD_PREVIEW_ORDER_IMPACT,
336 trd_preview_order_impact
337);
338qot_proto_json_handler!(
339 asset_trend,
340 "get-asset-trend",
341 TRD_GET_ASSET_TREND,
342 trd_get_asset_trend
343);
344qot_proto_json_handler!(
345 yield_trend,
346 "get-yield-trend",
347 TRD_GET_YIELD_TREND,
348 trd_get_yield_trend
349);
350qot_proto_json_handler!(
351 return_calendar,
352 "get-return-calendar",
353 TRD_GET_RETURN_CALENDAR,
354 trd_get_return_calendar
355);
356qot_proto_json_handler!(
357 order_relations,
358 "get-order-relations",
359 TRD_GET_ORDER_RELATIONS,
360 trd_get_order_relations
361);
362qot_proto_json_handler!(
363 position_corporate_actions,
364 "get-position-corporate-actions",
365 TRD_GET_POSITION_CORPORATE_ACTIONS,
366 trd_get_position_corporate_actions
367);
368qot_proto_json_handler!(
369 place_order_group,
370 "place-order-group",
371 TRD_PLACE_ORDER_GROUP,
372 trd_place_order_group
373);
374qot_proto_json_handler!(
375 modify_order_group,
376 "modify-order-group",
377 TRD_MODIFY_ORDER_GROUP,
378 trd_modify_order_group
379);
380qot_proto_json_handler!(
381 cancel_order_group,
382 "cancel-order-group",
383 TRD_CANCEL_ORDER_GROUP,
384 trd_cancel_order_group
385);
386qot_proto_json_handler!(
387 delete_order_group,
388 "delete-order-group",
389 TRD_DELETE_ORDER_GROUP,
390 trd_delete_order_group
391);
392qot_proto_json_handler!(
393 place_algo_order,
394 "place-algo-order",
395 TRD_PLACE_ALGO_ORDER,
396 trd_place_algo_order
397);
398qot_proto_json_handler!(
399 modify_algo_order,
400 "modify-algo-order",
401 TRD_MODIFY_ALGO_ORDER,
402 trd_modify_algo_order
403);
404qot_proto_json_handler!(
405 algo_order_logs,
406 "algo-order-logs",
407 TRD_GET_ALGO_ORDER_LOGS,
408 trd_get_algo_order_logs
409);
410qot_proto_json_handler!(
411 reverse_position,
412 "reverse-position",
413 TRD_REVERSE_POSITION,
414 trd_reverse_position
415);
416qot_proto_json_handler!(
417 roll_position,
418 "roll-position",
419 TRD_ROLL_POSITION,
420 trd_roll_position
421);
422qot_proto_json_handler!(
423 clear_futures_positions,
424 "clear-futures-positions",
425 TRD_CLEAR_FUTURES_POSITIONS,
426 trd_clear_futures_positions
427);
428qot_proto_json_handler!(
429 batch_close_positions,
430 "batch-close-positions",
431 TRD_BATCH_CLOSE_POSITIONS,
432 trd_batch_close_positions
433);
434qot_proto_json_handler!(
435 indicator_list,
436 "indicator-list",
437 QOT_GET_INDICATOR_LIST,
438 qot_get_indicator_list
439);
440qot_proto_json_handler!(
441 macro_indicator_history,
442 "macro-indicator-history",
443 QOT_GET_MACRO_INDICATOR_HISTORY,
444 qot_get_macro_indicator_history
445);
446qot_proto_json_handler!(
447 fed_watch_target_rate,
448 "fed-watch-target-rate",
449 QOT_GET_FED_WATCH_TARGET_RATE,
450 qot_get_fed_watch_target_rate
451);
452qot_proto_json_handler!(
453 fed_watch_dot_plot,
454 "fed-watch-dot-plot",
455 QOT_GET_FED_WATCH_DOT_PLOT,
456 qot_get_fed_watch_dot_plot
457);
458qot_proto_json_handler!(
459 earnings_beat_rank,
460 "earnings-beat-rank",
461 QOT_GET_EARNINGS_BEAT_RANK,
462 qot_get_earnings_beat_rank
463);
464qot_proto_json_handler!(
465 dividend_rank,
466 "dividend-rank",
467 QOT_GET_DIVIDEND_RANK,
468 qot_get_dividend_rank
469);
470qot_proto_json_handler!(
471 dividend_calendar,
472 "dividend-calendar",
473 QOT_GET_DIVIDEND_CALENDAR,
474 qot_get_dividend_calendar
475);
476qot_proto_json_handler!(
477 economic_calendar,
478 "economic-calendar",
479 QOT_GET_ECONOMIC_CALENDAR,
480 qot_get_economic_calendar
481);
482qot_proto_json_handler!(
483 us_pre_market_rank,
484 "us-pre-market-rank",
485 QOT_GET_US_PRE_MARKET_RANK,
486 qot_get_us_pre_market_rank
487);
488qot_proto_json_handler!(
489 us_after_hours_rank,
490 "us-after-hours-rank",
491 QOT_GET_US_AFTER_HOURS_RANK,
492 qot_get_us_after_hours_rank
493);
494qot_proto_json_handler!(
495 us_overnight_rank,
496 "us-overnight-rank",
497 QOT_GET_US_OVERNIGHT_RANK,
498 qot_get_us_overnight_rank
499);
500qot_proto_json_handler!(
501 top_movers_rank,
502 "top-movers-rank",
503 QOT_GET_TOP_MOVERS_RANK,
504 qot_get_top_movers_rank
505);
506qot_proto_json_handler!(hot_list, "hot-list", QOT_GET_HOT_LIST, qot_get_hot_list);
507qot_proto_json_handler!(
508 short_selling_rank,
509 "short-selling-rank",
510 QOT_GET_SHORT_SELLING_RANK,
511 qot_get_short_selling_rank
512);
513qot_proto_json_handler!(
514 period_change_rank,
515 "period-change-rank",
516 QOT_GET_PERIOD_CHANGE_RANK,
517 qot_get_period_change_rank
518);
519qot_proto_json_handler!(
520 high_dividend_soe_rank,
521 "high-dividend-soe-rank",
522 QOT_GET_HIGH_DIVIDEND_SOE_RANK,
523 qot_get_high_dividend_soe_rank
524);
525qot_proto_json_handler!(
526 institution_list,
527 "institution-list",
528 QOT_GET_INSTITUTION_LIST,
529 qot_get_institution_list
530);
531qot_proto_json_handler!(
532 institution_profile,
533 "institution-profile",
534 QOT_GET_INSTITUTION_PROFILE,
535 qot_get_institution_profile
536);
537qot_proto_json_handler!(
538 institution_distribution,
539 "institution-distribution",
540 QOT_GET_INSTITUTION_DISTRIBUTION,
541 qot_get_institution_distribution
542);
543qot_proto_json_handler!(
544 institution_holding_change,
545 "institution-holding-change",
546 QOT_GET_INSTITUTION_HOLDING_CHANGE,
547 qot_get_institution_holding_change
548);
549qot_proto_json_handler!(
550 institution_holding_list,
551 "institution-holding-list",
552 QOT_GET_INSTITUTION_HOLDING_LIST,
553 qot_get_institution_holding_list
554);
555qot_proto_json_handler!(
556 ark_fund_holding,
557 "ark-fund-holding",
558 QOT_GET_ARK_FUND_HOLDING,
559 qot_get_ark_fund_holding
560);
561qot_proto_json_handler!(
562 ark_stock_dynamic,
563 "ark-stock-dynamic",
564 QOT_GET_ARK_STOCK_DYNAMIC,
565 qot_get_ark_stock_dynamic
566);
567qot_proto_json_handler!(
568 ark_active_transaction,
569 "ark-active-transaction",
570 QOT_GET_ARK_ACTIVE_TRANSACTION,
571 qot_get_ark_active_transaction
572);
573qot_proto_json_handler!(
574 rating_change,
575 "rating-change",
576 QOT_GET_RATING_CHANGE,
577 qot_get_rating_change
578);
579qot_proto_json_handler!(
580 search_quote,
581 "search-quote",
582 QOT_GET_SEARCH_QUOTE,
583 qot_get_search_quote
584);
585qot_proto_json_handler!(
586 search_news,
587 "search-news",
588 QOT_GET_SEARCH_NEWS,
589 qot_get_search_news
590);
591qot_proto_json_handler!(
592 option_market_statistic,
593 "option-market-statistic",
594 QOT_GET_OPTION_MARKET_STATISTIC,
595 qot_get_option_market_statistic
596);
597qot_proto_json_handler!(
598 option_underlying_his_statistic,
599 "option-underlying-his-statistic",
600 QOT_GET_OPTION_UNDERLYING_HIS_STATISTIC,
601 qot_get_option_underlying_his_statistic
602);
603qot_proto_json_handler!(
604 option_underlying_overview,
605 "option-underlying-overview",
606 QOT_GET_OPTION_UNDERLYING_OVERVIEW,
607 qot_get_option_underlying_overview
608);
609qot_proto_json_handler!(
610 option_underlying_his_volatility,
611 "option-underlying-his-volatility",
612 QOT_GET_OPTION_UNDERLYING_HIS_VOLATILITY,
613 qot_get_option_underlying_his_volatility
614);
615qot_proto_json_handler!(
616 option_underlying_rank,
617 "option-underlying-rank",
618 QOT_GET_OPTION_UNDERLYING_RANK,
619 qot_get_option_underlying_rank
620);
621qot_proto_json_handler!(
622 option_rank,
623 "option-rank",
624 QOT_GET_OPTION_RANK,
625 qot_get_option_rank
626);
627qot_proto_json_handler!(
628 option_event,
629 "option-event",
630 QOT_GET_OPTION_EVENT,
631 qot_get_option_event
632);
633qot_proto_json_handler!(
634 option_event_alert,
635 "option-event-alert",
636 QOT_GET_OPTION_EVENT_ALERT,
637 qot_get_option_event_alert
638);
639qot_proto_json_handler!(
640 set_option_event_alert,
641 "set-option-event-alert",
642 QOT_SET_OPTION_EVENT_ALERT,
643 qot_set_option_event_alert
644);
645qot_proto_json_handler!(
646 option_zero_dte_screener,
647 "option-zero-dte-screener",
648 QOT_GET_OPTION_ZERO_DTE_SCREENER,
649 qot_get_option_zero_dte_screener
650);
651qot_proto_json_handler!(
652 option_zero_dte_contract,
653 "option-zero-dte-contract",
654 QOT_GET_OPTION_ZERO_DTE_CONTRACT,
655 qot_get_option_zero_dte_contract
656);
657qot_proto_json_handler!(
658 option_earnings_screener,
659 "option-earnings-screener",
660 QOT_GET_OPTION_EARNINGS_SCREENER,
661 qot_get_option_earnings_screener
662);
663qot_proto_json_handler!(
664 option_seller_screener,
665 "option-seller-screener",
666 QOT_GET_OPTION_SELLER_SCREENER,
667 qot_get_option_seller_screener
668);
669qot_proto_json_handler!(
670 industrial_chain_list,
671 "industrial-chain-list",
672 QOT_GET_INDUSTRIAL_CHAIN_LIST,
673 qot_get_industrial_chain_list
674);
675qot_proto_json_handler!(
676 industrial_chain_detail,
677 "industrial-chain-detail",
678 QOT_GET_INDUSTRIAL_CHAIN_DETAIL,
679 qot_get_industrial_chain_detail
680);
681qot_proto_json_handler!(
682 industrial_chain_by_plate,
683 "industrial-chain-by-plate",
684 QOT_GET_INDUSTRIAL_CHAIN_BY_PLATE,
685 qot_get_industrial_chain_by_plate
686);
687qot_proto_json_handler!(
688 industrial_plate_info,
689 "industrial-plate-info",
690 QOT_GET_INDUSTRIAL_PLATE_INFO,
691 qot_get_industrial_plate_info
692);
693qot_proto_json_handler!(
694 industrial_plate_stock,
695 "industrial-plate-stock",
696 QOT_GET_INDUSTRIAL_PLATE_STOCK,
697 qot_get_industrial_plate_stock
698);
699qot_proto_json_handler!(
700 heat_map_data,
701 "heat-map-data",
702 QOT_GET_HEAT_MAP_DATA,
703 qot_get_heat_map_data
704);
705qot_proto_json_handler!(
706 rise_fall_distribution,
707 "rise-fall-distribution",
708 QOT_GET_RISE_FALL_DISTRIBUTION,
709 qot_get_rise_fall_distribution
710);
711
712pub async fn combo_max_trd_qtys(
713 client: &Arc<FutuClient>,
714 c2s: futu_proto::trd_get_combo_max_trd_qtys::C2s,
715) -> Result<String> {
716 let response: futu_proto::trd_get_combo_max_trd_qtys::Response = send_proto(
717 client,
718 futu_core::proto_id::TRD_GET_COMBO_MAX_TRD_QTYS,
719 futu_proto::trd_get_combo_max_trd_qtys::Request { c2s },
720 )
721 .await?;
722 finish_response(
723 "combo-max-trd-qtys",
724 response.ret_type,
725 response.ret_msg.as_deref(),
726 response.err_code,
727 &response,
728 )
729}
730
731pub async fn place_combo_order(
732 client: &Arc<FutuClient>,
733 mut c2s: futu_proto::trd_place_combo_order::C2s,
734 idempotency_key: Option<String>,
735) -> Result<String> {
736 c2s.packet_id = match idempotency_key.as_deref() {
737 Some(key) => packet_id_for_idempotency_key(key),
738 None => {
739 let conn_id = client
740 .conn_id()
741 .ok_or_else(|| anyhow!("combo-order missing InitConnect conn_id"))?;
742 next_packet_id(conn_id)
743 }
744 };
745
746 let response: futu_proto::trd_place_combo_order::Response = send_proto(
747 client,
748 futu_core::proto_id::TRD_PLACE_COMBO_ORDER,
749 futu_proto::trd_place_combo_order::Request { c2s },
750 )
751 .await?;
752 finish_response(
753 "combo-order",
754 response.ret_type,
755 response.ret_msg.as_deref(),
756 response.err_code,
757 &response,
758 )
759}
760
761async fn send_proto<Req, Resp>(
762 client: &Arc<FutuClient>,
763 proto_id: u32,
764 request: Req,
765) -> Result<Resp>
766where
767 Req: Message,
768 Resp: Message + Default,
769{
770 let frame = client.request(proto_id, request.encode_to_vec()).await?;
771 Resp::decode(frame.body.as_ref()).map_err(|err| anyhow!("decode response: {err}"))
772}
773
774fn finish_response<T: Serialize>(
775 label: &str,
776 ret_type: i32,
777 ret_msg: Option<&str>,
778 err_code: Option<i32>,
779 response: &T,
780) -> Result<String> {
781 if ret_type != 0 {
782 bail!("{label} ret_type={ret_type} msg={ret_msg:?} err_code={err_code:?}");
783 }
784 Ok(serde_json::to_string_pretty(response)?)
785}
786
787fn trd_env_label(trd_env: i32) -> Result<&'static str> {
788 match trd_env {
789 0 => Ok("simulate"),
790 1 => Ok("real"),
791 other => {
792 bail!("unsupported combo-order header.trd_env={other}; expected 0 simulate or 1 real")
793 }
794 }
795}
796
797fn trd_write_market_label(endpoint: &str, trd_market: i32) -> Result<String> {
798 if let Some(label) = trade_market::canonical_fund_trd_market_label(trd_market) {
799 bail!(
800 "{endpoint} header.trd_market={trd_market} ({label}) is view-only; \
801 use a write-capable main market for combo trade paths"
802 );
803 }
804 trd_market_label(endpoint, trd_market)
805}
806
807fn trd_market_label(endpoint: &str, trd_market: i32) -> Result<String> {
808 let market = TrdMarketEnum::from_i32(trd_market)
809 .ok_or_else(|| anyhow!("unsupported {endpoint} header.trd_market={trd_market}"))?;
810 let int_values = TrdMarketEnum::all_int_values();
811 let string_values = TrdMarketEnum::all_string_values();
812 let idx = int_values
813 .iter()
814 .position(|&value| value == market.as_i32())
815 .ok_or_else(|| anyhow!("{endpoint} trd_market has no canonical label"))?;
816 Ok(string_values[idx].to_string())
817}
818
819fn next_packet_id(conn_id: u64) -> futu_proto::common::PacketId {
820 let serial_no = PACKET_SERIAL.fetch_add(1, Ordering::Relaxed);
821 futu_proto::common::PacketId { conn_id, serial_no }
822}
823
824fn packet_id_for_idempotency_key(key: &str) -> futu_proto::common::PacketId {
825 let mut hasher = DefaultHasher::new();
826 key.hash(&mut hasher);
827 futu_proto::common::PacketId {
828 conn_id: hasher.finish(),
829 serial_no: 0,
830 }
831}
832
833#[cfg(test)]
834mod tests;