Skip to main content

futu_cache/event_contract/
metadata.rs

1use super::catalog::localized;
2use super::*;
3
4impl EventContractCache {
5    pub fn apply_securities_response(
6        &self,
7        body: &[u8],
8        app_lang: i32,
9    ) -> Result<(), EventContractCacheError> {
10        let response = SecuritiesRsp::decode(body)
11            .map_err(|error| EventContractCacheError::Decode(error.to_string()))?;
12        if response.ret_code != Some(0) {
13            return Err(EventContractCacheError::Validation(
14                "event contract securities backend ret_code is not success",
15            ));
16        }
17        let _writer = self.metadata_writer.lock();
18        let mut next = (*self.metadata_snapshot()).clone();
19        for item in response.event_contract_info_items {
20            // Ref: frozen C++ aec0f6cda1
21            // EventContractProtoHelper.cpp:35-41 ignores the nested `ret`;
22            // NNBiz_Qot_StockInfoReq.cpp:334-367 gates the batch on outer ret_code.
23            let base = item
24                .event_contract_info
25                .ok_or(EventContractCacheError::Validation(
26                    "event contract metadata item has no base",
27                ))?;
28            insert_event_contract_metadata(&mut next, base, app_lang)?;
29        }
30        self.metadata.store(Arc::new(next));
31        Ok(())
32    }
33
34    pub fn apply_extra_response(
35        &self,
36        body: &[u8],
37        app_lang: i32,
38    ) -> Result<(), EventContractCacheError> {
39        let response = GetEventContractExtraInfoRsp::decode(body)
40            .map_err(|error| EventContractCacheError::Decode(error.to_string()))?;
41        if response.err_code != Some(0) {
42            return Err(EventContractCacheError::Validation(
43                "event contract extra backend err_code is not success",
44            ));
45        }
46        let _writer = self.metadata_writer.lock();
47        let mut next = (*self.metadata_snapshot()).clone();
48        for item in response.extra_info_list {
49            // Ref: frozen C++ aec0f6cda1
50            // NNBiz_Qot_EventContract.cpp:296-307 gates on outer err_code and
51            // ignores the per-item ret_code.
52            let stock_id = item.stock_id.filter(|value| *value != 0).ok_or(
53                EventContractCacheError::Validation("event contract extra stock_id is missing"),
54            )?;
55            if !next.securities_by_id.contains_key(&stock_id) {
56                return Err(EventContractCacheError::Validation(
57                    "event contract extra has no base metadata",
58                ));
59            }
60            next.extras_by_id.insert(
61                stock_id,
62                SecurityExtra {
63                    series: item.series_extra_info.map(|extra| SeriesExtra {
64                        frequency: extra.frequency.unwrap_or(0),
65                    }),
66                    event: item.event_extra_info.map(|extra| EventExtra {
67                        mutually_exclusive: extra.mutually_exclusive.unwrap_or(0),
68                        competition_id: extra.competition_id.unwrap_or(0),
69                        competition_scope_id: extra.competition_scope_id.unwrap_or(0),
70                        start_date: extra.start_date.unwrap_or(0),
71                        end_date: extra.end_date.unwrap_or(0),
72                    }),
73                    contract: item.contract_extra_info.map(|extra| ContractExtra {
74                        liquidity: extra.liquidity.unwrap_or(0),
75                        latest_expiration_time: extra.latest_expiration_time.unwrap_or(0),
76                        can_close_early: extra.can_close_early.unwrap_or(0) != 0,
77                    }),
78                    milestone: item.milestone_extra_info.map(|extra| MilestoneExtra {
79                        notification_message: localized(
80                            extra.notification_message.as_ref(),
81                            app_lang,
82                        ),
83                        related_events: extra
84                            .related_events
85                            .into_iter()
86                            .map(|event| RelatedEvent {
87                                stock_id: event.stock_id.unwrap_or(0),
88                                futu_symbol: event.futu_symbol.unwrap_or_default(),
89                                is_valid: event.is_valid.unwrap_or(0) != 0,
90                                is_main_event: event.is_main_event.unwrap_or(0) != 0,
91                                is_primary_event: event.is_primary_event.unwrap_or(0) != 0,
92                            })
93                            .collect(),
94                    }),
95                },
96            );
97        }
98        self.metadata.store(Arc::new(next));
99        Ok(())
100    }
101
102    /// Applies a ComboList-triggered 21165 refresh with the frozen C++ row
103    /// transaction semantics.
104    ///
105    /// The outer protobuf/`err_code` is one response transaction: malformed
106    /// or failed responses publish nothing. Within a successful response,
107    /// rows are independent, zero/missing ids are skipped, and valid extra
108    /// rows publish even when their base 20106 row has not arrived yet. The
109    /// resulting set of accepted rows is published as one immutable
110    /// generation.
111    ///
112    /// Ref: frozen C++ aec0f6cda1
113    /// `NNBiz_Qot_EventContract.cpp:289-375`.
114    pub fn apply_combo_extra_response_best_effort(
115        &self,
116        body: &[u8],
117        app_lang: i32,
118    ) -> Result<(), EventContractCacheError> {
119        let response = GetEventContractExtraInfoRsp::decode(body)
120            .map_err(|error| EventContractCacheError::Decode(error.to_string()))?;
121        if response.err_code.unwrap_or(0) != 0 {
122            return Err(EventContractCacheError::Validation(
123                "event contract extra backend err_code is not success",
124            ));
125        }
126        let _writer = self.metadata_writer.lock();
127        let mut next = (*self.metadata_snapshot()).clone();
128        for item in response.extra_info_list {
129            let Some(stock_id) = item.stock_id.filter(|value| *value != 0) else {
130                continue;
131            };
132            // Frozen C++ deliberately ignores item.ret_code and stores each
133            // present hierarchy independently of base-metadata arrival.
134            let extra = SecurityExtra {
135                series: item.series_extra_info.map(|extra| SeriesExtra {
136                    frequency: extra.frequency.unwrap_or(0),
137                }),
138                event: item.event_extra_info.map(|extra| EventExtra {
139                    mutually_exclusive: extra.mutually_exclusive.unwrap_or(0),
140                    competition_id: extra.competition_id.unwrap_or(0),
141                    competition_scope_id: extra.competition_scope_id.unwrap_or(0),
142                    start_date: extra.start_date.unwrap_or(0),
143                    end_date: extra.end_date.unwrap_or(0),
144                }),
145                contract: item.contract_extra_info.map(|extra| ContractExtra {
146                    liquidity: extra.liquidity.unwrap_or(0),
147                    latest_expiration_time: extra.latest_expiration_time.unwrap_or(0),
148                    can_close_early: extra.can_close_early.unwrap_or(0) != 0,
149                }),
150                milestone: item.milestone_extra_info.map(|extra| MilestoneExtra {
151                    notification_message: localized(extra.notification_message.as_ref(), app_lang),
152                    related_events: extra
153                        .related_events
154                        .into_iter()
155                        .map(|event| RelatedEvent {
156                            stock_id: event.stock_id.unwrap_or(0),
157                            futu_symbol: event.futu_symbol.unwrap_or_default(),
158                            is_valid: event.is_valid.unwrap_or(0) != 0,
159                            is_main_event: event.is_main_event.unwrap_or(0) != 0,
160                            is_primary_event: event.is_primary_event.unwrap_or(0) != 0,
161                        })
162                        .collect(),
163                }),
164            };
165            if extra.series.is_none()
166                && extra.event.is_none()
167                && extra.contract.is_none()
168                && extra.milestone.is_none()
169            {
170                continue;
171            }
172            next.extras_by_id.insert(stock_id, extra);
173        }
174        self.metadata.store(Arc::new(next));
175        Ok(())
176    }
177
178    pub fn apply_relation_response(&self, body: &[u8]) -> Result<(), EventContractCacheError> {
179        self.apply_relation_response_with_lang(body, 2)
180    }
181
182    /// Publishes relation ids and any nested detail metadata as one immutable
183    /// generation. `need_detail=true` relation replies are an authoritative
184    /// metadata source; the selected child must not require a second 20106.
185    ///
186    /// Ref: frozen C++ aec0f6cda1
187    /// `NNBiz_Qot_EventContract.cpp:125-171,235-286`.
188    pub fn apply_relation_response_with_lang(
189        &self,
190        body: &[u8],
191        app_lang: i32,
192    ) -> Result<(), EventContractCacheError> {
193        let response = GetEventContractRelationRsp::decode(body)
194            .map_err(|error| EventContractCacheError::Decode(error.to_string()))?;
195        if response.err_code != Some(0) {
196            return Err(EventContractCacheError::Validation(
197                "event contract relation backend err_code is not success",
198            ));
199        }
200        let _writer = self.metadata_writer.lock();
201        let mut next = (*self.metadata_snapshot()).clone();
202        for relation in response.relation_list {
203            let contract = relation
204                .contract
205                .ok_or(EventContractCacheError::Validation(
206                    "event contract relation has no contract",
207                ))?;
208            let contract_stock_id = contract
209                .stock_id
210                .or_else(|| {
211                    contract
212                        .event_contract_info
213                        .as_ref()
214                        .and_then(|info| info.stock_id)
215                })
216                .filter(|value| *value != 0)
217                .ok_or(EventContractCacheError::Validation(
218                    "event contract relation has no contract stock_id",
219                ))?;
220            for info in [
221                relation.series.as_ref(),
222                relation.event.as_ref(),
223                Some(&contract),
224                relation.sub_contracts_yes.as_ref(),
225                relation.sub_contracts_no.as_ref(),
226            ]
227            .into_iter()
228            .flatten()
229            {
230                if let Some(detail) = info.event_contract_info.clone() {
231                    if info
232                        .stock_id
233                        .is_some_and(|stock_id| Some(stock_id) != detail.stock_id)
234                    {
235                        return Err(EventContractCacheError::Validation(
236                            "event contract relation detail stock_id disagrees with relation",
237                        ));
238                    }
239                    insert_event_contract_metadata(&mut next, detail, app_lang)?;
240                }
241            }
242            next.sub_contracts_by_contract_id.insert(
243                contract_stock_id,
244                SubContractRelation {
245                    yes_stock_id: relation
246                        .sub_contracts_yes
247                        .and_then(|info| info.stock_id)
248                        .filter(|value| *value != 0),
249                    no_stock_id: relation
250                        .sub_contracts_no
251                        .and_then(|info| info.stock_id)
252                        .filter(|value| *value != 0),
253                },
254            );
255        }
256        self.metadata.store(Arc::new(next));
257        Ok(())
258    }
259}
260
261fn insert_event_contract_metadata(
262    next: &mut MetadataGeneration,
263    base: EventContract,
264    app_lang: i32,
265) -> Result<(), EventContractCacheError> {
266    let stock_id =
267        base.stock_id
268            .filter(|value| *value != 0)
269            .ok_or(EventContractCacheError::Validation(
270                "event contract stock_id is missing or zero",
271            ))?;
272    let futu_symbol = base.futu_symbol.filter(|value| !value.is_empty()).ok_or(
273        EventContractCacheError::Validation("event contract futu_symbol is missing"),
274    )?;
275    if let Some(existing) = next.security_id_by_code.get(&futu_symbol)
276        && *existing != stock_id
277    {
278        return Err(EventContractCacheError::Validation(
279            "event contract futu_symbol maps to multiple stock ids",
280        ));
281    }
282    next.security_id_by_code
283        .insert(futu_symbol.clone(), stock_id);
284    next.securities_by_id.insert(
285        stock_id,
286        SecurityMetadata {
287            stock_id,
288            futu_symbol,
289            exchange: base.exchange.unwrap_or_default(),
290            market_code: base.market_code.unwrap_or(0),
291            currency_code: base.currency_code.unwrap_or(0),
292            sub_instrument_type_v2: base.sub_instrument_type_v2.unwrap_or(0),
293            series_stock_id: base.series_stock_id.unwrap_or(0),
294            parent_stock_id: base.parent_stock_id.unwrap_or(0),
295            contract_type: base.contract_type.unwrap_or(0),
296            direction: base.direction.unwrap_or(0),
297            status: base.status.unwrap_or(0),
298            result: base.result.unwrap_or(0),
299            category_id: base.category_id.unwrap_or(0),
300            tag_ids: base.tag_ids,
301            name: localized(base.name.as_ref(), app_lang),
302            sub_name: localized(base.sub_name.as_ref(), app_lang),
303            spread_table_code: base.spread_table_code.unwrap_or(0),
304            first_trading_time: base.first_trading_time.unwrap_or(0),
305            last_trading_time: base.last_trading_time.unwrap_or(0),
306            determination_time: base.determination_time.unwrap_or(0),
307            settlement_time: base.settlement_time.unwrap_or(0),
308            settlement_value: base.settlement_value.unwrap_or(0),
309            expiration_value: base.expiration_value.unwrap_or_default(),
310        },
311    );
312    Ok(())
313}