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}