Skip to main content

futu_cache/trd_cache/
types.rs

1// 交易缓存数据模型。
2
3use futu_core::account_locator::AccountCardRecord;
4
5mod funds;
6pub use funds::*;
7
8/// 账户 key: acc_id
9pub type AccKey = u64;
10
11/// 缓存的账户信息
12#[derive(Debug, Clone, Default)]
13pub struct CachedTrdAcc {
14    /// 账户 ID
15    pub acc_id: u64,
16    /// 后端/mobile native intra account id (C++ `Ndt_Trd_AccItem.nIntraAccID`).
17    ///
18    /// 公开 FTAPI `acc_id` 与 backend 查询 body 里的 `account_id` 不是同一层
19    /// 语义。需要发 backend native account_id 的 handler 应优先用这个字段,
20    /// 不要从公开 `acc_id` 低 32 位反推。
21    pub intra_acc_id: Option<u64>,
22    /// 交易环境(0=Simulate / 1=Real)
23    pub trd_env: i32,
24    /// 该账户有权限访问的交易市场列表
25    pub trd_market_auth_list: Vec<i32>,
26    /// 账户类型(Cash / Margin / Derivative / ...)
27    pub acc_type: Option<i32>,
28    /// 账户卡号(后段数字,仅用于显示识别)
29    pub card_num: Option<String>,
30    /// 账户所属 broker(FutuHK=1 / FutuUS=2 / ...)
31    pub security_firm: Option<i32>,
32    /// 模拟账户子类型
33    pub sim_acc_type: Option<i32>,
34    /// 统一卡号(跨市场账户聚合标识)
35    pub uni_card_num: Option<String>,
36    /// 账户状态码(正常 / 冻结 / ...)
37    pub acc_status: Option<i32>,
38    /// Backend raw `FTUsrTrdAcc::Account.state`.
39    ///
40    /// C++ API layer keeps this distinct from public `TrdAccStatus`:
41    /// `OPENED(1)` is returned as Active, `CLOSED(2)` is returned in the
42    /// disabled-real tail, while `OPENING(0)` is skipped by
43    /// `APIServer_Trd_GetAccList.cpp:109-115`. Do not derive this back from
44    /// `acc_status`, because both CLOSED and OPENING are non-active.
45    pub acc_open_state: Option<i32>,
46    /// 账户角色(主账户 / 子账户 / 顾问)
47    pub acc_role: Option<i32>,
48    /// Daemon-derived user-visible account label.
49    ///
50    /// Some opened business accounts are not representable by
51    /// `Trd_Common.TrdMarket` (for example crypto) or overload protocol role
52    /// values (for example equity-incentive / IPO route). The bridge derives a
53    /// label from backend account metadata and stores it here so public account
54    /// discovery does not rely on numeric market allowlists.
55    pub acc_label: Option<String>,
56    /// 日本账户附加类型标签
57    pub jp_acc_type: Vec<i32>,
58    // --- 以下为审计补全的字段 ---
59    /// 账户所有者 UID
60    pub owner_uid: Option<u64>,
61    /// 账户操作者 UID
62    pub opr_uid: Option<u64>,
63    /// 混合状态 (C++ enAccState / MixedState)
64    pub mixed_state: Option<i32>,
65    /// IRA 类型 (CA: TFSA=1, RRSP=2, SRRSP=3)
66    pub ira_type: Option<i32>,
67    /// 授权状态 (GrantState)
68    pub grant_state: Option<i32>,
69    /// 口座类型 (JP: Cash=1, Margin=2, Derivative=3)
70    pub kouza_type: Option<i32>,
71    /// 交易市场 (Account.market, 单个值)
72    pub trd_market: Option<i32>,
73    /// 关联账户 ID (基金账户绑定)
74    pub association_acc_id: Option<u64>,
75    /// 综合账户子账户标志 (0=非子账户)
76    pub acc_flag: Option<i32>,
77    /// 原始顺序索引 (用于保持后端返回的自然顺序)
78    pub order_index: usize,
79    /// C++ 排序 key: (BrokerID << 48) | (TrdMkt << 32) | IntraAccID
80    pub sort_key: u64,
81}
82
83impl AccountCardRecord for CachedTrdAcc {
84    fn acc_id(&self) -> u64 {
85        self.acc_id
86    }
87
88    fn card_num(&self) -> Option<&str> {
89        self.card_num.as_deref()
90    }
91
92    fn uni_card_num(&self) -> Option<&str> {
93        self.uni_card_num.as_deref()
94    }
95}
96
97impl CachedTrdAcc {
98    /// v1.4.108: identify crypto from bridge-derived backend metadata label.
99    ///
100    /// Account discovery must not hide opened crypto accounts, but `Trd_Common`
101    /// has no public `TrdMarket_Crypto` variant. The bridge therefore derives
102    /// `acc_label=crypto` from `FTUsrTrdAcc.AccountMarket::Crypto` /
103    /// `TradingCapability::NaCrypto`; cache consumers should read that label
104    /// rather than re-hardcoding market numbers.
105    pub fn is_crypto_account(&self) -> bool {
106        self.acc_label.as_deref() == Some("crypto")
107    }
108
109    pub fn is_encrypted(&self) -> bool {
110        self.is_crypto_account()
111    }
112
113    /// v1.4.97 J-Acc-Q3 + v1.4.98 T2-6: derived `acc_label` for
114    /// `/api/accounts` REST response.
115    ///
116    /// **Priority order**:
117    /// 1. bridge-derived backend label (`crypto`, `equity_incentive`,
118    ///    `ipo_route`, ...);
119    /// 2. `"paper_trade"` — `trd_env==0 (Simulate)` (v1.4.98).
120    ///
121    /// Returns `None` for "no special label" (default Margin / regular Cash).
122    ///
123    /// **Spec**: REST-only enrichment (no proto change for gRPC clients —
124    /// gRPC 不看 proto extension field, 只看 /api/accounts REST output).
125    /// Clients should `treat unknown labels as opaque strings` for forward
126    /// compatibility; new labels may appear when backend account categories are
127    /// surfaced through the bridge.
128    ///
129    /// Labels are opaque strings for clients. Unknown labels should be rendered
130    /// as-is rather than treated as an error.
131    pub fn derive_acc_label(&self) -> Option<&str> {
132        if let Some(label) = self.acc_label.as_deref() {
133            return Some(label);
134        }
135        if self.trd_env == 0 {
136            return Some("paper_trade");
137        }
138        None
139    }
140}
141
142/// 缓存的持仓 (对齐 C++ Ndt_Trd_AccPosition 全字段)
143#[derive(Debug, Clone, Default)]
144pub struct CachedPosition {
145    pub position_id: u64,
146    /// Backend business position id used by JP combo close/order paths.
147    ///
148    /// C++ `NNProto_Trd_AccReal.cpp:262-269` stores
149    /// `asset_query.AccPstnInfo.business_position_id` as
150    /// `Ndt_Trd_AccPosition.sBusinessPositionID` and, for FutuJP, exposes
151    /// `Position.positionID = HashStrToU64(sBusinessPositionID)`. Combo
152    /// trade-write paths must reverse that mapping before sending backend
153    /// CMD2297/CMD4701.
154    pub business_position_id: Option<String>,
155    /// C++ `Ndt_Trd_AccPosition.nPositionAccID`; used by JP combo legs as
156    /// backend `pos_account_id`.
157    pub position_acc_id: Option<u64>,
158    /// C++ `Ndt_Trd_AccPosition.nSubAccountID`; used by JP combo legs as
159    /// backend `pos_sub_account_id`.
160    pub sub_account_id: Option<u64>,
161    pub position_side: i32, // 0=多仓, 1=空仓
162    pub code: String,
163    pub name: String,
164    pub qty: f64,
165    pub can_sell_qty: f64,
166    pub price: f64,                      // 当前价
167    pub cost_price: f64,                 // 摊薄成本价
168    pub val: f64,                        // 市值
169    pub pl_val: f64,                     // 盈亏金额
170    pub pl_ratio: Option<f64>,           // 盈亏比例
171    pub sec_market: Option<i32>,         // 证券市场
172    pub td_pl_val: Option<f64>,          // 今日盈亏
173    pub td_trd_val: Option<f64>,         // 今日成交额
174    pub td_buy_val: Option<f64>,         // 今日买入金额
175    pub td_buy_qty: Option<f64>,         // 今日买入数量
176    pub td_sell_val: Option<f64>,        // 今日卖出金额
177    pub td_sell_qty: Option<f64>,        // 今日卖出数量
178    pub unrealized_pl: Option<f64>,      // 未实现盈亏 (期货)
179    pub realized_pl: Option<f64>,        // 已实现盈亏 (期货)
180    pub currency: Option<i32>,           // 货币
181    pub trd_market: Option<i32>,         // 交易市场
182    pub diluted_cost_price: Option<f64>, // 摊薄成本
183    pub average_cost_price: Option<f64>, // 平均成本
184    pub average_pl_ratio: Option<f64>,   // 平均盈亏比例
185    /// C++ 10.7 `Ndt_Trd_AccPosition.nComboIDHash`, projected as
186    /// `Trd_Common.Position.comboID` only for combo summary/leg rows.
187    pub combo_id: Option<u64>,
188    /// Backend asset-system combo id string (`Ndt_Trd_AccPosition.sComboIDSvr`).
189    ///
190    /// Public `comboID` is a hash, but C++ JP combo close/order paths write the
191    /// original string back to backend `OrderNewReq.combo_id`; keep both
192    /// representations so public projection and backend write paths do not
193    /// fight each other.
194    pub business_combo_id: Option<String>,
195    /// C++ 10.7 option strategy type after backend `combo_identify` ->
196    /// NN -> public `Qot_Common.OptionStrategyType` mapping.
197    pub strategy_type: Option<i32>,
198    /// C++ 10.7 `NN_PositionType`, projected as public
199    /// `Trd_Common.PositionType` (`Combined=1`, `Leg=2`).
200    pub position_type: Option<i32>,
201    /// C++ 10.7 position account id. JP sub-account rows may use a different
202    /// long account id; otherwise the handler falls back to request acc_id.
203    pub acc_id: Option<u64>,
204    /// C++ 10.7 JP sub-account type.
205    pub jp_acc_type: Option<i32>,
206    /// v1.4.42 (external reviewer v1.4.40 roadmap #2 + P2.3): 期权持仓到期日距今天数。
207    /// backend 不返,daemon 按 code 推导(option code 才有值)。
208    pub expiry_date_distance: Option<i32>,
209}
210
211/// 缓存的订单 (对齐 C++ Ndt_Trd_Order 全字段)
212#[derive(Debug, Clone, Default)]
213pub struct CachedOrder {
214    pub order_id: u64,
215    pub order_id_ex: String, // 服务端订单 ID 字符串
216    pub code: String,
217    pub name: String,
218    pub trd_side: i32,
219    pub order_type: i32,
220    pub order_status: i32,
221    pub qty: f64,
222    pub price: f64,
223    pub fill_qty: f64,
224    pub fill_avg_price: f64,
225    pub create_time: String,
226    pub update_time: String,
227    pub last_err_msg: Option<String>,   // 最后错误信息
228    pub sec_market: Option<i32>,        // 证券市场
229    pub create_timestamp: Option<f64>,  // 创建时间戳
230    pub update_timestamp: Option<f64>,  // 更新时间戳
231    pub remark: Option<String>,         // 备注
232    pub time_in_force: Option<i32>,     // 有效期类型
233    pub fill_outside_rth: Option<bool>, // 是否允许盘前盘后成交
234    pub aux_price: Option<f64>,         // 触发价格
235    pub trail_type: Option<i32>,        // 跟踪类型
236    pub trail_value: Option<f64>,       // 跟踪值
237    pub trail_spread: Option<f64>,      // 跟踪价差
238    pub currency: Option<i32>,          // 货币
239    pub trd_market: Option<i32>,        // 交易市场
240
241    /// C++ `Ndt_Trd_Order.enOptionStgyType`, projected in
242    /// `OrderData_NNToAPI` as public `Order.strategyType` and used to override
243    /// combo order display name (for example "垂直策略") at response time.
244    pub strategy_type: Option<i32>,
245    /// C++ `INNData_Trd_Order::{GetOrderComboLegList,GetHistOrderComboLegList}`
246    /// equivalent for current-order cache rows. History reads project directly
247    /// from backend `multi_leg_info`; current-order reads must carry the same
248    /// data through cache so `order_list_query` and `history_order_list_query`
249    /// expose identical combo semantics.
250    pub combo_legs: Vec<futu_proto::qot_common::ComboLeg>,
251
252    /// v1.4.106 codex 0219 Finding 4 / 0226 F7: backend snapshot 字段集合.
253    ///
254    /// PlaceOrder ack 后 backend 返 `OrderNewRsp.order_id` (= `szOrderID`,
255    /// 服务端真实订单 id, alphanumeric 字符串). FTAPI `Trd_PlaceOrder.S2C.order_id`
256    /// 是这个 string 的 hash (`HashStrToU64` 结果, 见 `trade_query::hash_str_to_u64`).
257    ///
258    /// **必填语义**: ModifyOrder / CancelOrder backend req 的 `order_id` 字段
259    /// **必须**填 backend `szOrderID`, 不是 hash. 反模式 (v1.4.105 及以前):
260    /// 没有 orderIDEx 时直接 `order_id_ex.parse().unwrap_or(0)` 把 hash 当
261    /// backend id 发 — backend 拒错或匹配失败.
262    ///
263    /// 修法 (v1.4.106 Finding 1+4): cache 存 `backend_order_id` (= szOrderID)
264    /// 字段; trade-write handler 通过 `find_order_for_trade_write` lookup 拿到
265    /// `ResolvedOrderContext { backend_order_id, version, exchange, exchange_code,
266    /// security_type, ... }` 后再发 backend req.
267    pub backend_order_id: String,
268
269    /// v1.4.106 codex 0219 Finding 4: backend `Order.version` (proto field 21).
270    ///
271    /// ModifyOrder backend `OrderReplaceReq.order_version` 必填 — 让 backend
272    /// 能拒接收已经被其他客户端改过版本的旧请求. C++ `FillModifyOrderReq:736`:
273    /// `req.set_order_version((u32_t)order.nVersion)`.
274    pub order_version: i32,
275
276    /// v1.4.106 codex 0219 Finding 4: backend `Order.exchange_code` (proto field 37).
277    ///
278    /// 期货所属交易所代码 (e.g. `1` = HKEX, `2` = NYSE 之类, 取值参考
279    /// `NN_QotMarket`). ModifyOrder / CancelOrder backend 必填 (期货必填,
280    /// 股票为 0). C++ `FillModifyOrderReq:773`:
281    /// `req.set_exchange_code((u32_t)order.enMktID)`.
282    pub exchange_code: i32,
283
284    /// v1.4.106 codex 0219 Finding 4: backend `Order.exchange` (proto field 49).
285    ///
286    /// 股票所属交易所字符串 (e.g. "SEHK", "NYSE", "NASDAQ"). ModifyOrder /
287    /// CancelOrder backend 必填. C++ `FillModifyOrderReq:774`:
288    /// `req.set_exchange(order.szExchange)`.
289    pub exchange: String,
290
291    /// v1.4.106 codex 0219 Finding 4: backend `Order.security_type` (proto field 29).
292    ///
293    /// 取值参考 backend `odr_sys_cmn::SecurityType`
294    /// (1=COMMON, 2=OPTION, 4=FUTURES, 5=BOND).
295    /// CancelOrder single 必填 (`req.add_security_type(GetSecurityType(...))`,
296    /// C++ `FillCancelOrderReq:817`).
297    pub security_type: i32,
298
299    /// v1.4.98 T1-8 (mobile-source-audit): 美股盘前/盘中/盘后 session 标识.
300    /// proto/Trd_Common.proto:455 字段 27. 同 time_in_force / fill_outside_rth
301    /// 系列, 美股 RTH/Pre-Market/After-Hours order routing 显示用.
302    pub session: Option<i32>,
303
304    /// Backend `odr_sys_cmn.Order.order_trade_time_type` / C++ `order.enOrderTradeTimeType`.
305    ///
306    /// `GetMaxTrdQtys` real option IM side request (CMD5004) mirrors C++
307    /// `NNProto_Trd_MaxQty::QueryOptionIM`: when querying max qty for a
308    /// modification order, the side request forwards the cached backend order's
309    /// trade-time type if it is not UNSET.
310    pub order_trade_time_type: Option<u32>,
311
312    /// v1.4.98 T1-8 (mobile-source-audit): 日本子账户类型 (security_firm=7
313    /// FutuJP 时填充). proto/Trd_Common.proto:456 字段 28.
314    /// 8 enum 值: GENERAL/TOKUTEI/NISA_GENERAL/NISA_TSUMITATE 等
315    /// (per docs/reference/rest-api.md D6).
316    pub jp_acc_type: Option<i32>,
317
318    /// v1.4.90 S BUG-e4da-009: stub 标志。
319    ///
320    /// PlaceOrder/CancelOrder handler 成功响应后**立刻** upsert 一个 stub
321    /// `CachedOrder`(v1.4.82 A2 / `place_order.rs:427`),让 `/api/orders`
322    /// 0ms 可见。`is_stub=true` 标记此条尚未被 backend 权威列表 ack。
323    ///
324    /// 后续 backend `query_orders` 返回包含同 `order_id` 的 enriched 数据时,
325    /// 经 `merge_preserving_stubs` 合并 → `is_stub=false`。
326    ///
327    /// 历史坑:v1.4.73 A1 PlaceOrder 后 spawn refresh 直接 `orders.insert`
328    /// **整覆盖**,把刚 upsert 的 stub 抹掉(race 22ms 内即清零)。
329    /// 跨 v1.4.73 → v1.4.89 7 版未真修。本字段是根因修法的一部分。
330    pub is_stub: bool,
331
332    /// C++ `Ndt_Trd_Order.bIsLocalOrder` equivalent.
333    ///
334    /// C++ `UpdateOrderList` replaces the backend list, then re-inserts rows
335    /// returned by `INNData_Trd_Order::GetLocalOrderList` where
336    /// `bIsLocalOrder=true`.
337    ///
338    /// Refs:
339    /// - `FutuOpenD/Src/NNProtoCenter/Trade/_NNProto_Trd_Comm.cpp:345-359`
340    /// - `FutuOpenD/Src/NNDataCenter/Trade/INNData_Trd_Order.cpp:13-31`
341    /// - `FutuOpenD/Src/NNProtoCenter/Trade/Operate/NNProto_Trd_OrderOpBase.cpp:95`
342    ///
343    /// Rust uses this narrowly for local terminal rows that C++ keeps visible
344    /// after backend refresh, notably DeleteFailOrder success -> Deleted(23).
345    /// It is intentionally distinct from `is_stub`: stubs expire by TTL, while
346    /// local Deleted rows are C++ list state and must survive an empty backend
347    /// refresh until a backend row with the same id replaces them.
348    pub is_local_order: bool,
349
350    /// v1.4.90 S BUG-e4da-009: stub 插入时间(unix epoch ms)。
351    ///
352    /// `merge_preserving_stubs` 用于 evict 老 stub:backend 如果连续多次
353    /// 不返某 stub `order_id` 且 stub 已超过 `STUB_TTL_MS` (30s) → evict。
354    /// 防止 stub 因 backend 拒单(never appear in list)永久滞留。
355    ///
356    /// `0` 表示非 stub(与 `is_stub=false` 配套)。
357    pub stub_inserted_at_ms: u64,
358
359    /// v1.4.105 BUG-v1.4.104-001 (P0): broker 异步 confirm 标志.
360    ///
361    /// PlaceOrder backend 同步 ack (CMD 4701 result=0) 仅说明 backend 收到请求,
362    /// **不**代表 broker 真的接受订单. C++ `OnOMEvent_Reply_PlaceOrder`
363    /// (`APIServer_Trd_PlaceOrder.cpp:794`) 是**异步 event handler** — broker
364    /// 真 confirm 后才 fire `set_orderid(nOrderIDHash)`. backend `OrderNewRsp.
365    /// need_op_confirm` (proto field 6, default=true) 即表示 "真 broker confirm
366    /// 还在路上, 等 OMEvent push (notice_type 4/5/8/100)".
367    ///
368    /// **历史坑 (external reviewer BUG-v1.4.104-001 实锤)**: v1.4.82-104 stub 上 cache 时直接
369    /// 视为已确认, 没有 broker async confirm 等待 → 三次不同订单返同 order_id_ex
370    /// 时客户端看 success → 误以为生效 → 加仓重下 → 风控 auto-cancel error 10003.
371    ///
372    /// **语义**:
373    /// - `true`: backend 已 ack 但 broker 未确认 — `is_stub=true` 时 stub 不进
374    ///   `/api/orders` 响应 (filter), 等 push notice_type=4/5/8/100 confirm 后翻
375    ///   `false` 才 expose. 30s 内未 confirm → cleanup task 删 stub + warn.
376    /// - `false`: backend 权威 / broker 已 confirm — 正常 expose 给 client.
377    ///   query_orders 返的所有 order 都是 `false` (backend list 是 broker-confirmed
378    ///   的权威列表).
379    ///
380    /// **与 `is_stub` 关系**:
381    /// - `is_stub=true && is_pending_broker_confirm=true`: 刚 PlaceOrder ack, 还没
382    ///   push confirm. **client 不可见** (Layer 4 filter).
383    /// - `is_stub=true && is_pending_broker_confirm=false`: backend `need_op_confirm=
384    ///   false` 路径 (sim 账户 / 立即生效场景). client 可见.
385    /// - `is_stub=false`: backend authoritative (query_orders 返 / push merge 后).
386    ///   `is_pending_broker_confirm` 必为 false. client 可见.
387    pub is_pending_broker_confirm: bool,
388}
389
390/// v1.4.106 codex 0219 Finding 1+4: trade-write resolution snapshot.
391///
392/// 用于 ModifyOrder / CancelOrder handler 把 cached order 字段一次性
393/// 拿出来构造 backend req. **不在 hot path 改 CachedOrder**, 而是返一个
394/// 只读 snapshot 防 caller mutate cache.
395///
396/// 字段 1:1 映射 backend `OrderReplaceReq` / `OrderCancelReq` 必填项 +
397/// modify validation 用到的 (`order_type` / `trd_side` / `qty` /
398/// `price` / `order_status`).
399#[derive(Debug, Clone, Default, PartialEq)]
400pub struct CachedOrderSnapshot {
401    /// = backend `szOrderID` (alphanumeric 字符串, 服务端真实 id).
402    pub backend_order_id: String,
403    /// = backend `Order.version`.
404    pub order_version: i32,
405    /// = backend `Order.exchange_code` (期货所属交易所代码, e.g. NN_QotMarket).
406    pub exchange_code: i32,
407    /// = backend `Order.exchange` (股票所属交易所字符串, e.g. "SEHK").
408    pub exchange: String,
409    /// = backend `Order.security_type` (1=COMMON, 2=OPTION, 4=FUTURES, 5=BOND).
410    pub security_type: i32,
411    /// FTAPI `OrderType` (modify validation 按原 order_type 决定 price /
412    /// aux_price / trail* 是否必填).
413    pub order_type: i32,
414    /// FTAPI `TrdSide` (1=Buy / 2=Sell / 3=SellShort / 4=BuyBack).
415    /// trailing modify 计算 sign 用.
416    pub trd_side: i32,
417    /// FTAPI `OrderStatus` (`IsNotSupportOrderOp` 检查用).
418    pub order_status: i32,
419    /// FTAPI `TrdMarket` (modify validation 中 sec_type futures 路径用).
420    pub trd_market: Option<i32>,
421    /// FTAPI `Order.qty` (历史值, 仅 modify validation 引用).
422    pub qty: f64,
423    /// FTAPI `Order.price` (历史值, 仅 modify validation 引用).
424    pub price: f64,
425    /// FTAPI `Order.code` (用于错误提示 + log 关联).
426    pub code: String,
427    /// PlaceOrder stub 标记. PlaceOrder ack 时插入的 stub `is_stub=true` +
428    /// `order_version=0`. ModifyOrder 按 C++ 本地回显订单 `nVersion=-1`
429    /// 映射为 backend wire `u32::MAX`; CancelOrder 不依赖 order_version.
430    pub is_stub: bool,
431    /// PlaceOrder stub 是否仍在等待 broker/backend 权威确认.
432    ///
433    /// Real write handlers use this to avoid reporting success against a local
434    /// optimistic echo when the authoritative order list has not accepted the
435    /// order yet.
436    pub is_pending_broker_confirm: bool,
437}
438
439impl CachedOrderSnapshot {
440    /// 从 `CachedOrder` 抽出 trade-write resolution 用到的字段子集.
441    pub fn from_order(o: &CachedOrder) -> Self {
442        Self {
443            backend_order_id: o.backend_order_id.clone(),
444            order_version: o.order_version,
445            exchange_code: o.exchange_code,
446            exchange: o.exchange.clone(),
447            security_type: o.security_type,
448            order_type: o.order_type,
449            trd_side: o.trd_side,
450            order_status: o.order_status,
451            trd_market: o.trd_market,
452            qty: o.qty,
453            price: o.price,
454            code: o.code.clone(),
455            is_stub: o.is_stub,
456            is_pending_broker_confirm: o.is_pending_broker_confirm,
457        }
458    }
459}
460
461/// v1.4.106 codex 0219 Finding 1: ResolveOrderError 区分三种 cache miss 形态.
462#[derive(Debug, Clone, PartialEq, Eq)]
463pub enum ResolveOrderError {
464    /// 同时缺 `order_id` 和 `order_id_ex`. caller 必须早期 reject.
465    InvalidInput,
466    /// `(acc_id, order_id)` 在 cache 找不到. 提示用户先刷新 `/api/orders`
467    /// 或传 orderIDEx.
468    CacheMiss,
469    /// cache 命中但 `backend_order_id` 字段空 (老版本 cache entry).
470    /// 提示用户先刷新 `/api/orders` 或传 orderIDEx.
471    MissingBackendId,
472}
473
474impl std::fmt::Display for ResolveOrderError {
475    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
476        match self {
477            Self::InvalidInput => f.write_str(
478                "modify/cancel: 必须传 order_id 或 order_id_ex (orderIDEx 优先)",
479            ),
480            Self::CacheMiss => f.write_str(
481                "modify/cancel: 订单不在本地缓存. 请先调 /api/orders 刷新, 或在请求里传 orderIDEx (= backend szOrderID).",
482            ),
483            Self::MissingBackendId => f.write_str(
484                "modify/cancel: 本地缓存缺 backend orderIDEx (老版本 daemon 写入的 cache). 请先调 /api/orders 刷新, 或在请求里传 orderIDEx.",
485            ),
486        }
487    }
488}
489
490impl std::error::Error for ResolveOrderError {}
491
492/// **v1.4.106 Finding A** (codex source audit 2026-05-01): funds cache currency-aware key.
493///
494/// 对齐 C++ `INNData_Trd_Acc.cpp::m_mapAccFund`:
495///   `m_mapAccFund: NN_AssetKey -> NN_TrdCurrency -> Ndt_Trd_AccFund`
496///
497/// Universal/Futures 账户对**不同 currency** 有独立 funds snapshot, 之前 Rust
498/// 用 `DashMap<AccKey, CachedFunds>` (1 acc_id → 1 snapshot) 会被 backend
499/// pushed snapshots **互相覆盖** — 用户传 `currency=USD` 拿到的可能是 stale
500/// CAD 数据, 客户端无法察觉.
501///
502/// 字段语义:
503/// - `acc_id`: 账户 (主 key)
504/// - `asset_category`: `Trd_Common.proto::AssetCategory` enum (0=Default 等),
505///   对齐 C++ NN_AssetKey 子集. 若 client 传 `c2s.asset_category=None`, 用 0.
506/// - `currency`: `Some(c)` 表示 per-currency snapshot (Futures/Universal 路径);
507///   `None` 表示 legacy 单币种账户 native (无 per-currency 概念). C++ 等价于
508///   "first available currency" snapshot.
509#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
510pub struct FundsCacheKey {
511    pub acc_id: u64,
512    pub asset_category: i32,
513    pub currency: Option<i32>,
514}
515
516impl FundsCacheKey {
517    /// Legacy single-account snapshot key (acc_id only, no per-currency / no
518    /// per-asset_category dimension). 用于 SingleCurrency 账户 / 无 currency
519    /// context 的 cache write.
520    #[must_use]
521    pub const fn legacy(acc_id: u64) -> Self {
522        Self {
523            acc_id,
524            asset_category: 0,
525            currency: None,
526        }
527    }
528
529    /// Per-currency snapshot key (Universal/Futures 路径).
530    #[must_use]
531    pub const fn per_currency(acc_id: u64, currency: i32) -> Self {
532        Self {
533            acc_id,
534            asset_category: 0,
535            currency: Some(currency),
536        }
537    }
538
539    /// Per-asset-category + per-currency snapshot key (full path, asset_category
540    /// 非 0 时用).
541    #[must_use]
542    pub const fn full(acc_id: u64, asset_category: i32, currency: Option<i32>) -> Self {
543        Self {
544            acc_id,
545            asset_category,
546            currency,
547        }
548    }
549}
550
551/// **v1.4.107 PositionList asset-category key**.
552///
553/// C++ `APIServer_Trd_GetPositionList.cpp::FillPositionList` reads positions
554/// by `NN_AssetKey { accid, enCategory }`. FutuJP margin / derivative accounts
555/// therefore need independent position snapshots per asset category, just like
556/// funds. Category 0 keeps the legacy single-bucket behavior for non-JP and sim
557/// accounts.
558#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
559pub struct PositionsCacheKey {
560    pub acc_id: u64,
561    pub asset_category: i32,
562}
563
564impl PositionsCacheKey {
565    #[must_use]
566    pub const fn legacy(acc_id: u64) -> Self {
567        Self {
568            acc_id,
569            asset_category: 0,
570        }
571    }
572
573    #[must_use]
574    pub const fn scoped(acc_id: u64, asset_category: i32) -> Self {
575        Self {
576            acc_id,
577            asset_category,
578        }
579    }
580}
581
582/// v1.4.106 codex 0226 F1+F2: PlaceOrder 阶段 backend 返回 `OrderNewRsp.action`
583/// (CltAction.type == ORDER_CONFIRM=5) 时, 携带 `CltActionOrderConfirm` 二次
584/// 确认上下文 — 客户端需把这些字段透传给 backend `OrderConfirmReq` (cmd 4728).
585///
586/// 来源 (proto-internal/odr_sys_cmn.proto:883-893):
587/// ```text
588/// message CltActionOrderConfirm {
589///     optional string order_id = 1;             // 订单 id (= backend 真实
590///                                                //  szOrderID, alphanumeric)
591///     optional string title = 2;                // 弹窗标题文案
592///     optional string content = 3;              // 弹窗内容文案
593///     optional string confirm_button_title = 4;
594///     optional string cancel_button_title = 5;
595///     optional uint32 confirm_type = 6;         // 必传给 OrderConfirmReq
596///     optional uint32 exchange_code = 7;        // 期货上游交易所代码
597///     optional string exchange = 8;             // 股票所属交易所
598/// }
599/// ```
600///
601/// daemon 在 PlaceOrder ack 路径里 capture 此 context, 然后在
602/// `Trd_ReconfirmOrder` 收到客户端确认请求时按 (acc_id, ftapi_order_id) 取出来
603/// 构造 backend `OrderConfirmReq`. 缺 context = backend 不需要二次确认 (e.g.
604/// HK 高买低卖未触发 / sim 路径) → ReconfirmOrder handler 早 reject loud.
605///
606/// **来源 cmd_id**: `CS_CMDID_TRADE_ORDER_CONFIRM_ORDER = 4728`
607/// (`moomoo/Moomoo/Include/FTTrade/TradeCmdDefine.h:132`).
608#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
609pub struct OrderConfirmContext {
610    /// backend 真实 szOrderID (alphanumeric); 写到 `OrderConfirmReq.order_id`.
611    pub backend_order_id: String,
612    /// `CltActionOrderConfirm.confirm_type` (来自 OrderConfirmType enum).
613    /// **必传** (backend 用此值校验客户端确实看到了对应弹窗).
614    pub confirm_type: u32,
615    /// `CltActionOrderConfirm.exchange_code` (期货必传).
616    pub exchange_code: u32,
617    /// `CltActionOrderConfirm.exchange` (股票必传).
618    pub exchange: String,
619    /// 弹窗 title (用于 daemon 日志, 客户端可参考).
620    pub title: String,
621    /// 弹窗 content (用于 daemon 日志, 客户端可参考).
622    pub content: String,
623    /// confirm_button / cancel_button title 透传 (UX, 不影响 backend).
624    pub confirm_button_title: String,
625    pub cancel_button_title: String,
626    /// 写入时间 (unix epoch ms), TTL 用. PlaceOrder 与 ReconfirmOrder 之间通常
627    /// <60s, 远超 60s 视为 stale → handler 拒绝.
628    pub inserted_at_ms: u64,
629}
630
631/// v1.4.106 codex 0226 F1+F2: pending OrderConfirm cache key.
632///
633/// `(acc_id, ftapi_order_id)` 而不是 `(acc_id, backend_order_id)`, 因为 FTAPI
634/// 客户端发 ReconfirmOrder 用的是 PlaceOrder 返的 `s2c.order_id` (FTAPI u64,
635/// 由 `hash_backend_id_to_u64` 派生). FTAPI 客户端**不**直接看到 backend
636/// alphanumeric szOrderID; daemon 必须按 FTAPI order_id 反查 context.
637#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
638pub struct OrderConfirmKey {
639    pub acc_id: u64,
640    /// FTAPI order_id (= `hash_backend_id_to_u64(backend.order_id)`).
641    pub ftapi_order_id: u64,
642}
643
644impl OrderConfirmKey {
645    pub fn new(acc_id: u64, ftapi_order_id: u64) -> Self {
646        Self {
647            acc_id,
648            ftapi_order_id,
649        }
650    }
651}
652
653/// C++ `m_mapReqIDOrderID` key, scoped by account to avoid cross-account
654/// collisions in the shared Rust trade cache.
655#[derive(Debug, Clone, PartialEq, Eq, Hash)]
656pub struct OrderOpReqKey {
657    pub acc_id: u64,
658    pub req_id: String,
659}
660
661impl OrderOpReqKey {
662    pub fn new(acc_id: u64, req_id: String) -> Self {
663        Self { acc_id, req_id }
664    }
665}
666
667/// C++ `FindOrderIDByReqID` clears the helper map once it reaches 512 entries.
668pub const ORDER_OP_REQ_ORDER_MAP_CLEAR_LIMIT: usize = 512;
669
670/// v1.4.106 codex 0226 F1+F2: pending order confirm context TTL (ms).
671///
672/// PlaceOrder → ReconfirmOrder 通常 <60s (用户看到弹窗后立即点确认). 超过 60s
673/// 视为弹窗已被忽略 / 用户 abandon → daemon 不允许 reconfirm (backend 也会拒绝
674/// 因为 confirm_type 已过期). 当前选 5min (300_000 ms) 作宽松 TTL — 给真机用户
675/// 更多缓冲, 同时防内存泄漏.
676pub const ORDER_CONFIRM_CONTEXT_TTL_MS: u64 = 300_000;