futu_backend/
code_change.rs1use std::sync::Arc;
15
16use chrono::{NaiveDateTime, Utc};
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20#[repr(i32)]
21#[non_exhaustive]
22pub enum CodeChangeType {
23 Unknown = 0,
24 GemToMain = 1,
25 Unpaid = 2,
26 ChangeLot = 3,
27 Split = 4,
28 Joint = 5,
29 JointSplit = 6,
30 SplitJoint = 7,
31 Other = 8,
32}
33
34impl CodeChangeType {
35 fn from_event_type(event_type: i64) -> Option<Self> {
37 match event_type {
38 1 => Some(Self::Unpaid),
39 2 => Some(Self::ChangeLot),
40 3 => Some(Self::Split),
41 4 => Some(Self::Joint),
42 5 => Some(Self::JointSplit),
43 6 => Some(Self::SplitJoint),
44 7 => Some(Self::Other),
45 _ => None,
46 }
47 }
48}
49
50#[derive(Debug, Clone)]
52pub struct CodeChangeInfo {
53 pub change_type: CodeChangeType,
54 pub qot_market: i32,
56 pub sec_code: String,
57 pub relate_sec_code: String,
58 pub public_time: u64,
60 pub effective_time: u64,
62 pub end_time: u64,
64}
65
66pub type CodeChangeCache = Arc<parking_lot::RwLock<Vec<CodeChangeInfo>>>;
68
69pub fn new_cache() -> CodeChangeCache {
71 Arc::new(parking_lot::RwLock::new(Vec::new()))
72}
73
74const TEMPLATE_DATE: &str = "2019-08-13";
76
77const URL_CODE_RELATION: &str = "https://openquotenew-1251001049.cos.ap-guangzhou.myqcloud.com/hk_trans_code/HK_CodeRelationship/HK_CodeRelationship_2019-08-13.json";
79
80const URL_CODE_TEMP: &str = "https://openquotenew-1251001049.cos.ap-guangzhou.myqcloud.com/hk_trans_code/HK_TempParTrade/HK_TempParTrade_2019-08-13.json";
82
83fn make_url(template: &str) -> String {
87 let yesterday = Utc::now() - chrono::TimeDelta::days(1);
88 let date_str = yesterday.format("%Y-%m-%d").to_string();
89 template.replace(TEMPLATE_DATE, &date_str)
90}
91
92fn parse_time_str(s: &str) -> u64 {
98 if s.is_empty() {
99 return 0;
100 }
101 if let Ok(dt) = NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S") {
103 return dt.and_utc().timestamp() as u64;
104 }
105 if let Ok(d) = chrono::NaiveDate::parse_from_str(s, "%Y-%m-%d")
107 && let Some(dt) = d.and_hms_opt(0, 0, 0)
108 {
109 return dt.and_utc().timestamp() as u64;
110 }
111 tracing::warn!(time_str = s, "failed to parse time string");
112 0
113}
114
115fn strip_suffix_a(code: &str) -> &str {
119 match code.find('A') {
120 Some(pos) => &code[..pos],
121 None => code,
122 }
123}
124
125fn parse_code_relation(json_array: &[serde_json::Value]) -> Vec<CodeChangeInfo> {
129 let mut result = Vec::new();
130 for item in json_array {
131 let code_define = item.get("CodeDefine").and_then(|v| v.as_i64()).unwrap_or(0);
133 if code_define != 202 {
134 continue;
135 }
136
137 let sec_code = item.get("SecuCode").and_then(|v| v.as_str()).unwrap_or("");
138 let relate_sec_code = item
139 .get("RelatedSecuCode")
140 .and_then(|v| v.as_str())
141 .unwrap_or("");
142 let public_time_str = item
143 .get("InfoPublDate")
144 .and_then(|v| v.as_str())
145 .unwrap_or("");
146 let effective_time_str = item
147 .get("EffectiveDate")
148 .and_then(|v| v.as_str())
149 .unwrap_or("");
150
151 result.push(CodeChangeInfo {
152 change_type: CodeChangeType::GemToMain,
153 qot_market: 1, sec_code: strip_suffix_a(sec_code).to_string(),
155 relate_sec_code: strip_suffix_a(relate_sec_code).to_string(),
156 public_time: parse_time_str(public_time_str),
157 effective_time: parse_time_str(effective_time_str),
158 end_time: 0,
159 });
160 }
161 result
162}
163
164fn parse_code_temp(json_array: &[serde_json::Value]) -> Vec<CodeChangeInfo> {
168 let mut result = Vec::new();
169 for item in json_array {
170 let event_type = item.get("EventType").and_then(|v| v.as_i64()).unwrap_or(0);
171 let change_type = match CodeChangeType::from_event_type(event_type) {
172 Some(t) => t,
173 None => continue, };
175
176 let sec_code = item.get("SecuCode").and_then(|v| v.as_str()).unwrap_or("");
177 let relate_sec_code = item
179 .get("TempShareCode")
180 .and_then(|v| v.as_str())
181 .unwrap_or("");
182 let public_time_str = item
183 .get("InfoPublDate")
184 .and_then(|v| v.as_str())
185 .unwrap_or("");
186 let effective_time_str = item
188 .get("SimulTradeBeginDate")
189 .and_then(|v| v.as_str())
190 .unwrap_or("");
191 let end_time_str = item
193 .get("SimulTradeEndDate")
194 .and_then(|v| v.as_str())
195 .unwrap_or("");
196
197 result.push(CodeChangeInfo {
198 change_type,
199 qot_market: 1, sec_code: strip_suffix_a(sec_code).to_string(),
201 relate_sec_code: strip_suffix_a(relate_sec_code).to_string(),
202 public_time: parse_time_str(public_time_str),
203 effective_time: parse_time_str(effective_time_str),
204 end_time: parse_time_str(end_time_str),
205 });
206 }
207 result
208}
209
210async fn download_json(
212 url: &str,
213) -> Result<Vec<serde_json::Value>, Box<dyn std::error::Error + Send + Sync>> {
214 tracing::debug!(url, "downloading code change data");
215 let resp = reqwest::get(url).await?;
216 let status = resp.status();
217 if !status.is_success() {
218 return Err(format!("HTTP {status} for {url}").into());
222 }
223 let body = resp.text().await?;
224 let json_array: Vec<serde_json::Value> = serde_json::from_str(&body)?;
225 Ok(json_array)
226}
227
228fn is_404(e: &(dyn std::error::Error + Send + Sync)) -> bool {
230 e.to_string().contains("HTTP 404")
231}
232
233pub async fn load_code_change_data() -> Vec<CodeChangeInfo> {
238 let mut all_changes = Vec::new();
239
240 let relation_url = make_url(URL_CODE_RELATION);
242 match download_json(&relation_url).await {
243 Ok(json_array) => {
244 let changes = parse_code_relation(&json_array);
245 tracing::info!(
246 count = changes.len(),
247 "loaded code relation data (GemToMain)"
248 );
249 all_changes.extend(changes);
250 }
251 Err(e) => {
252 if is_404(e.as_ref()) {
255 tracing::debug!(error = %e, url = %relation_url, "code relation data not available for today (likely weekend/holiday)");
256 } else {
257 tracing::warn!(error = %e, url = %relation_url, "failed to download code relation data");
258 }
259 }
260 }
261
262 let temp_url = make_url(URL_CODE_TEMP);
264 match download_json(&temp_url).await {
265 Ok(json_array) => {
266 let changes = parse_code_temp(&json_array);
267 tracing::info!(count = changes.len(), "loaded code temp data");
268 all_changes.extend(changes);
269 }
270 Err(e) => {
271 if is_404(e.as_ref()) {
272 tracing::debug!(error = %e, url = %temp_url, "code temp data not available for today (likely weekend/holiday)");
273 } else {
274 tracing::warn!(error = %e, url = %temp_url, "failed to download code temp data");
275 }
276 }
277 }
278
279 all_changes
280}
281
282#[cfg(test)]
283mod tests;