Skip to main content

futucli/cli/
dispatch.rs

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