futu_cache/event_contract/
metadata.rs1use 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 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 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 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 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 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}