Skip to main content

futucli/cmd/trade_ext/
idempotency.rs

1/// v1.4.41 (eli v1.4.40 报告 P3.6 修): opt-in auto key 改成**参数 deterministic
2/// hash**,让同参数重试真能 dedup。
3///
4/// **v1.4.40 bug**:用 `rand::thread_rng` 每次生成新 UUID → daemon 看到不同
5/// packet_id → 每次都当新请求,dedup 失效。eli wire-level 实证跑 2 次同参数
6/// 的 ConnID 字节完全不同。
7///
8/// **v1.4.41 修法**:用 `(acc_id, market, code, side, qty, price, order_type)`
9/// 7 元组 hash 成 deterministic key。daemon 端 90s TTL cache 会合并同参数
10/// retry,返回第一次的结果,避免重复下单。
11///
12/// **footgun 提示保持**:
13/// - 跨 daemon 进程 retry 还是靠 daemon cache TTL(默认 90s),超时就重新下单
14/// - 同 user-intended 下两次不同单(比如 qty=100 + qty=200)因 hash 不同,不会
15///   被合并(符合预期)
16/// - **同参数**两次下单**会**被合并(只生成一单),用户想再下必须手动改参
17///   (price 调整 0.001 / qty 调整 1 / 或加 --remark 作区分)—— 这是 opt-in
18///   幂等性的代价。默认关闭不影响不 opt-in 的用户。
19///
20/// 不加 opt-in 默认关闭(`None`),保持 v1.4.39 行为不变。
21///
22/// **Signature 变化**:v1.4.40 `(Option<String>) -> Option<String>` →
23/// v1.4.41 `(Option<String>, &IdempotencyParams) -> Option<String>`
24#[derive(Debug, Clone, Copy)]
25pub(crate) struct IdempotencyParams<'a> {
26    pub acc_id: u64,
27    pub market: &'a str,
28    pub code: &'a str,
29    pub side: &'a str,
30    pub qty: f64,
31    pub price: Option<f64>,
32    pub order_type: &'a str,
33}
34
35pub(crate) fn resolve_auto_idempotency_key(
36    user_provided: Option<String>,
37    params: &IdempotencyParams<'_>,
38) -> Option<String> {
39    if user_provided.is_some() {
40        return user_provided;
41    }
42    if std::env::var("FUTU_CLI_AUTO_IDEM")
43        .ok()
44        .is_some_and(|v| matches!(v.trim(), "1" | "true" | "yes" | "on"))
45    {
46        use std::collections::hash_map::DefaultHasher;
47        use std::hash::{Hash, Hasher};
48        let mut hasher = DefaultHasher::new();
49        params.acc_id.hash(&mut hasher);
50        params.market.hash(&mut hasher);
51        params.code.hash(&mut hasher);
52        params.side.hash(&mut hasher);
53        // f64 Hash 不实现 —— 走 bits()(同 f64 值 => 同 bits)
54        params.qty.to_bits().hash(&mut hasher);
55        params.price.map(|p| p.to_bits()).hash(&mut hasher);
56        params.order_type.hash(&mut hasher);
57        let h = hasher.finish();
58        // 格式化为 "auto-<16 hex>"(8 byte hash 输出)便于 daemon log 识别
59        Some(format!("auto-{h:016x}"))
60    } else {
61        None
62    }
63}