Skip to main content

futu_cache/trd_cache/
order_list.rs

1use super::*;
2use futu_domain_trade_account::{
3    LocalDeletedOrderDecision, LocalDeletedOrderMatchFacts, OrphanOrderDecision, OrphanOrderFacts,
4    PendingConfirmAccountFacts, PendingConfirmDecision, PendingConfirmOrderFacts,
5    PendingStubPurgeDecision, PendingStubPurgeFacts, decide_orphan_order_like_cpp,
6    decide_pending_confirm_clear_for_account_like_cpp,
7    decide_pending_confirm_clear_for_order_ids_like_cpp, decide_pending_stub_purge_for_cleanup,
8    plan_local_deleted_order_mark_like_cpp,
9};
10
11impl TrdCache {
12    /// Mark an existing cached order as local Deleted(23) after a successful
13    /// DeleteFailOrder operation.
14    ///
15    /// C++ keeps local orders (`bIsLocalOrder=true`) across `UpdateOrderList`
16    /// backend refreshes. Rust must do the same for delete success, otherwise a
17    /// backend refresh that omits the just-deleted failed order makes
18    /// `order_list_query()` return empty while official C++ still shows
19    /// Deleted(23).
20    #[must_use]
21    pub fn mark_order_deleted_local(
22        &self,
23        acc_id: u64,
24        order_id: u64,
25        order_id_ex: Option<&str>,
26    ) -> bool {
27        let trimmed_ex = order_id_ex.map(str::trim).filter(|s| !s.is_empty());
28        let Some(mut entry) = self.orders.get_mut(&acc_id) else {
29            return false;
30        };
31        let Some((order, patch)) = entry.iter_mut().find_map(|order| {
32            match plan_local_deleted_order_mark_like_cpp(LocalDeletedOrderMatchFacts {
33                requested_order_id: order_id,
34                requested_order_id_ex: trimmed_ex,
35                cached_order_id: order.order_id,
36                cached_order_id_ex: &order.order_id_ex,
37                cached_backend_order_id: &order.backend_order_id,
38            }) {
39                LocalDeletedOrderDecision::MarkDeleted(patch) => Some((order, patch)),
40                LocalDeletedOrderDecision::Keep => None,
41            }
42        }) else {
43            return false;
44        };
45
46        order.order_status = patch.order_status;
47        order.is_stub = patch.is_stub;
48        order.is_local_order = patch.is_local_order;
49        order.stub_inserted_at_ms = patch.stub_inserted_at_ms;
50        order.is_pending_broker_confirm = patch.is_pending_broker_confirm;
51        true
52    }
53
54    /// v1.4.105 BUG-v1.4.104-001 (P0): broker async confirm 到达后清 pending 标志.
55    ///
56    /// 当 push notice_type=4/5/8/100 (ORDER_UPDATE / ORDER_LIST_UPDATE /
57    /// TRADE_STATISTIC / ORDER_NTF) 到达对应 acc_id 时, 调本 fn 把所有
58    /// `is_pending_broker_confirm=true` 的 order 翻成 `false`.
59    ///
60    /// 设计选择: 不按 order_id 精确匹配清 — push notice 通常不带具体 order_id,
61    /// 只表 "本 acc 有 order 状态变化". 简化处理: acc 内任何 ORDER 类 push 到
62    /// 即视为 broker 已开始处理本 acc 的 stub orders.
63    /// 后续 query_orders refresh 会通过 `merge_preserving_stubs` 把 enriched
64    /// 版本写入, 替换 stub.
65    ///
66    /// 返被清的 order 数 (caller 用于 audit log).
67    pub fn clear_pending_confirm_for_acc(&self, acc_id: u64) -> usize {
68        let mut cleared = 0;
69        if let Some(mut entry) = self.orders.get_mut(&acc_id) {
70            for o in entry.iter_mut() {
71                if decide_pending_confirm_clear_for_account_like_cpp(PendingConfirmAccountFacts {
72                    is_pending_broker_confirm: o.is_pending_broker_confirm,
73                }) == PendingConfirmDecision::Clear
74                {
75                    o.is_pending_broker_confirm = false;
76                    cleared += 1;
77                }
78            }
79        }
80        cleared
81    }
82
83    /// v1.4.106 codex 0226 F4 (P2): selective clear pending confirm by order_ids.
84    ///
85    /// `clear_pending_confirm_for_acc` 是 acc-level 全清, 但 ORDER push notify
86    /// 在 backend 实际带具体 `order_ids` 时(notice_type=4 ORDER_UPDATE 通常
87    /// 带), daemon 应**只**清对应订单的 pending flag, 而不是把同账户其他还没
88    /// confirm 的 stub 一并误清.
89    ///
90    /// 触发场景 (`bridge/dispatcher.rs:251-268`):
91    /// - notice_type=4/5/9 + 非空 `order_ids` (backend 真带 → 按订单清)
92    /// - notice_type=4/5/9 + 空 `order_ids` → fall back to `clear_pending_confirm_for_acc`
93    ///
94    /// match 逻辑: `o.backend_order_id` / `o.order_id_ex` (alphanumeric backend
95    /// szOrderID) 与 `order_ids` 任一相等. 不 match `o.order_id` (FTAPI u64
96    /// hash) 因为 backend push 带的 `order_ids` 是 backend 原生 string id.
97    ///
98    /// 返被清的 order 数 (caller 用于 audit log).
99    pub fn clear_pending_confirm_for_orders(&self, acc_id: u64, order_ids: &[String]) -> usize {
100        if order_ids.is_empty() {
101            return 0;
102        }
103        let mut cleared = 0;
104        if let Some(mut entry) = self.orders.get_mut(&acc_id) {
105            for o in entry.iter_mut() {
106                let decision = decide_pending_confirm_clear_for_order_ids_like_cpp(
107                    PendingConfirmOrderFacts {
108                        is_pending_broker_confirm: o.is_pending_broker_confirm,
109                        order_id_ex: &o.order_id_ex,
110                        backend_order_id: &o.backend_order_id,
111                    },
112                    order_ids,
113                );
114                if decision == PendingConfirmDecision::Clear {
115                    o.is_pending_broker_confirm = false;
116                    cleared += 1;
117                }
118            }
119        }
120        cleared
121    }
122
123    /// v1.4.105 BUG-v1.4.104-001 (P0): cleanup task 删超时未 confirm 的 pending stub.
124    ///
125    /// 触发: PlaceOrder spawn 一个 30s 延迟 task, 到点检查 (acc_id, order_id_ex)
126    /// 对应的 stub 是否仍 `is_stub=true && is_pending_broker_confirm=true`.
127    /// 若是 → 删 stub + warn (push channel 断 / broker 拒单未 push 的兜底).
128    ///
129    /// **不**简单调 LOCAL_STUB_RETENTION_TTL_MS evict — 那个是 query_orders merge 时的逻辑,
130    /// 这里是主动 GC pending stub. 两者互补.
131    ///
132    /// 返 (purged: bool, reason: 描述), caller 写 audit log.
133    pub fn purge_pending_stub_if_still_pending(
134        &self,
135        acc_id: u64,
136        order_id: u64,
137    ) -> Option<String> {
138        if let Some(mut entry) = self.orders.get_mut(&acc_id) {
139            let before = entry.len();
140            let mut purged_code = None;
141            entry.retain(|o| {
142                let decision = decide_pending_stub_purge_for_cleanup(PendingStubPurgeFacts {
143                    requested_order_id: order_id,
144                    cached_order_id: o.order_id,
145                    is_stub: o.is_stub,
146                    is_pending_broker_confirm: o.is_pending_broker_confirm,
147                });
148                if decision == PendingStubPurgeDecision::Purge {
149                    purged_code = Some(o.code.clone());
150                }
151                decision != PendingStubPurgeDecision::Purge
152            });
153            let after = entry.len();
154            if before != after {
155                // v1.4.111 P2-1 Tier 3 audit comment: purged_code 是 audit log
156                // 字段, `Some(empty_string)` 跟 `Some("CODE")` 都表示 "purge 成功",
157                // caller (e.g. post_ack.rs:50-80 stub cleanup) 不基于 code 内容
158                // mutate state. 非 silent-success risk (audit verified).
159                return Some(purged_code.unwrap_or_default());
160            }
161        }
162        None
163    }
164
165    /// v1.4.83 §9 F6: 扫全 cache 查 orphan orders.
166    ///
167    /// **Orphan 定义**: `order_status ∈ {0, 1, 2, 4}` (未达到 Submitted=5
168    /// 之前的 in-flight stub) **且** `create_timestamp.is_some()` **且**
169    /// `now_secs - create_timestamp > threshold_secs`.
170    ///
171    /// 含义对应 C++ proto OrderStatus enum (Trd_Common.proto:108):
172    /// - 0 = Unsubmitted (未提交) — 极端情况, daemon stub 修后不应该出现 (v1.4.103 P0 hotfix)
173    /// - 1 = WaitingSubmit (等待提交) — 条件单 stub 初值, 等触发
174    /// - 2 = Submitting (提交中) — 普通单 stub 初值 (v1.4.103 起)
175    /// - 4 = TimeOut (处理超时) — 后端回 timeout, 状态未知
176    ///
177    /// **为什么需要**: v1.4.82 A2 PlaceOrder 成功后直接 upsert stub order
178    /// 让 `/api/orders` 立刻可见 (BUG-60b0-002 fix). 后续 push notice_type=
179    /// 4/5/8 / re-fetch 把 status 推到 5 (Submitted) / 10/11 (Filled).
180    /// 若 push 通道断流 (§9 CMD3020 chain broken), stub 卡住 5min+ = orphan.
181    ///
182    /// **v1.4.103 P0 (BUG-WUZONG-001)**: stub status 从 0 (proto 定义为
183    /// Unsubmitted "未提交", 触发客户端 retry 多下单) 改成 1/2 (WaitingSubmit/
184    /// Submitting, 对齐 C++ NNProto_Trd_OrderOp.cpp:483-510). orphan 检测同步
185    /// 扩展到 {0, 1, 2, 4} 全 in-flight 状态 — 老 daemon 留下来 status=0 的
186    /// 卡死 stub 也能被检测到.
187    ///
188    /// 返 `Vec<OrphanOrder>`; caller 决定 log 级别 + metric bump.
189    #[must_use]
190    pub fn scan_orphan_orders(&self, now_secs: f64, threshold_secs: f64) -> Vec<OrphanOrder> {
191        let mut orphans = Vec::new();
192        for entry in self.orders.iter() {
193            let acc_id = *entry.key();
194            for order in entry.value().iter() {
195                if let OrphanOrderDecision::Orphan { age_secs } =
196                    decide_orphan_order_like_cpp(OrphanOrderFacts {
197                        is_stub: order.is_stub,
198                        order_status: order.order_status,
199                        create_timestamp: order.create_timestamp,
200                        stub_inserted_at_ms: order.stub_inserted_at_ms,
201                        now_secs,
202                        threshold_secs,
203                    })
204                {
205                    orphans.push(OrphanOrder {
206                        acc_id,
207                        order_id: order.order_id,
208                        order_id_ex: order.order_id_ex.clone(),
209                        code: order.code.clone(),
210                        age_secs,
211                    });
212                };
213            }
214        }
215        orphans
216    }
217}
218
219/// v1.4.83 §9 F6: orphan order 结构化报告.
220#[derive(Debug, Clone)]
221pub struct OrphanOrder {
222    pub acc_id: u64,
223    pub order_id: u64,
224    pub order_id_ex: String,
225    pub code: String,
226    /// 距离 create_timestamp 的秒数
227    pub age_secs: f64,
228}