use anyhow::Result;
use async_trait::async_trait;
use serde_json::Value;
use crate::stocks::base::{
as_f64, as_string, dedup_codes, percent,
};
use crate::stocks::transforms::base::CommonCodeTransform;
use crate::stocks::transforms::netease::{NeteaseCommonCodeTransform, NeteaseSearchCodeTransform};
use crate::types::{Stock, StockApi};
#[derive(Clone)]
pub struct NeteaseApi {
client: reqwest::Client,
}
impl NeteaseApi {
pub fn new(client: reqwest::Client) -> Self {
Self { client }
}
fn clean_jsonp(raw: &str) -> String {
raw.replace(['(', ')', ';', '\n', '\r', '\t', ' '], "")
.replace("topstock", "")
.replace("{{", "{")
.replace("}}}", "}}")
}
fn stock_from_value(code: &str, v: &Value) -> Stock {
let name = as_string(v.get("name"));
let now = as_f64(v.get("price"));
let low = as_f64(v.get("low"));
let high = as_f64(v.get("high"));
let yesterday = as_f64(v.get("yestclose"));
Stock {
code: code.to_uppercase(),
name,
now,
low,
high,
yesterday,
percent: percent(now, yesterday),
}
}
}
#[async_trait]
impl StockApi for NeteaseApi {
async fn get_stock(&self, code: &str) -> Result<Stock> {
let transformer = NeteaseCommonCodeTransform;
let transformed = transformer.transform(code)?;
let url = format!(
"https://api.money.126.net/data/feed/{},money.api?callback=topstock",
transformed
);
let text = self.client.get(url).send().await?.text().await?;
let row = Self::clean_jsonp(&text);
let root: Value = serde_json::from_str(&row)?;
match root.get(&transformed) {
Some(v) => Ok(Self::stock_from_value(code, v)),
None => Ok(Stock::default_with_code(code)),
}
}
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 = NeteaseCommonCodeTransform;
let transformed = transformer.transforms(&codes)?;
let url = format!(
"https://api.money.126.net/data/feed/{},money.api?callback=topstock",
transformed.join(",")
);
let text = self.client.get(url).send().await?.text().await?;
let row = Self::clean_jsonp(&text);
let root: Value = serde_json::from_str(&row)?;
Ok(codes
.iter()
.map(|code| {
let transformed = transformer.transform(code).unwrap_or_default();
root.get(&transformed)
.map(|v| Self::stock_from_value(code, v))
.unwrap_or_else(|| Stock::default_with_code(code))
})
.collect())
}
async fn search_stocks(&self, key: &str) -> Result<Vec<Stock>> {
let url = format!(
"https://quotes.money.163.com/stocksearch/json.do?type=&count=5&word={}&callback=topstock",
urlencoding::encode(key)
);
let text = self.client.get(url).send().await?.text().await?;
let row = Self::clean_jsonp(&text);
let items: Vec<Value> = serde_json::from_str(&row)?;
let search_transformer = NeteaseSearchCodeTransform;
let codes: Vec<String> = items
.iter()
.map(|item| {
let tag = item.get("tag").and_then(Value::as_str).unwrap_or_default();
let tp = item.get("type").and_then(Value::as_str).unwrap_or_default();
let symbol = item
.get("symbol")
.and_then(Value::as_str)
.unwrap_or_default();
search_transformer.transform(tag, tp, symbol)
})
.filter(|s| !s.is_empty())
.collect();
self.get_stocks(&dedup_codes(&codes)).await
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::stocks::transforms::base::CommonCodeTransform;
use crate::stocks::transforms::netease::NeteaseCommonCodeTransform;
#[test]
fn test_to_api_code() {
let transformer = NeteaseCommonCodeTransform;
assert_eq!(transformer.transform("SH510500").unwrap(), "0510500");
assert_eq!(transformer.transform("SZ399001").unwrap(), "1399001");
assert_eq!(transformer.transform("HKHSI").unwrap(), "hkHSI");
assert_eq!(transformer.transform("USDJI").unwrap(), "US_DJI");
}
#[test]
fn test_stock_from_value() {
let v: Value = serde_json::json!({
"name": "500ETF",
"price": 7.224,
"low": 7.085,
"high": 7.28,
"yestclose": 7.149
});
let s = NeteaseApi::stock_from_value("SH510500", &v);
assert_eq!(s.code, "SH510500");
assert_eq!(s.name, "500ETF");
assert!((s.now - 7.224).abs() < 1e-12);
assert!((s.low - 7.085).abs() < 1e-12);
assert!((s.high - 7.28).abs() < 1e-12);
assert!((s.yesterday - 7.149).abs() < 1e-12);
}
}