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}