use anyhow::Result;
use async_trait::async_trait;
use encoding_rs::GB18030;
use crate::stocks::base::{
code_market, dedup_codes, parse_num, percent, COMMON_HK, COMMON_SH, COMMON_SZ, COMMON_US,
};
use crate::stocks::transforms::base::CommonCodeTransform;
use crate::stocks::transforms::sina::{SinaCommonCodeTransform, SinaSearchCodeTransform};
use crate::types::{Stock, StockApi};
#[derive(Clone)]
pub struct SinaApi {
client: reqwest::Client,
}
impl SinaApi {
pub fn new(client: reqwest::Client) -> Self {
Self { client }
}
fn parse_line(code: &str, line: &str) -> Stock {
let params_unformatted = line.split('=').nth(1).unwrap_or("").trim();
if params_unformatted == "\"\"" {
return Stock::default_with_code(code);
}
let params: Vec<&str> = params_unformatted.trim_matches('"').split(',').collect();
let market = code_market(code);
let name = match market {
COMMON_SH | COMMON_SZ | COMMON_US => params.first().unwrap_or(&"---").to_string(),
COMMON_HK => params.get(1).unwrap_or(&"---").to_string(),
_ => "---".to_string(),
};
let now = match market {
COMMON_SH | COMMON_SZ => parse_num(params.get(3).copied()),
COMMON_HK => parse_num(params.get(6).copied()),
COMMON_US => parse_num(params.get(1).copied()),
_ => 0.0,
};
let low = match market {
COMMON_SH | COMMON_SZ | COMMON_HK => parse_num(params.get(5).copied()),
COMMON_US => parse_num(params.get(7).copied()),
_ => 0.0,
};
let high = match market {
COMMON_SH | COMMON_SZ | COMMON_HK => parse_num(params.get(4).copied()),
COMMON_US => parse_num(params.get(6).copied()),
_ => 0.0,
};
let yesterday = match market {
COMMON_SH | COMMON_SZ => parse_num(params.get(2).copied()),
COMMON_HK => parse_num(params.get(3).copied()),
COMMON_US => parse_num(params.get(26).copied()),
_ => 0.0,
};
Stock {
code: code.to_uppercase(),
name,
now,
low,
high,
yesterday,
percent: percent(now, yesterday),
}
}
}
#[async_trait]
impl StockApi for SinaApi {
async fn get_stock(&self, code: &str) -> Result<Stock> {
let transformer = SinaCommonCodeTransform;
let transformed = transformer.transform(code)?;
let url = format!("https://hq.sinajs.cn/list={}", transformed);
let body = self.client.get(url).send().await?.bytes().await?;
let (text, _, _) = GB18030.decode(&body);
let line = text.split(";\n").next().unwrap_or_default();
Ok(Self::parse_line(code, line))
}
async fn get_stocks(&self, codes: &[String]) -> Result<Vec<Stock>> {
let codes = dedup_codes(codes);
if codes.is_empty() {
return Ok(Vec::new());
}
let transformer = SinaCommonCodeTransform;
let transformed = transformer.transforms(&codes)?;
let url = format!("https://hq.sinajs.cn/list={}", transformed.join(","));
let body = self.client.get(url).send().await?.bytes().await?;
let (text, _, _) = GB18030.decode(&body);
let rows: Vec<&str> = text.split(";\n").collect();
Ok(codes
.iter()
.enumerate()
.map(|(index, code)| Self::parse_line(code, rows.get(index).copied().unwrap_or_default()))
.collect())
}
async fn search_stocks(&self, key: &str) -> Result<Vec<Stock>> {
let url = format!(
"http://suggest3.sinajs.cn/suggest/type=2&key={}",
urlencoding::encode(key)
);
let body = self.client.get(url).send().await?.bytes().await?;
let (text, _, _) = GB18030.decode(&body);
let sanitized = text
.replace("var suggestvalue=\"", "")
.replace("\";", "");
let rows: Vec<&str> = sanitized.split(';').collect();
let search_transformer = SinaSearchCodeTransform;
let mut codes: Vec<String> = Vec::new();
for row in rows {
let code = row.split(',').next().unwrap_or_default();
codes.extend(search_transformer.transform(code));
}
self.get_stocks(&dedup_codes(&codes)).await
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::stocks::transforms::base::CommonCodeTransform;
use crate::stocks::transforms::sina::SinaCommonCodeTransform;
#[test]
fn test_to_api_code() {
let transformer = SinaCommonCodeTransform;
assert_eq!(transformer.transform("SH510500").unwrap(), "sh510500");
assert_eq!(transformer.transform("SZ399001").unwrap(), "sz399001");
assert_eq!(transformer.transform("HKHSI").unwrap(), "hkHSI");
assert_eq!(transformer.transform("USDJI").unwrap(), "gb_dji");
}
#[test]
fn test_parse_line_markets() {
let sh_line = "var hq_str_sh510500=\"500ETF,7.147,7.149,7.224,7.280,7.085\"";
let sz_line = "var hq_str_sz399001=\"深证成指,13464.207,13466.854,13637.883,13748.034,13399.859\"";
let hk_line = "var hq_str_hkHSI=\"Hang Seng Index,恒生指数,24747.29,24710.59,24938.85,24534.79,24595.35\"";
let us_line = "var hq_str_gb_dji=\"道琼斯,26428.3203,0.44,2020-08-01 05:07:40,114.6700,26409.3301,26440.0195,26013.5898,29568.5703,18213.6504,491372564,397524206,0,0.00,--,0.00,0.00,0.00,0.00,0,0,0.0000,0.00,0.0000,,Jul 31 05:07PM EDT,26313.6504,0,1,2020\"";
let sh = SinaApi::parse_line("SH510500", sh_line);
assert_eq!(sh.code, "SH510500");
assert_eq!(sh.name, "500ETF");
assert!((sh.now - 7.224).abs() < 1e-12);
assert!((sh.low - 7.085).abs() < 1e-12);
assert!((sh.high - 7.28).abs() < 1e-12);
assert!((sh.yesterday - 7.149).abs() < 1e-12);
let sz = SinaApi::parse_line("SZ399001", sz_line);
assert_eq!(sz.name, "深证成指");
assert!((sz.now - 13637.883).abs() < 1e-12);
let hk = SinaApi::parse_line("HKHSI", hk_line);
assert_eq!(hk.name, "恒生指数");
assert!((hk.now - 24595.35).abs() < 1e-12);
let us = SinaApi::parse_line("USDJI", us_line);
assert_eq!(us.name, "道琼斯");
assert!((us.now - 26428.3203).abs() < 1e-12);
assert!((us.low - 26013.5898).abs() < 1e-12);
assert!((us.high - 26440.0195).abs() < 1e-12);
assert!((us.yesterday - 26313.6504).abs() < 1e-12);
}
}