Skip to main content

futucli/cmd/trade_ext/
hints.rs

1use futu_core::error::FutuError;
2
3/// v1.4.92 D1: backend ret_msg → 用户可执行 hint 翻译表
4///
5/// 当 backend 返 `ret_type != 0 + ret_msg=<chinese>` 时,把常见 ret_msg pattern
6/// 翻译成用户下一步该跑什么命令。返 `Some(hint_string)` 表示能识别;返
7/// `None` 表示未匹配(保持原 ret_msg 显示,不画蛇添足)。
8///
9/// **设计原则**:
10/// - 只匹配,不替换 — 原始 ret_msg 仍由上层 `Context` chain 完整显示
11/// - 不假设 — 没匹配的 pattern 直接返 None,不强行猜
12/// - 用户视角 — hint 内容是"下一步跑什么命令",不是"为什么错了"
13pub(crate) fn translate_trade_ret_msg(ret_msg: &str) -> Option<String> {
14    // 统一去掉前后空白 / 防 backend 偶尔多个空格
15    let m = ret_msg.trim();
16
17    // 1. 解锁交易 — 写路径必须先 CMD 2900/2901 解锁
18    if m.contains("请重新解锁交易") || m.contains("请先解锁交易") || m.contains("未解锁交易")
19    {
20        return Some(
21            "建议: futucli unlock-trade --pwd <md5(交易密码)> \
22             (写路径需先解锁;2FA 账户用 --totp <6位数字>)"
23                .to_string(),
24        );
25    }
26
27    // 2. 余额 / 资金不足
28    if m.contains("余额不足") || m.contains("可用资金") || m.contains("资金不足") {
29        return Some(
30            "建议: futucli funds --acc-id <ID> --env real 查看可用资金;\
31             下单 qty × price 需 ≤ 可用资金(含手续费 buffer)"
32                .to_string(),
33        );
34    }
35
36    // 3. 交易未启用 / daemon 未开 trade
37    if m.contains("交易未启用") || m.contains("交易未开通") || m.contains("未开通交易")
38    {
39        return Some(
40            "建议: futu-opend 启动时需走完 unlock-trade 流程;\
41             或 daemon 起得太新还没拿到 cipher,等 30s 重试"
42                .to_string(),
43        );
44    }
45
46    // 4. 标的代码错 / 不存在
47    if m.contains("代码不存在")
48        || m.contains("标的代码错误")
49        || m.contains("代码错误")
50        || m.contains("证券不存在")
51        || m.contains("无效代码")
52        || m.contains("找不到证券")
53    {
54        return Some(
55            "建议: 检查 prefix (HK./US./SH./SZ.) 是否正确;\
56             用 `futucli static-info --code <CODE> --market <MK>` 验证代码存在"
57                .to_string(),
58        );
59    }
60
61    // 5. 市场已休市 / 非交易时段
62    if m.contains("市场已休市") || m.contains("非交易时段") || m.contains("休市") {
63        return Some(
64            "建议: 用 `futucli market-state --market <MK>` 确认当前 trading hours;\
65             盘前 / 盘后单需明确 session 字段"
66                .to_string(),
67        );
68    }
69
70    // 6. 数量 / 价格不符合规则(lot size / tick size / 涨跌停)
71    if m.contains("最小变动")
72        || m.contains("最小交易单位")
73        || m.contains("交易单位")
74        || m.contains("整手")
75        || m.contains("不符合最小")
76    {
77        return Some(
78            "建议: HK 股票按手(lot size)整数倍下单(如 700 至少 100 股);\
79             用 `futucli static-info --code <CODE>` 查 lot_size"
80                .to_string(),
81        );
82    }
83
84    if m.contains("价格超出") || m.contains("涨跌停") || m.contains("价格不在") {
85        return Some(
86            "建议: 价格需在涨跌停范围内;用 `futucli snapshot --code <CODE>` \
87             查 highest_price / lowest_price 边界"
88                .to_string(),
89        );
90    }
91
92    // 7. 持仓不足(卖单)
93    if m.contains("持仓不足") || m.contains("可卖") || m.contains("持仓数量") {
94        return Some(
95            "建议: futucli positions --acc-id <ID> --env real 查可卖数量;\
96             T+1 / 港股交收 settled qty 才可卖出"
97                .to_string(),
98        );
99    }
100
101    // 未匹配 — 让上层显示原 ret_msg,不画蛇添足
102    None
103}
104
105/// v1.4.92 D1: 把 trade RPC 的 `Result<T>` 错误尽可能加上 user-actionable
106/// hint 到 stderr,再原样 propagate。
107///
108/// **行为不变性**:error 完整原样返回,exit code 不变;hint 是**纯增量**输出
109/// 到 stderr 前缀 `💡 Hint:`,不替换原 error chain。
110///
111/// 接收 anyhow::Error;尝试 downcast 到 `FutuError::ServerError` 抓 ret_msg。
112/// 若不是 ServerError,返 None(保持沉默)。
113pub(crate) fn emit_trade_hint_if_known(err: &anyhow::Error) {
114    // anyhow::Error 包了多层 Context,downcast 到 FutuError 看是否 ServerError
115    if let Some(FutuError::ServerError { ret_type: _, msg }) = err.downcast_ref::<FutuError>()
116        && let Some(hint) = translate_trade_ret_msg(msg)
117    {
118        eprintln!("💡 Hint: {hint}");
119    }
120}