tqsdk_rs/
utils.rs

1//! 工具函数
2//!
3//! 提供各种工具函数,包括:
4//! - 流式 JSON 下载
5//! - 时间转换
6//! - 字符串处理
7
8use crate::errors::{Result, TqError};
9use chrono::{DateTime, Utc};
10use futures::StreamExt;
11use serde_json::Value;
12use std::time::Duration;
13use tracing::{debug, info};
14
15/// 流式下载 JSON 文件
16///
17/// 使用 reqwest 的 stream 特性流式下载 JSON 文件,支持 gzip 和 brotli 压缩
18///
19/// # 参数
20///
21/// * `url` - 要下载的 URL
22///
23/// # 返回
24///
25/// 解析后的 JSON Value
26///
27/// # 示例
28///
29/// ```no_run
30/// # use tqsdk_rs::utils::fetch_json;
31/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
32/// let json = fetch_json("https://openmd.shinnytech.com/t/md/symbols/latest.json").await?;
33/// # Ok(())
34/// # }
35/// ```
36pub async fn fetch_json(url: &str) -> Result<Value> {
37    info!("开始下载 JSON: {}", url);
38
39    // 创建支持 gzip 和 brotli 的 HTTP 客户端
40    let client = reqwest::Client::builder()
41        .gzip(true)
42        .brotli(true)
43        .timeout(Duration::from_secs(30))
44        .build()
45        .map_err(|e| TqError::NetworkError(format!("创建 HTTP 客户端失败: {}", e)))?;
46
47    // 发送请求
48    let response = client
49        .get(url)
50        .send()
51        .await
52        .map_err(|e| TqError::NetworkError(format!("请求失败: {}", e)))?;
53
54    // 检查状态码
55    if !response.status().is_success() {
56        return Err(TqError::NetworkError(format!(
57            "HTTP 状态码错误: {}",
58            response.status()
59        )));
60    }
61
62    debug!("HTTP 状态: {}", response.status());
63
64    // 流式下载并累积字节
65    let mut stream = response.bytes_stream();
66    let mut buffer = Vec::new();
67
68    while let Some(chunk) = stream.next().await {
69        let chunk = chunk.map_err(|e| TqError::NetworkError(format!("下载数据失败: {}", e)))?;
70        buffer.extend_from_slice(&chunk);
71        debug!("已下载: {} 字节", buffer.len());
72    }
73
74    info!("下载完成,总大小: {} 字节", buffer.len());
75
76    // 解析 JSON
77    let json: Value = serde_json::from_slice(&buffer)
78        .map_err(|e| TqError::ParseError(format!("JSON 解析失败: {}", e)))?;
79
80    Ok(json)
81}
82
83/// 将纳秒时间戳转换为 DateTime
84///
85/// # 参数
86///
87/// * `nanos` - 纳秒时间戳
88///
89/// # 返回
90///
91/// DateTime<Utc> 对象
92pub fn nanos_to_datetime(nanos: i64) -> DateTime<Utc> {
93    let secs = nanos / 1_000_000_000;
94    let nsecs = (nanos % 1_000_000_000) as u32;
95    DateTime::from_timestamp(secs, nsecs).unwrap_or_else(|| Utc::now())
96}
97
98/// 将 DateTime 转换为纳秒时间戳
99///
100/// # 参数
101///
102/// * `dt` - DateTime<Utc> 对象
103///
104/// # 返回
105///
106/// 纳秒时间戳
107pub fn datetime_to_nanos(dt: &DateTime<Utc>) -> i64 {
108    dt.timestamp() * 1_000_000_000 + dt.timestamp_subsec_nanos() as i64
109}
110
111/// 将合约代码拆分为交易所和合约
112///
113/// # 参数
114///
115/// * `symbol` - 合约代码(格式:EXCHANGE.INSTRUMENT)
116///
117/// # 返回
118///
119/// (exchange, instrument) 元组
120///
121/// # 示例
122///
123/// ```
124/// # use tqsdk_rs::utils::split_symbol;
125/// let (exchange, instrument) = split_symbol("SHFE.au2602");
126/// assert_eq!(exchange, "SHFE");
127/// assert_eq!(instrument, "au2602");
128/// ```
129pub fn split_symbol(symbol: &str) -> (&str, &str) {
130    if let Some(pos) = symbol.find('.') {
131        let exchange = &symbol[..pos];
132        let instrument = &symbol[pos + 1..];
133        (exchange, instrument)
134    } else {
135        ("", symbol)
136    }
137}
138
139/// 获取交易所前缀
140///
141/// # 参数
142///
143/// * `symbol` - 合约代码
144///
145/// # 返回
146///
147/// 交易所代码
148pub fn get_exchange(symbol: &str) -> &str {
149    split_symbol(symbol).0
150}
151
152/// 生成唯一的图表 ID
153///
154/// # 参数
155///
156/// * `prefix` - 前缀
157///
158/// # 返回
159///
160/// 唯一的图表 ID
161pub fn generate_chart_id(prefix: &str) -> String {
162    let uuid = uuid::Uuid::new_v4();
163    format!("{}_{}", prefix, uuid)
164}
165
166/// 检查值是否为 NaN 字符串
167///
168/// # 参数
169///
170/// * `s` - 字符串
171///
172/// # 返回
173///
174/// 是否为 NaN
175pub fn is_nan_string(s: &str) -> bool {
176    s == "NaN" || s == "-" || s.is_empty()
177}
178
179/// 将 JSON Value 转换为 i64
180///
181/// # 参数
182///
183/// * `value` - JSON Value
184///
185/// # 返回
186///
187/// i64 值
188pub fn value_to_i64(value: &Value) -> i64 {
189    match value {
190        Value::Number(n) => n.as_i64().unwrap_or(0),
191        Value::String(s) => s.parse().unwrap_or(0),
192        _ => 0,
193    }
194}
195
196/// 将 JSON Value 转换为 f64
197///
198/// # 参数
199///
200/// * `value` - JSON Value
201///
202/// # 返回
203///
204/// f64 值
205pub fn value_to_f64(value: &Value) -> f64 {
206    match value {
207        Value::Number(n) => n.as_f64().unwrap_or(0.0),
208        Value::String(s) => {
209            if is_nan_string(s) {
210                f64::NAN
211            } else {
212                s.parse().unwrap_or(0.0)
213            }
214        }
215        _ => 0.0,
216    }
217}
218
219#[cfg(test)]
220mod tests {
221    use super::*;
222
223    #[test]
224    fn test_split_symbol() {
225        let (exchange, instrument) = split_symbol("SHFE.au2602");
226        assert_eq!(exchange, "SHFE");
227        assert_eq!(instrument, "au2602");
228
229        let (exchange, instrument) = split_symbol("DCE.m2512");
230        assert_eq!(exchange, "DCE");
231        assert_eq!(instrument, "m2512");
232
233        let (exchange, instrument) = split_symbol("invalid");
234        assert_eq!(exchange, "");
235        assert_eq!(instrument, "invalid");
236    }
237
238    #[test]
239    fn test_get_exchange() {
240        assert_eq!(get_exchange("SHFE.au2602"), "SHFE");
241        assert_eq!(get_exchange("DCE.m2512"), "DCE");
242        assert_eq!(get_exchange("invalid"), "");
243    }
244
245    #[test]
246    fn test_is_nan_string() {
247        assert!(is_nan_string("NaN"));
248        assert!(is_nan_string("-"));
249        assert!(is_nan_string(""));
250        assert!(!is_nan_string("123"));
251        assert!(!is_nan_string("0"));
252    }
253
254    #[test]
255    fn test_datetime_conversion() {
256        let now = Utc::now();
257        let nanos = datetime_to_nanos(&now);
258        let dt = nanos_to_datetime(nanos);
259        
260        // 允许少量误差(纳秒精度可能有损失)
261        let diff = (dt.timestamp() - now.timestamp()).abs();
262        assert!(diff <= 1);
263    }
264
265    #[test]
266    fn test_generate_chart_id() {
267        let id1 = generate_chart_id("TQGO_kline");
268        let id2 = generate_chart_id("TQGO_kline");
269        
270        assert!(id1.starts_with("TQGO_kline_"));
271        assert!(id2.starts_with("TQGO_kline_"));
272        assert_ne!(id1, id2); // 应该是不同的 ID
273    }
274}
275