Skip to main content

futu_cache/qot_right/
backend_apply.rs

1//! Split from qot_right.rs: backend / push apply path.
2//!
3//! CMD 6024 full apply, quota-only apply, CMD 6651 / 6006 / direct-auth push
4//! patches, and the shared `apply_backend_projection` field writer.
5
6use super::*;
7
8impl QotRightCache {
9    pub fn apply_6651_changes(&self, items: &[(i32, i32, i32)]) -> Vec<i32> {
10        let mut d = self.data.write();
11        let mut changed_types = Vec::new();
12
13        for &(quote_type, before, after) in items {
14            let current = QotRightPushCurrentFacts {
15                api_us_qot_right: d.api_us_qot_right,
16                us_lv2_arca: d.us_lv2_arca_qot_right,
17                us_lv2_nyse: d.us_lv2_nyse_qot_right,
18                us_lv2_nasdaq_totalview: d.us_lv2_nasdaq_totalview_qot_right,
19            };
20            if let Some(projection) = project_qot_right_6651_change_with_current(
21                QotRight6651Change {
22                    quote_type,
23                    before,
24                    after,
25                },
26                current,
27            ) {
28                apply_backend_projection(&mut d, projection.patch);
29                changed_types.push(projection.quote_type);
30            }
31        }
32        if !changed_types.is_empty() {
33            drop(d);
34            let mut m = self.meta.lock();
35            m.freshness = qot_right_after_push_changes(m.freshness.clone(), true);
36            if m.freshness.is_fresh() {
37                m.last_refresh_at_ms = now_ms();
38            }
39        }
40        changed_types
41    }
42
43    pub fn apply_direct_auth_changes(&self, items: &[(i32, u32)]) -> Vec<i32> {
44        let mut d = self.data.write();
45        let mut changed_types = Vec::new();
46
47        for &(quote_type, after) in items {
48            if let Some(projection) =
49                project_qot_right_direct_auth_change(QotRightDirectAuthChange { quote_type, after })
50            {
51                apply_backend_projection(&mut d, projection.patch);
52                changed_types.push(projection.quote_type);
53            }
54        }
55        if !changed_types.is_empty() {
56            drop(d);
57            let mut m = self.meta.lock();
58            m.freshness = qot_right_after_push_changes(m.freshness.clone(), true);
59            if m.freshness.is_fresh() {
60                m.last_refresh_at_ms = now_ms();
61            }
62        }
63        changed_types
64    }
65
66    /// Applies one legacy CMD6006 partial authorization push without
67    /// reclassifying it as a full CMD6024 snapshot.
68    ///
69    /// A current route may update an unowned cold cache (provenance remains
70    /// `None`) or the full snapshot produced by that same publication epoch.
71    /// It may never graft a new-route push onto facts owned by another epoch.
72    pub fn apply_6006_auth_changes_if_source(
73        &self,
74        expected_generation: u64,
75        source_publication_epoch: u64,
76        items: &[(i32, u32)],
77        cn_after: Option<u32>,
78    ) -> Option<Vec<i32>> {
79        let mut data = self.data.write();
80        let mut meta = self.meta.lock();
81        if meta.backend_generation != expected_generation
82            || meta
83                .source_publication_epoch
84                .is_some_and(|epoch| epoch != source_publication_epoch)
85        {
86            return None;
87        }
88
89        let mut changed_types = Vec::new();
90        for &(quote_type, after) in items {
91            if let Some(projection) =
92                project_qot_right_direct_auth_change(QotRightDirectAuthChange { quote_type, after })
93            {
94                apply_backend_projection(&mut data, projection.patch);
95                changed_types.push(projection.quote_type);
96            }
97        }
98        if let Some(cn_after) = cn_after {
99            let projection = project_qot_right_backend_update(
100                QotRightBackendCurrent {
101                    us_qot_right: data.us_qot_right,
102                },
103                QotRightBackendUpdate {
104                    cn_got: Some(cn_after),
105                    ..Default::default()
106                },
107            );
108            apply_backend_projection(&mut data, projection);
109            changed_types.push(2);
110        }
111        if !changed_types.is_empty() {
112            meta.freshness = qot_right_after_push_changes(meta.freshness.clone(), true);
113            meta.last_refresh_at_ms = now_ms();
114        }
115        Some(changed_types)
116    }
117
118    /// 从后端 CMD 6024 响应更新权限数据.
119    ///
120    /// **v1.4.106 codex 1217 F2 [P1] (5月1日)**: us_future_detail 子字段存在 =
121    /// 覆盖 (含值=0). 之前 `if cme > 0` 跳过 0 让 LV1 → 0 (No) 实质降级被
122    /// silent 保留;但 C++ 逐个 `has_open_api_*_auth()` 判断,父字段存在不代表
123    /// 未返回的子市场可按 0 覆盖。
124    ///
125    /// **v1.4.106 codex 1217 F1**: apply 成功后自动 mark Fresh.
126    pub fn update_from_backend(
127        &self,
128        update: QotRightBackendUpdate,
129    ) -> QotRightBackendApplyOutcome {
130        let mut data = self.data.write();
131        let mut meta = self.meta.lock();
132        let expected_generation = meta.backend_generation;
133        Self::apply_full_backend_update_locked(
134            &mut data,
135            &mut meta,
136            QotRightFullApplyFacts {
137                expected_generation,
138                source_publication_epoch: None,
139                is_highest: false,
140                force_resub: false,
141                pushed_notify_version: None,
142            },
143            update,
144        )
145    }
146
147    pub fn update_from_backend_if_generation(
148        &self,
149        facts: QotRightFullApplyFacts,
150        update: QotRightBackendUpdate,
151    ) -> Option<QotRightBackendApplyOutcome> {
152        let mut d = self.data.write();
153        let mut m = self.meta.lock();
154        if m.backend_generation != facts.expected_generation {
155            return None;
156        }
157        Some(Self::apply_full_backend_update_locked(
158            &mut d, &mut m, facts, update,
159        ))
160    }
161
162    fn apply_full_backend_update_locked(
163        d: &mut QotRightData,
164        m: &mut QotRightStateMeta,
165        facts: QotRightFullApplyFacts,
166        update: QotRightBackendUpdate,
167    ) -> QotRightBackendApplyOutcome {
168        let has_set_qot_right_before = has_set_qot_right_like_cpp(d);
169        let public_before = QotRightPublicSnapshot::from(&*d);
170        let reconnect_before = QotRightReconnectSnapshot::from(&*d);
171        let has_us_utp_before = d.has_us_utp;
172        let quota_before = (d.sub_quota, d.history_kl_quota);
173        let projection = project_qot_right_backend_update(
174            QotRightBackendCurrent {
175                us_qot_right: d.us_qot_right,
176            },
177            update,
178        );
179        let event_contract_became_level1 = projection.event_contract_qot_right
180            == Some(QOT_RIGHT_LEVEL1)
181            && d.event_contract_qot_right != QOT_RIGHT_LEVEL1;
182        apply_backend_projection(d, projection);
183        let quota_data_changed = quota_before != (d.sub_quota, d.history_kl_quota);
184        let public_rights_changed = public_before != QotRightPublicSnapshot::from(&*d);
185        let reconnect_quote_types = reconnect_before.changed_quote_types(d);
186        let has_us_utp_changed = has_us_utp_before != d.has_us_utp;
187
188        // F1 (v1.4.106 codex 1217): apply 成功 → 自动 mark Fresh.
189        m.freshness = qot_right_mark_fresh();
190        m.source_publication_epoch = facts.source_publication_epoch;
191        m.last_refresh_at_ms = now_ms();
192        m.event_contract_category_preload_pending |= event_contract_became_level1;
193
194        if facts.is_highest
195            && (!reconnect_quote_types.is_empty() || facts.force_resub)
196            && facts.pushed_notify_version == Some(m.last_pushed_quote_change_notify_version)
197        {
198            m.last_pushed_quote_change_notify = None;
199        }
200
201        QotRightBackendApplyOutcome {
202            public_rights_changed,
203            reconnect_quote_types,
204            has_set_qot_right_before,
205            has_us_utp_changed,
206            quota_data_changed,
207            // `update_from_backend` is the successful full-response apply owner.
208            // Even an absent OpenAPIAuth object yields the current canonical
209            // quota projection for the independent APIQuota fingerprint owner.
210            quota_projection_available: true,
211        }
212    }
213
214    pub fn update_quotas_from_backend_if_generation(
215        &self,
216        expected_generation: u64,
217        update: QotRightBackendUpdate,
218    ) -> Option<QotRightBackendApplyOutcome> {
219        let projection =
220            project_qot_right_backend_update(QotRightBackendCurrent::default(), update);
221        let mut d = self.data.write();
222        let m = self.meta.lock();
223        if m.backend_generation != expected_generation {
224            return None;
225        }
226        let quota_before = (d.sub_quota, d.history_kl_quota);
227        if let Some(value) = projection.sub_quota {
228            d.sub_quota = value;
229        }
230        if let Some(value) = projection.history_kl_quota {
231            d.history_kl_quota = value;
232        }
233        Some(QotRightBackendApplyOutcome {
234            public_rights_changed: false,
235            reconnect_quote_types: Vec::new(),
236            has_set_qot_right_before: false,
237            has_us_utp_changed: false,
238            quota_data_changed: quota_before != (d.sub_quota, d.history_kl_quota),
239            quota_projection_available: true,
240        })
241    }
242}
243
244fn apply_backend_projection(data: &mut QotRightData, projection: QotRightBackendProjection) {
245    if let Some(v) = projection.hk_own_qot_right {
246        data.hk_own_qot_right = v;
247    }
248    if let Some(v) = projection.hk_qot_right {
249        data.hk_qot_right = v;
250    }
251    if let Some(v) = projection.us_own_qot_right {
252        data.us_own_qot_right = v;
253    }
254    if let Some(v) = projection.us_qot_right {
255        data.us_qot_right = v;
256    }
257    if let Some(v) = projection.api_us_qot_right {
258        data.api_us_qot_right = v;
259    }
260    if let Some(v) = projection.has_us_utp {
261        data.has_us_utp = v;
262    }
263    if let Some(v) = projection.has_us_overnight {
264        data.has_us_overnight = v;
265    }
266    if let Some(v) = projection.sh_qot_right {
267        data.sh_qot_right = v;
268    }
269    if let Some(v) = projection.sz_qot_right {
270        data.sz_qot_right = v;
271    }
272    if let Some(v) = projection.hk_option_qot_right {
273        data.hk_option_qot_right = v;
274    }
275    if let Some(v) = projection.hk_future_qot_right {
276        data.hk_future_qot_right = v;
277    }
278    if let Some(v) = projection.hk_option_orderbook_depth {
279        data.hk_option_orderbook_depth = Some(v);
280    }
281    if let Some(v) = projection.hk_future_orderbook_depth {
282        data.hk_future_orderbook_depth = Some(v);
283    }
284    if let Some(v) = projection.has_us_option_qot_right {
285        data.has_us_option_qot_right = v;
286    }
287    if let Some(v) = projection.us_option_qot_right {
288        data.us_option_qot_right = v;
289    }
290    if let Some(v) = projection.us_index_qot_right {
291        data.us_index_qot_right = v;
292    }
293    if let Some(v) = projection.us_otc_qot_right {
294        data.us_otc_qot_right = v;
295    }
296    if let Some(v) = projection.us_cme_future_qot_right {
297        data.us_cme_future_qot_right = v;
298    }
299    if let Some(v) = projection.us_cbot_future_qot_right {
300        data.us_cbot_future_qot_right = v;
301    }
302    if let Some(v) = projection.us_nymex_future_qot_right {
303        data.us_nymex_future_qot_right = v;
304    }
305    if let Some(v) = projection.us_comex_future_qot_right {
306        data.us_comex_future_qot_right = v;
307    }
308    if let Some(v) = projection.us_cboe_future_qot_right {
309        data.us_cboe_future_qot_right = v;
310    }
311    if let Some(v) = projection.sg_future_qot_right {
312        data.sg_future_qot_right = v;
313    }
314    if let Some(v) = projection.jp_future_qot_right {
315        data.jp_future_qot_right = v;
316    }
317    if let Some(v) = projection.sg_stock_qot_right {
318        data.sg_stock_qot_right = v;
319    }
320    if let Some(v) = projection.my_stock_qot_right {
321        data.my_stock_qot_right = v;
322    }
323    if let Some(v) = projection.jp_stock_qot_right {
324        data.jp_stock_qot_right = v;
325    }
326    if let Some(v) = projection.cc_qot_right {
327        data.cc_qot_right = v;
328    }
329    if let Some(v) = projection.has_digital_future_auth {
330        data.has_digital_future_auth = v;
331    }
332    if let Some(v) = projection.cc_pt_orderbook_qot_right {
333        data.cc_pt_orderbook_qot_right = v;
334    }
335    if let Some(v) = projection.event_contract_qot_right {
336        data.event_contract_qot_right = v;
337    }
338    if let Some(ids) = projection.event_contract_prohibited_category_ids {
339        data.event_contract_prohibited_category_ids = ids;
340    }
341    if let Some(v) = projection.us_lv2_arca_qot_right {
342        data.us_lv2_arca_qot_right = v;
343    }
344    if let Some(v) = projection.us_lv2_nyse_qot_right {
345        data.us_lv2_nyse_qot_right = v;
346    }
347    if let Some(v) = projection.us_lv2_nasdaq_totalview_qot_right {
348        data.us_lv2_nasdaq_totalview_qot_right = v;
349    }
350    if let Some(v) = projection.sub_quota {
351        data.sub_quota = v;
352    }
353    if let Some(v) = projection.history_kl_quota {
354        data.history_kl_quota = v;
355    }
356}