Skip to main content

futu_backend/
code_change.rs

1//! 代码变更数据 HTTP 下载 + 解析
2//!
3//! C++ 对应: NNBiz_CodeChange
4//!
5//! 从腾讯云 CDN 下载 JSON 数据:
6//! - CodeRelation: 创业板转主板 (CodeDefine=202 → GemToMain)
7//!   URL 模板: https://openquotenew-1251001049.cos.ap-guangzhou.myqcloud.com/hk_trans_code/HK_CodeRelationship/HK_CodeRelationship_{date}.json
8//! - CodeTemp: 临时代码 (EventType 1-7 → Unpaid/ChangeLot/Split/Joint/JointSplit/SplitJoint/Other)
9//!   URL 模板: https://openquotenew-1251001049.cos.ap-guangzhou.myqcloud.com/hk_trans_code/HK_TempParTrade/HK_TempParTrade_{date}.json
10//!
11//! 日期为服务器时间前一天 (YYYY-MM-DD 格式)。
12//! JSON 格式均为数组,每个元素包含 SecuCode/RelatedSecuCode(或 TempShareCode)/InfoPublDate/EffectiveDate(或 SimulTradeBeginDate/SimulTradeEndDate) 等字段。
13
14use std::sync::Arc;
15
16use chrono::{NaiveDateTime, Utc};
17
18/// 代码变更类型 (C++ CodeChangeType)
19#[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    /// C++ CodeChangeType_SvrToNN: EventType(1-7) → CodeChangeType
36    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/// 单条代码变更记录 (C++ CodeChangeInfo)
51#[derive(Debug, Clone)]
52pub struct CodeChangeInfo {
53    pub change_type: CodeChangeType,
54    /// HK 市场 (C++ NN_QuoteMktType_HK = 1) → QotMarket = 1
55    pub qot_market: i32,
56    pub sec_code: String,
57    pub relate_sec_code: String,
58    /// 公布时间 (Unix 时间戳秒)
59    pub public_time: u64,
60    /// 生效时间 (Unix 时间戳秒)
61    pub effective_time: u64,
62    /// 结束时间 (Unix 时间戳秒, GemToMain 类型不存在)
63    pub end_time: u64,
64}
65
66/// 代码变更缓存
67pub type CodeChangeCache = Arc<parking_lot::RwLock<Vec<CodeChangeInfo>>>;
68
69/// 创建空缓存
70pub fn new_cache() -> CodeChangeCache {
71    Arc::new(parking_lot::RwLock::new(Vec::new()))
72}
73
74/// URL 模板中的占位日期
75const TEMPLATE_DATE: &str = "2019-08-13";
76
77/// CodeRelation URL 模板
78const URL_CODE_RELATION: &str = "https://openquotenew-1251001049.cos.ap-guangzhou.myqcloud.com/hk_trans_code/HK_CodeRelationship/HK_CodeRelationship_2019-08-13.json";
79
80/// CodeTemp URL 模板
81const URL_CODE_TEMP: &str = "https://openquotenew-1251001049.cos.ap-guangzhou.myqcloud.com/hk_trans_code/HK_TempParTrade/HK_TempParTrade_2019-08-13.json";
82
83/// 构造实际 URL: 将模板中的 "2019-08-13" 替换为昨天的日期
84///
85/// C++ GetCodeChangeUrl: 使用服务器时间前一天 (GetSvrTimeStamp() - M_OneDaySecs)
86fn 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
92/// 解析时间字符串为 Unix 时间戳
93///
94/// C++ FullTimeStrToTimeStamp 支持多种格式, 这里处理 JSON 中出现的格式:
95/// - "2024-01-15 00:00:00" (datetime)
96/// - "2024-01-15" (date only, 按 00:00:00 处理)
97fn parse_time_str(s: &str) -> u64 {
98    if s.is_empty() {
99        return 0;
100    }
101    // 尝试 datetime 格式
102    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    // 尝试 date-only 格式
106    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
115/// 从 SecuCode 中截取 'A' 之前的部分
116///
117/// C++ 中: strSecCode.find_first_of("A") → substr(0, nPos)
118fn strip_suffix_a(code: &str) -> &str {
119    match code.find('A') {
120        Some(pos) => &code[..pos],
121        None => code,
122    }
123}
124
125/// 解析 CodeRelation JSON 数据
126///
127/// C++ ParserCodeRelationData: 只取 CodeDefine==202 的记录 → GemToMain
128fn parse_code_relation(json_array: &[serde_json::Value]) -> Vec<CodeChangeInfo> {
129    let mut result = Vec::new();
130    for item in json_array {
131        // C++: opItem.read("CodeDefine", nType); If_Do(nType != 202, continue);
132        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, // C++: NN_QuoteMktType_HK → QotMarket_HK_Security = 1
154            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
164/// 解析 CodeTemp JSON 数据
165///
166/// C++ ParserCodeTempData: EventType(1-7) → 对应 CodeChangeType
167fn 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, // C++: If_Do(enType == CodeChangeType_Unkonw, continue)
174        };
175
176        let sec_code = item.get("SecuCode").and_then(|v| v.as_str()).unwrap_or("");
177        // C++ CodeTemp 使用 "TempShareCode" 而不是 "RelatedSecuCode"
178        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        // C++ CodeTemp 使用 "SimulTradeBeginDate" 作为 effectiveTime
187        let effective_time_str = item
188            .get("SimulTradeBeginDate")
189            .and_then(|v| v.as_str())
190            .unwrap_or("");
191        // C++ CodeTemp 使用 "SimulTradeEndDate" 作为 endTime
192        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, // C++: NN_QuoteMktType_HK
200            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
210/// 下载单个 URL 的 JSON 数据
211async 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        // 404 = URL 按日期构造但 CDN 无当日文件(周末/假日 / 未更新),是
219        // 可预期的软失败。5xx / 网络错才是真正需要 WARN 的。错误 msg 保留
220        // 原始 status code,上层按字符串判断降级。
221        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
228/// 判断错误是不是 404 软失败(上层降级为 DEBUG)。
229fn is_404(e: &(dyn std::error::Error + Send + Sync)) -> bool {
230    e.to_string().contains("HTTP 404")
231}
232
233/// 下载并解析所有代码变更数据
234///
235/// C++ NNBiz_CodeChange::UpdateCodeChange:
236/// 同时下载 CodeRelation 和 CodeTemp,分别解析后合并到缓存
237pub async fn load_code_change_data() -> Vec<CodeChangeInfo> {
238    let mut all_changes = Vec::new();
239
240    // 下载 CodeRelation (创业板转主板)
241    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            // v1.4.27(BUG-4):404 = 周末/假日 CDN 无当日文件,降 DEBUG;
253            // 5xx / 网络错才 WARN
254            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    // 下载 CodeTemp (临时代码)
263    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;