stock-rust 0.1.0

Rust version of stock-api
Documentation
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);
    }
}