futu_backend/
code_change.rs1use std::sync::Arc;
15
16use chrono::{DateTime, 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_at(template: &str, server_now: DateTime<Utc>) -> String {
87 let yesterday = server_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 client: &reqwest::Client,
213 url: &str,
214) -> Result<Vec<serde_json::Value>, Box<dyn std::error::Error + Send + Sync>> {
215 tracing::debug!(url, "downloading code change data");
216 let resp = client.get(url).send().await?;
217 let status = resp.status();
218 if !status.is_success() {
219 return Err(format!("HTTP {status} for {url}").into());
223 }
224 let body = resp.text().await?;
225 let json_array: Vec<serde_json::Value> = serde_json::from_str(&body)?;
226 Ok(json_array)
227}
228
229fn is_404(e: &(dyn std::error::Error + Send + Sync)) -> bool {
231 e.to_string().contains("HTTP 404")
232}
233
234pub async fn load_code_change_data_at(
239 client: &reqwest::Client,
240 server_now: DateTime<Utc>,
241) -> Vec<CodeChangeInfo> {
242 let mut all_changes = Vec::new();
243
244 let relation_url = make_url_at(URL_CODE_RELATION, server_now);
246 match download_json(client, &relation_url).await {
247 Ok(json_array) => {
248 let changes = parse_code_relation(&json_array);
249 tracing::info!(
250 count = changes.len(),
251 "loaded code relation data (GemToMain)"
252 );
253 all_changes.extend(changes);
254 }
255 Err(e) => {
256 if is_404(e.as_ref()) {
259 tracing::debug!(error = %e, url = %relation_url, "code relation data not available for today (likely weekend/holiday)");
260 } else {
261 tracing::warn!(error = %e, url = %relation_url, "failed to download code relation data");
262 }
263 }
264 }
265
266 let temp_url = make_url_at(URL_CODE_TEMP, server_now);
268 match download_json(client, &temp_url).await {
269 Ok(json_array) => {
270 let changes = parse_code_temp(&json_array);
271 tracing::info!(count = changes.len(), "loaded code temp data");
272 all_changes.extend(changes);
273 }
274 Err(e) => {
275 if is_404(e.as_ref()) {
276 tracing::debug!(error = %e, url = %temp_url, "code temp data not available for today (likely weekend/holiday)");
277 } else {
278 tracing::warn!(error = %e, url = %temp_url, "failed to download code temp data");
279 }
280 }
281 }
282
283 all_changes
284}
285
286pub async fn load_code_change_data() -> Vec<CodeChangeInfo> {
291 match crate::reference_http::build_client() {
292 Ok(client) => load_code_change_data_at(&client, Utc::now()).await,
293 Err(error) => {
294 tracing::error!(
295 %error,
296 "reference-data HTTP client config failed; skipping code-change preload"
297 );
298 Vec::new()
299 }
300 }
301}
302
303#[cfg(test)]
304mod tests;