1use anyhow::Result;
2
3use super::Command;
4use crate::cmd;
5use crate::common::parse_symbol_csv;
6use crate::output::OutputFormat;
7
8mod qot;
9mod tier_m;
10
11pub async fn dispatch(gateway: &str, output: OutputFormat, command: Command) -> Result<()> {
16 match command {
17 Command::Ping => cmd::ping::run(gateway, output).await,
18 Command::Quote(args) => qot::dispatch_quote(gateway, output, args).await,
19 Command::Snapshot(args) => qot::dispatch_snapshot(gateway, output, args).await,
20 Command::Sub(args) => qot::dispatch_sub(gateway, output, args).await,
21 Command::Kline(args) => qot::dispatch_kline(gateway, output, args).await,
22 Command::Orderbook(args) => qot::dispatch_orderbook(gateway, output, args).await,
23 Command::Ticker(args) => qot::dispatch_ticker(gateway, output, args).await,
24 Command::Rt(args) => qot::dispatch_rt(gateway, output, args).await,
25 Command::Static(args) => qot::dispatch_static(gateway, output, args).await,
26 Command::Broker(args) => qot::dispatch_broker(gateway, output, args).await,
27 Command::PlateList(args) => qot::dispatch_plate_list(gateway, output, args).await,
28 Command::PlateStocks(args) => qot::dispatch_plate_stocks(gateway, output, args).await,
29 Command::Account {
30 market,
31 security_firm,
32 all,
33 } => {
34 cmd::account::list_accounts(
35 gateway,
36 output,
37 market.as_deref(),
38 security_firm.as_deref(),
39 all,
40 )
41 .await
42 }
43 Command::Funds(args) => {
44 let acc_id = cmd::account::resolve_account_locator(
45 gateway,
46 args.acc_id,
47 args.card_num.as_deref(),
48 "funds",
49 )
50 .await?;
51 cmd::account::funds(
52 gateway,
53 &args.env,
54 acc_id,
55 args.market.as_deref(),
56 args.currency.as_deref(),
57 output,
58 )
59 .await
60 }
61 Command::Position(args) => {
62 let acc_id = cmd::account::resolve_account_locator(
63 gateway,
64 args.acc_id,
65 args.card_num.as_deref(),
66 "positions",
67 )
68 .await?;
69 cmd::account::positions(gateway, &args.env, acc_id, &args.market, output).await
70 }
71 Command::Order(args) => {
72 let acc_id = cmd::account::resolve_account_locator(
73 gateway,
74 args.acc_id,
75 args.card_num.as_deref(),
76 "orders",
77 )
78 .await?;
79 cmd::account::orders(gateway, &args.env, acc_id, &args.market, output).await
80 }
81 Command::Deal(args) => {
82 let acc_id = cmd::account::resolve_account_locator(
83 gateway,
84 args.acc_id,
85 args.card_num.as_deref(),
86 "deals",
87 )
88 .await?;
89 cmd::account::deals(gateway, &args.env, acc_id, &args.market, output).await
90 }
91 Command::UnlockTrade {
92 lock,
93 from_stdin,
94 otp,
95 security_firm,
96 acc_ids,
97 } => {
98 cmd::unlock::run(
99 gateway,
100 lock,
101 from_stdin,
102 otp,
103 security_firm,
104 acc_ids,
105 output,
106 )
107 .await
108 }
109 Command::SetTradePwd {
110 account,
111 from_stdin,
112 } => cmd::unlock::set_trade_pwd(&account, from_stdin).await,
113 Command::ClearTradePwd { account } => cmd::unlock::clear_trade_pwd(&account).await,
114 Command::SetLoginPwd {
115 account,
116 from_stdin,
117 } => cmd::unlock::set_login_pwd(&account, from_stdin).await,
118 Command::ClearLoginPwd { account } => cmd::unlock::clear_login_pwd(&account).await,
119 Command::Repl => {
120 anyhow::bail!(
123 "cannot enter REPL from this context (already nested / not a top-level invocation)"
124 )
125 }
126 Command::GenKey(args) => {
127 cmd::gen_key::run(cmd::gen_key::GenKeyCommand {
128 id: args.id,
129 scopes: args.scopes,
130 keys_file: args.keys_file,
131 expires: args.expires,
132 note: args.note,
133 allowed_markets: args.allowed_markets,
134 allowed_symbols: args.allowed_symbols,
135 max_order_value: args.max_order_value,
136 max_daily_value: args.max_daily_value,
137 hours_window: args.hours_window,
138 max_orders_per_minute: args.max_orders_per_minute,
139 allowed_trd_sides: args.allowed_trd_sides,
140 allowed_acc_ids: args.allowed_acc_ids,
141 allowed_card_nums: args.allowed_card_nums,
142 bind_this_machine: args.bind_this_machine,
143 bind_machines: args.bind_machines,
144 })
145 .await
146 }
147 Command::BindKey {
148 id,
149 keys_file,
150 this_machine,
151 machines,
152 replace,
153 clear,
154 freeze,
155 } => {
156 cmd::bind_key::run(cmd::bind_key::BindKeyCommand {
157 id,
158 keys_file,
159 this_machine,
160 machines,
161 replace,
162 clear,
163 freeze,
164 })
165 .await
166 }
167 Command::MachineId { for_key } => cmd::machine::run(for_key).await,
168 Command::ListKeys { keys_file, json } => cmd::keys::list(keys_file, json).await,
169 Command::RevokeKey { id, keys_file, yes } => cmd::keys::revoke(id, keys_file, yes).await,
170
171 Command::PlaceOrder(args) => {
173 let acc_id = cmd::account::resolve_account_locator(
174 gateway,
175 args.acc_id,
176 args.card_num.as_deref(),
177 "place-order",
178 )
179 .await?;
180 cmd::trade_ext::run_place_order(cmd::trade_ext::PlaceOrderCommand {
181 gateway,
182 env: &args.env,
183 acc_id,
184 market: &args.market,
185 side: &args.side,
186 order_type: &args.order_type,
187 code: &args.code,
188 qty: args.qty,
189 price: args.price,
190 confirm: args.confirm,
191 idempotency_key: args.idempotency_key,
192 stop_price: args.stop_price,
193 trail_type: args.trail_type,
194 trail_value: args.trail_value,
195 trail_spread: args.trail_spread,
196 output,
197 })
198 .await
199 }
200 Command::ModifyOrder(args) => {
201 let acc_id = cmd::account::resolve_account_locator(
202 gateway,
203 args.acc_id,
204 args.card_num.as_deref(),
205 "modify-order",
206 )
207 .await?;
208 cmd::trade_ext::run_modify_order(cmd::trade_ext::ModifyOrderCommand {
209 gateway,
210 env: &args.env,
211 acc_id,
212 market: &args.market,
213 order_id: args.order_id,
214 op: &args.op,
215 qty: args.qty,
216 price: args.price,
217 confirm: args.confirm,
218 idempotency_key: args.idempotency_key,
219 output,
220 })
221 .await
222 }
223 Command::CancelOrder(args) => {
224 let acc_id = cmd::account::resolve_account_locator(
225 gateway,
226 args.acc_id,
227 args.card_num.as_deref(),
228 "cancel-order",
229 )
230 .await?;
231 cmd::trade_ext::run_cancel_order(
232 gateway,
233 &args.env,
234 acc_id,
235 &args.market,
236 args.order_id,
237 args.confirm,
238 args.idempotency_key,
239 output,
240 )
241 .await
242 }
243 Command::ReconfirmOrder(args) => {
244 let acc_id = cmd::account::resolve_account_locator(
245 gateway,
246 args.acc_id,
247 args.card_num.as_deref(),
248 "reconfirm-order",
249 )
250 .await?;
251 cmd::trade_ext::run_reconfirm_order(
252 gateway,
253 &args.env,
254 acc_id,
255 &args.market,
256 args.order_id,
257 args.reason,
258 args.confirm,
259 output,
260 )
261 .await
262 }
263 Command::HistoryOrders(args) => {
264 let acc_id = cmd::account::resolve_account_locator(
265 gateway,
266 args.acc_id,
267 args.card_num.as_deref(),
268 "history-orders",
269 )
270 .await?;
271 let code_list = args
272 .codes
273 .map(|s| {
274 s.split(',')
275 .map(|x| x.trim().to_string())
276 .filter(|x| !x.is_empty())
277 .collect()
278 })
279 .unwrap_or_default();
280 cmd::trade_ext::run_history_orders(cmd::trade_ext::HistoryOrdersCommand {
281 gateway,
282 env: &args.env,
283 acc_id,
284 market: &args.market,
285 codes: code_list,
286 begin: args.begin,
287 end: args.end,
288 output,
289 })
290 .await
291 }
292 Command::HistoryDeals(args) => {
293 let acc_id = cmd::account::resolve_account_locator(
294 gateway,
295 args.acc_id,
296 args.card_num.as_deref(),
297 "history-deals",
298 )
299 .await?;
300 let code_list = args
301 .codes
302 .map(|s| {
303 s.split(',')
304 .map(|x| x.trim().to_string())
305 .filter(|x| !x.is_empty())
306 .collect()
307 })
308 .unwrap_or_default();
309 cmd::trade_ext::run_history_deals(cmd::trade_ext::HistoryDealsCommand {
310 gateway,
311 env: &args.env,
312 acc_id,
313 market: &args.market,
314 codes: code_list,
315 begin: args.begin,
316 end: args.end,
317 output,
318 })
319 .await
320 }
321 Command::MaxQtys(args) => {
322 let acc_id = cmd::account::resolve_account_locator(
323 gateway,
324 args.acc_id,
325 args.card_num.as_deref(),
326 "max-trd-qtys",
327 )
328 .await?;
329 cmd::trade_ext::run_max_qtys(cmd::trade_ext::MaxQtysCommand {
330 gateway,
331 env: &args.env,
332 acc_id,
333 market: &args.market,
334 order_type: &args.order_type,
335 code: &args.code,
336 price: args.price,
337 output,
338 })
339 .await
340 }
341 Command::MarginRatio(args) => {
343 let acc_id = cmd::account::resolve_account_locator(
344 gateway,
345 args.acc_id,
346 args.card_num.as_deref(),
347 "margin-ratio",
348 )
349 .await?;
350 let symbols = args.symbols.or(args.symbols_arg).ok_or_else(|| {
351 anyhow::anyhow!("margin-ratio: 需要位置参数 <SYMBOLS> 或 --code / --symbols")
352 })?;
353 let syms: Vec<String> = symbols.split(',').map(|s| s.trim().to_string()).collect();
354 cmd::trade_ext::run_margin_ratio(
355 gateway,
356 &args.env,
357 acc_id,
358 &args.market,
359 &syms,
360 output,
361 )
362 .await
363 }
364 Command::OrderFee(args) => {
365 let acc_id = cmd::account::resolve_account_locator(
366 gateway,
367 args.acc_id,
368 args.card_num.as_deref(),
369 "order-fee",
370 )
371 .await?;
372 let ids: Vec<String> = args
373 .order_ids
374 .split(',')
375 .map(|s| s.trim().to_string())
376 .collect();
377 cmd::trade_ext::run_order_fee(gateway, &args.env, acc_id, &args.market, &ids, output)
378 .await
379 }
380 Command::CapitalFlow {
381 symbol,
382 period_type,
383 begin,
384 end,
385 } => {
386 cmd::analysis::run_capital_flow(
387 gateway,
388 &symbol,
389 period_type,
390 begin.as_deref(),
391 end.as_deref(),
392 output,
393 )
394 .await
395 }
396 Command::CapitalDistribution { symbol } => {
397 cmd::analysis::run_capital_distribution(gateway, &symbol, output).await
398 }
399 Command::MarketState { symbols } => {
400 let list = parse_symbol_csv(&symbols)?;
403 cmd::analysis::run_market_state(gateway, &list, output).await
404 }
405 Command::OwnerPlate { symbols } => {
406 let list = parse_symbol_csv(&symbols)?;
408 cmd::analysis::run_owner_plate(gateway, &list, output).await
409 }
410 Command::OptionChain {
411 owner,
412 owner_arg,
413 begin,
414 end,
415 option_type,
416 delta_min,
417 delta_max,
418 iv_min,
419 iv_max,
420 oi_min,
421 oi_max,
422 gamma_min,
423 gamma_max,
424 vega_min,
425 vega_max,
426 theta_min,
427 theta_max,
428 } => {
429 let owner = owner.or(owner_arg).ok_or_else(|| {
430 anyhow::anyhow!("option-chain: 需要位置参数 <OWNER> 或 --owner / --code")
431 })?;
432 cmd::analysis::run_option_chain(
433 gateway,
434 &owner,
435 &begin,
436 &end,
437 &option_type,
438 cmd::analysis::OptionChainGreekFilterArgs {
439 delta_min,
440 delta_max,
441 iv_min,
442 iv_max,
443 oi_min,
444 oi_max,
445 gamma_min,
446 gamma_max,
447 vega_min,
448 vega_max,
449 theta_min,
450 theta_max,
451 },
452 output,
453 )
454 .await
455 }
456
457 Command::TradingDays { market, begin, end } => {
459 cmd::analysis::run_trading_days(gateway, &market, &begin, &end, output).await
460 }
461 Command::Rehab { symbol } => cmd::analysis::run_rehab(gateway, &symbol, output).await,
462 Command::Suspend {
463 symbols,
464 symbols_arg,
465 begin,
466 end,
467 } => {
468 let symbols = symbols.or(symbols_arg).ok_or_else(|| {
469 anyhow::anyhow!("suspend: 需要位置参数 <SYMBOLS> 或 --code / --symbols")
470 })?;
471 let syms = parse_symbol_csv(&symbols)?;
473 cmd::analysis::run_suspend(gateway, &syms, &begin, &end, output).await
474 }
475 Command::UserSecurity { group, group_arg } => {
476 let group = group
477 .or(group_arg)
478 .ok_or_else(|| anyhow::anyhow!("user-security: 需要位置参数 <GROUP> 或 --group"))?;
479 cmd::analysis::run_user_security(gateway, &group, output).await
480 }
481 Command::UserSecurityGroups { group_type } => {
482 cmd::analysis::run_user_security_groups(gateway, group_type, output).await
483 }
484 Command::Warrant { owner, begin, num } => {
485 cmd::analysis::run_warrant(gateway, owner.as_deref(), begin, num, output).await
486 }
487 Command::IpoList { market } => cmd::analysis::run_ipo_list(gateway, &market, output).await,
488 Command::FutureInfo { symbols } => {
489 let syms = parse_symbol_csv(&symbols)?;
491 cmd::analysis::run_future_info(gateway, &syms, output).await
492 }
493 Command::StockFilter { market, begin, num } => {
494 cmd::analysis::run_stock_filter(gateway, &market, begin, num, output).await
495 }
496 Command::CancelAllOrder {
497 acc_id,
498 card_num,
499 env,
500 market,
501 confirm,
502 } => {
503 let acc_id = cmd::account::resolve_account_locator(
504 gateway,
505 acc_id,
506 card_num.as_deref(),
507 "cancel-all-order",
508 )
509 .await?;
510 cmd::trade_ext::run_cancel_all_order(
511 gateway,
512 acc_id,
513 &env,
514 market.as_deref(),
515 confirm,
516 output,
517 )
518 .await
519 }
520 Command::GlobalState => cmd::sys::run_global_state(gateway, output).await,
521 Command::UserInfo => cmd::sys::run_user_info(gateway, output).await,
522 Command::QuoteRights { refresh } => {
523 cmd::sys::run_quote_rights(gateway, refresh, output).await
524 }
525 Command::DelayStatistics => cmd::sys::run_delay_statistics(gateway, output).await,
526 Command::TokenState => cmd::sys::run_token_state(gateway, output).await,
527 Command::RiskFreeRate => cmd::sys::run_risk_free_rate(gateway, output).await,
528 Command::SpreadTable => cmd::sys::run_spread_table(gateway, output).await,
529 Command::TickerStatistic {
530 symbol_pos,
531 symbol,
532 ticker_type,
533 stat_type,
534 } => {
535 let sym = symbol_pos.or(symbol).ok_or_else(|| {
537 anyhow::anyhow!("ticker-statistic: SYMBOL or --symbol required (e.g. HK.00700)")
538 })?;
539 cmd::sys::run_ticker_statistic(gateway, &sym, ticker_type, stat_type, output).await
540 }
541 Command::TickerStatisticDetail {
543 symbol_pos,
544 symbol,
545 ticker_type,
546 ticker_time,
547 select_num,
548 data_from,
549 data_max_count,
550 stat_type,
551 } => {
552 let sym = symbol_pos.or(symbol).ok_or_else(|| {
553 anyhow::anyhow!(
554 "ticker-statistic-detail: SYMBOL or --symbol required (e.g. HK.00700)"
555 )
556 })?;
557 cmd::sys::run_ticker_statistic_detail(cmd::sys::TickerStatisticDetailCommand {
558 gateway,
559 symbol: &sym,
560 ticker_type,
561 ticker_time,
562 select_num,
563 data_from,
564 data_max_count,
565 stat_type,
566 format: output,
567 })
568 .await
569 }
570
571 Command::QuerySubscription { all_conn } => {
573 cmd::sys::run_query_subscription(gateway, all_conn, output).await
574 }
575 Command::UsedQuota => cmd::sys::run_used_quota(gateway, output).await,
576 Command::Unsubscribe {
577 symbols,
578 sub_types,
579 all,
580 } => {
581 let syms: Vec<String> = if symbols.trim().is_empty() {
582 vec![]
583 } else {
584 symbols.split(',').map(|s| s.trim().to_string()).collect()
585 };
586 let types: Vec<i32> = if sub_types.trim().is_empty() {
587 vec![]
588 } else {
589 sub_types
590 .split(',')
591 .map(|s| s.trim().parse::<i32>())
592 .collect::<std::result::Result<Vec<_>, _>>()
593 .map_err(|e| anyhow::anyhow!("invalid sub-type: {e}"))?
594 };
595 cmd::sys::run_unsubscribe(gateway, &syms, &types, all, output).await
596 }
597 Command::HistoryKlQuota { detail } => {
598 cmd::analysis::run_history_kl_quota(gateway, detail, output).await
599 }
600 Command::HoldingChange {
601 symbol,
602 category,
603 begin,
604 end,
605 } => {
606 cmd::analysis::run_holding_change(
607 gateway,
608 &symbol,
609 category,
610 begin.as_deref(),
611 end.as_deref(),
612 output,
613 )
614 .await
615 }
616 Command::ModifyUserSecurity { group, op, symbols } => {
617 let syms: Vec<String> = symbols.split(',').map(|s| s.trim().to_string()).collect();
618 cmd::analysis::run_modify_user_security(gateway, &group, op, &syms, output).await
619 }
620 Command::CodeChange { symbols } => {
621 let syms: Vec<String> = symbols.split(',').map(|s| s.trim().to_string()).collect();
622 cmd::analysis::run_code_change(gateway, &syms, output).await
623 }
624 Command::SetPriceReminder {
625 symbol,
626 op,
627 key,
628 r#type,
629 freq,
630 value,
631 note,
632 session,
633 } => {
634 cmd::analysis::run_set_price_reminder(cmd::analysis::SetPriceReminderCommand {
635 gateway,
636 symbol: &symbol,
637 op,
638 key,
639 reminder_type: r#type,
640 freq,
641 value,
642 note: note.as_deref(),
643 reminder_session_list: &session,
644 })
645 .await
646 }
647 Command::PriceReminder { symbol, market } => {
648 cmd::analysis::run_get_price_reminder(gateway, symbol.as_deref(), market, output).await
649 }
650 Command::OptionExpirationDate {
651 owner,
652 owner_arg,
653 index_type,
654 } => {
655 let owner = owner.or(owner_arg).ok_or_else(|| {
656 anyhow::anyhow!("option-expiration-date: 需要位置参数 <OWNER> 或 --owner")
657 })?;
658 cmd::analysis::run_option_expiration_date(gateway, &owner, index_type, output).await
659 }
660 Command::SubAccPush { acc_ids } => {
661 let ids: Vec<u64> = acc_ids
662 .split(',')
663 .map(|s| s.trim().parse::<u64>())
664 .collect::<std::result::Result<Vec<_>, _>>()
665 .map_err(|e| anyhow::anyhow!("invalid acc id: {e}"))?;
666 cmd::trade_ext::run_sub_acc_push(gateway, &ids, output).await
667 }
668 Command::AccCashFlow(args) => {
669 let acc_id = cmd::account::resolve_account_locator(
670 gateway,
671 args.acc_id.or(args.acc_id_arg),
672 args.card_num.as_deref(),
673 "acc-cash-flow",
674 )
675 .await?;
676 if let Some(range) = args.date_range {
678 let (from_s, to_s) = range.split_once("..").ok_or_else(|| {
680 anyhow::anyhow!("--date-range 格式应为 `YYYY-MM-DD..YYYY-MM-DD`,当前:{range}")
681 })?;
682 let from = chrono::NaiveDate::parse_from_str(from_s, "%Y-%m-%d")
683 .map_err(|e| anyhow::anyhow!("start date parse: {e}"))?;
684 let to = chrono::NaiveDate::parse_from_str(to_s, "%Y-%m-%d")
685 .map_err(|e| anyhow::anyhow!("end date parse: {e}"))?;
686 cmd::trade_ext::run_acc_cash_flow_range(cmd::trade_ext::AccCashFlowRangeCommand {
687 gateway,
688 acc_id,
689 date_from: from,
690 date_to: to,
691 env: &args.env,
692 market: &args.market,
693 direction: args.direction,
694 })
695 .await
696 } else if let Some(d) = args.date {
697 cmd::trade_ext::run_acc_cash_flow(
698 gateway,
699 acc_id,
700 &d,
701 &args.env,
702 &args.market,
703 args.direction,
704 output,
705 )
706 .await
707 } else {
708 anyhow::bail!("需传 --date <YYYY-MM-DD> 或 --date-range <FROM..TO>")
709 }
710 }
711 Command::DaemonStatus {
712 rest_url,
713 rest_port,
714 api_key,
715 } => {
716 let effective_url =
718 rest_url.or_else(|| rest_port.map(|p| format!("http://127.0.0.1:{p}")));
719 cmd::daemon::run_status(effective_url.as_deref(), api_key.as_deref(), output).await
720 }
721 Command::DaemonShutdown {
722 rest_url,
723 rest_port,
724 api_key,
725 } => {
726 let effective_url =
728 rest_url.or_else(|| rest_port.map(|p| format!("http://127.0.0.1:{p}")));
729 cmd::daemon::run_shutdown(effective_url.as_deref(), api_key.as_deref()).await
730 }
731 Command::DaemonReload {
732 rest_url,
733 rest_port,
734 api_key,
735 } => {
736 let effective_url =
738 rest_url.or_else(|| rest_port.map(|p| format!("http://127.0.0.1:{p}")));
739 cmd::daemon::run_reload(effective_url.as_deref(), api_key.as_deref()).await
740 }
741
742 Command::CashLog(args) => tier_m::dispatch_cash_log(gateway, output, args).await,
746 Command::CashDetail(args) => tier_m::dispatch_cash_detail(gateway, output, args).await,
747 Command::BizGroup(args) => tier_m::dispatch_biz_group(gateway, output, args).await,
748 Command::MarginInfo(args) => tier_m::dispatch_margin_info(gateway, output, args).await,
749 Command::AccountFlag(args) => tier_m::dispatch_account_flag(gateway, output, args).await,
750 Command::BondTotalAsset(args) => {
751 tier_m::dispatch_bond_total_asset(gateway, output, args).await
752 }
753 Command::BondSingleAsset(args) => {
754 tier_m::dispatch_bond_single_asset(gateway, output, args).await
755 }
756 Command::BondPositionList(args) => {
757 tier_m::dispatch_bond_position_list(gateway, output, args).await
758 }
759 Command::BondAnswerState(args) => {
760 tier_m::dispatch_bond_answer_state(gateway, output, args).await
761 }
762 Command::BondTradeReminder(args) => {
763 tier_m::dispatch_bond_trade_reminder(gateway, output, args).await
764 }
765 }
766}