futu_cache/qot_cache/order_book_merge.rs
1use std::collections::HashMap;
2
3use super::{CachedOrderBook, CachedOrderBookDetail, CachedOrderBookLevel};
4
5// =============================================================================
6// v1.4.110 codex QOT Phase 4 Slice 7: MergeMultipleOrderBookCaches pure fn.
7//
8// 对齐 C++ `QotRealTimeData::MergeMultipleOrderBookCaches` + `MergeOrderBookLists`
9// (QotRealTimeData.cpp:1440-1591):
10//
11// 1. **Per-side price-key merge** — 每档按 `price * 1e9` 取整作 key, 同 key
12// 累加 volume / order_count / detail_list (C++ MergedGear struct).
13// 2. **Side-aware sort** — bid 降序 (高价先), ask 升序 (低价先).
14// 3. **Top N truncate** — 仅当 `max_depth > 0` 时截断 (crypto LV2 用 40).
15// 4. **Timestamp**: bid/ask 各自取多 source 的最大 timestamp.
16// 5. **Skip price ≤ 0**: C++ "过滤空数据占位条目" (line 1466).
17// =============================================================================
18
19/// Per-side merge buffer (对齐 C++ `MergedGear`).
20#[derive(Debug, Clone)]
21struct MergedGear {
22 price: f64,
23 volume: i64,
24 order_count: i32,
25 detail_list: Vec<CachedOrderBookDetail>,
26 /// v1.4.110 codex audit Round4 R4-4: 高精度量累加器. 对齐 C++
27 /// `gear.dVolume += has_hpvolume() ? hpvolume() : volume()` —— 累加 de-scale
28 /// 后的真实量 (有 hp_volume 用之, 否则 fallback `volume`).
29 hp_volume: f64,
30}
31
32/// Per-side merge: take multiple lists, aggregate by price key, sort, optionally
33/// truncate to `max_depth`. 不会修改输入.
34///
35/// `is_bid=true` → sort 降序; `false` → 升序.
36fn merge_order_book_side(
37 sources: &[&[CachedOrderBookLevel]],
38 is_bid: bool,
39 max_depth: usize,
40) -> Vec<CachedOrderBookLevel> {
41 if sources.is_empty() {
42 return vec![];
43 }
44 // 价格精度放大到整数 (9 位小数, 对齐 C++ `PriceToKey`).
45 fn price_to_key(p: f64) -> i64 {
46 (p * 1e9 + 0.5) as i64
47 }
48
49 let mut merged: HashMap<i64, MergedGear> = HashMap::new();
50 for list in sources {
51 for level in list.iter() {
52 // C++: 过滤空数据占位条目 (price <= 0).
53 if level.price <= 0.0 {
54 continue;
55 }
56 let key = price_to_key(level.price);
57 let entry = merged.entry(key).or_insert(MergedGear {
58 price: level.price,
59 volume: 0,
60 order_count: 0,
61 detail_list: vec![],
62 hp_volume: 0.0,
63 });
64 entry.volume = entry.volume.saturating_add(level.volume);
65 entry.order_count = entry.order_count.saturating_add(level.order_count);
66 entry.detail_list.extend(level.detail_list.iter().cloned());
67 // v1.4.110 codex audit Round4 R4-4: 对齐 C++ merge gear.dVolume 累加 ——
68 // 有 hp_volume 用真实小数量, 否则 fallback `volume`.
69 entry.hp_volume += level.hp_volume.unwrap_or(level.volume as f64);
70 }
71 }
72
73 let mut keys: Vec<i64> = merged.keys().copied().collect();
74 if is_bid {
75 // 买盘: 价格从高到低 (降序).
76 keys.sort_by(|a, b| b.cmp(a));
77 } else {
78 // 卖盘: 价格从低到高 (升序).
79 keys.sort();
80 }
81
82 let mut out: Vec<CachedOrderBookLevel> = keys
83 .iter()
84 .map(|k| {
85 let g = &merged[k];
86 CachedOrderBookLevel {
87 price: g.price,
88 volume: g.volume,
89 order_count: g.order_count,
90 detail_list: g.detail_list.clone(),
91 // v1.4.110 codex audit Round4 R4-4: merge 输出 crypto 多交易所
92 // 合单盘口, 始终带累加后的 hp_volume (对齐 C++ set_hpvolume).
93 hp_volume: Some(g.hp_volume),
94 }
95 })
96 .collect();
97
98 // 深度截断: max_depth > 0 时生效.
99 if max_depth > 0 && out.len() > max_depth {
100 out.truncate(max_depth);
101 }
102 out
103}
104
105/// Merge N exchange-level orderbook caches into one broker-level cache.
106///
107/// 对齐 C++ `QotRealTimeData::MergeMultipleOrderBookCaches(vSources, pbMerged,
108/// nMaxDepth=40)` (QotRealTimeData.cpp:1534-1591):
109/// - 第一个 source 作为基础 (拷 svr_recv_time 等基本字段, 但 bid/ask list 重算)
110/// - 按 price key 聚合 bid/ask, 同 price 累加 volume + order_count + detail
111/// - bid 高价优先, ask 低价优先, 截断到 `max_depth`
112/// - bid/ask timestamp 取多 source 最大值
113///
114/// `max_depth = 0` → 不截断 (C++ 行为, line 1579-1583 `if nMaxDepth > 0`).
115/// `max_depth = 40` → crypto LV2 broker-level cache 用 (line 1014).
116pub fn merge_multiple_order_book_caches(
117 sources: &[CachedOrderBook],
118 max_depth: usize,
119) -> CachedOrderBook {
120 if sources.is_empty() {
121 return CachedOrderBook::default();
122 }
123
124 // v1.4.110 codex audit Round2 P2 #17: 收 slice-of-slice 而非 deep clone.
125 // merge_order_book_side 只读 levels (price/volume/order_count/detail_list),
126 // 不消费 Vec — 之前 `.bid_list.clone()` 每 source deep clone N×M levels
127 // (5 exchange × 40 level = 200 clone per LV2 push), crypto LV2 push 1Hz+
128 // 频率下浪费. 改 `.as_slice()` 只收引用, 零 Level clone.
129 let bid_sources: Vec<&[CachedOrderBookLevel]> =
130 sources.iter().map(|s| s.bid_list.as_slice()).collect();
131 let ask_sources: Vec<&[CachedOrderBookLevel]> =
132 sources.iter().map(|s| s.ask_list.as_slice()).collect();
133
134 let bid_merged = merge_order_book_side(&bid_sources, true, max_depth);
135 let ask_merged = merge_order_book_side(&ask_sources, false, max_depth);
136
137 // Timestamp 取所有 source 最大 (对齐 C++ line 1572-1575).
138 let max_bid_ts = sources
139 .iter()
140 .filter_map(|s| s.svr_recv_time_bid_timestamp)
141 .fold(None::<f64>, |acc, t| {
142 Some(acc.map(|a| a.max(t)).unwrap_or(t))
143 });
144 let max_ask_ts = sources
145 .iter()
146 .filter_map(|s| s.svr_recv_time_ask_timestamp)
147 .fold(None::<f64>, |acc, t| {
148 Some(acc.map(|a| a.max(t)).unwrap_or(t))
149 });
150
151 CachedOrderBook {
152 bid_list: bid_merged,
153 ask_list: ask_merged,
154 svr_recv_time_bid: None,
155 svr_recv_time_bid_timestamp: max_bid_ts,
156 svr_recv_time_ask: None,
157 svr_recv_time_ask_timestamp: max_ask_ts,
158 }
159}