use crate::client::AkShareClient;
use crate::error::{Error, Result};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IndexSpotEm {
pub code: String,
pub name: String,
#[serde(default)]
pub latest_price: Option<f64>,
#[serde(default)]
pub change_pct: Option<f64>,
#[serde(default)]
pub change_amount: Option<f64>,
#[serde(default)]
pub volume: Option<f64>,
#[serde(default)]
pub amount: Option<f64>,
#[serde(default)]
pub high: Option<f64>,
#[serde(default)]
pub low: Option<f64>,
#[serde(default)]
pub open: Option<f64>,
#[serde(default)]
pub prev_close: Option<f64>,
#[serde(default)]
pub internal_id: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IndexDailyCandle {
pub date: String,
pub open: f64,
pub high: f64,
pub low: f64,
pub close: f64,
#[serde(default)]
pub volume: Option<f64>,
#[serde(default)]
pub amount: Option<f64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IndexSpotSina {
pub code: String,
pub name: String,
#[serde(default)]
pub latest_price: Option<f64>,
#[serde(default)]
pub change_amount: Option<f64>,
#[serde(default)]
pub change_pct: Option<f64>,
#[serde(default)]
pub prev_close: Option<f64>,
#[serde(default)]
pub open: Option<f64>,
#[serde(default)]
pub high: Option<f64>,
#[serde(default)]
pub low: Option<f64>,
#[serde(default)]
pub volume: Option<f64>,
#[serde(default)]
pub amount: Option<f64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CsIndexValue {
pub date: String,
#[serde(default)]
pub index_code: Option<String>,
#[serde(default)]
pub index_name: Option<String>,
#[serde(default)]
pub open: Option<f64>,
#[serde(default)]
pub high: Option<f64>,
#[serde(default)]
pub low: Option<f64>,
#[serde(default)]
pub close: Option<f64>,
#[serde(default)]
pub change: Option<f64>,
#[serde(default)]
pub change_pct: Option<f64>,
#[serde(default)]
pub volume: Option<f64>,
#[serde(default)]
pub amount: Option<f64>,
#[serde(default)]
pub sample_count: Option<f64>,
#[serde(default)]
pub pe_ttm: Option<f64>,
}
impl AkShareClient {
pub async fn stock_zh_index_spot_em(&self) -> Result<Vec<IndexSpotEm>> {
let response = self
.get("https://push2.eastmoney.com/api/qt/clist/get")
.query(&[
("pn", "1"),
("pz", "5000"),
("po", "1"),
("np", "1"),
("ut", "bd1d9ddb04089700cf9c27f6f7426281"),
("fltt", "2"),
("invt", "2"),
("fid", "f3"),
("fs", "m:1+s:2,m:0+t:5,m:0+t:6,m:0+t:80,m:1+t:2,m:1+t:23"),
("fields", "f2,f3,f4,f5,f6,f12,f13,f14,f15,f16,f17,f18"),
])
.send()
.await
.map_err(Error::from)?
.error_for_status()
.map_err(Error::from)?;
#[derive(Deserialize)]
struct Env {
data: Option<EnvData>,
}
#[derive(Deserialize)]
struct EnvData {
diff: Option<Vec<serde_json::Value>>,
}
let payload: Env = response.json().await.map_err(Error::from)?;
let diff = payload
.data
.and_then(|d| d.diff)
.ok_or_else(|| Error::upstream("eastmoney index spot missing data"))?;
let items: Vec<IndexSpotEm> = diff
.iter()
.filter_map(|item| {
let code = item.get("f12")?.as_str()?.to_string();
let name = item.get("f14")?.as_str()?.to_string();
Some(IndexSpotEm {
code,
name,
latest_price: item.get("f2").and_then(|v| v.as_f64()),
change_pct: item.get("f3").and_then(|v| v.as_f64()),
change_amount: item.get("f4").and_then(|v| v.as_f64()),
volume: item.get("f5").and_then(|v| v.as_f64()),
amount: item.get("f6").and_then(|v| v.as_f64()),
high: item.get("f15").and_then(|v| v.as_f64()),
low: item.get("f16").and_then(|v| v.as_f64()),
open: item.get("f17").and_then(|v| v.as_f64()),
prev_close: item.get("f18").and_then(|v| v.as_f64()),
internal_id: item
.get("f13")
.and_then(|v| v.as_str())
.map(|s| s.to_string()),
})
})
.collect();
if items.is_empty() {
return Err(Error::not_found("eastmoney returned no index spot data"));
}
Ok(items)
}
pub async fn stock_zh_index_daily_em(
&self,
symbol: &str,
start_date: &str,
end_date: &str,
) -> Result<Vec<IndexDailyCandle>> {
let market = if symbol.starts_with('0') || symbol.starts_with('3') {
"1"
} else {
"0"
};
let secid = format!("{}.{}", market, symbol);
let response = self
.get("https://push2his.eastmoney.com/api/qt/stock/kline/get")
.query(&[
("secid", secid.as_str()),
("fields1", "f1,f2,f3,f4,f5,f6"),
("fields2", "f51,f52,f53,f54,f55,f56,f57,f58,f59,f60,f61"),
("klt", "101"),
("fqt", "1"),
("beg", start_date),
("end", end_date),
("lmt", "1000000"),
])
.send()
.await
.map_err(Error::from)?
.error_for_status()
.map_err(Error::from)?;
#[derive(Deserialize)]
struct Env {
data: Option<EnvData>,
}
#[derive(Deserialize)]
struct EnvData {
klines: Option<Vec<String>>,
}
let payload: Env = response.json().await.map_err(Error::from)?;
let klines = payload
.data
.and_then(|d| d.klines)
.ok_or_else(|| Error::upstream("index daily kline missing data"))?;
let items: Vec<IndexDailyCandle> = klines
.iter()
.filter_map(|line| {
let parts: Vec<&str> = line.split(',').collect();
if parts.len() < 7 {
return None;
}
Some(IndexDailyCandle {
date: parts[0].to_string(),
open: parts[1].parse().unwrap_or(0.0),
close: parts[2].parse().unwrap_or(0.0),
high: parts[3].parse().unwrap_or(0.0),
low: parts[4].parse().unwrap_or(0.0),
volume: parts[5].parse().ok(),
amount: parts[6].parse().ok(),
})
})
.collect();
if items.is_empty() {
return Err(Error::not_found("index daily returned no data"));
}
Ok(items)
}
pub async fn stock_zh_index_daily_tx(
&self,
symbol: &str,
start_date: &str,
end_date: &str,
) -> Result<Vec<IndexDailyCandle>> {
let tx_symbol = if symbol.starts_with("sh") || symbol.starts_with("sz") {
symbol.to_string()
} else if symbol.starts_with('0') {
format!("sh{}", symbol)
} else {
format!("sz{}", symbol)
};
let url = format!(
"https://web.ifzq.gtimg.cn/appstock/app/fqkline/get?param={},day,{},{},640,",
tx_symbol, start_date, end_date
);
#[derive(Deserialize)]
struct Resp {
data: Option<serde_json::Value>,
}
let resp: Resp = self.get(&url).send().await?.json().await?;
let data = resp
.data
.ok_or_else(|| Error::upstream("empty tencent index data"))?;
let ts_lower = tx_symbol.to_lowercase();
let klines = data
.get(&ts_lower)
.and_then(|v| v.get("day"))
.and_then(|v| v.as_array())
.cloned()
.unwrap_or_default();
let items: Vec<IndexDailyCandle> = klines
.iter()
.filter_map(|entry| {
let arr = entry.as_array()?;
if arr.len() < 6 {
return None;
}
Some(IndexDailyCandle {
date: arr[0].as_str().unwrap_or("").to_string(),
open: arr[1].as_f64().unwrap_or(0.0),
close: arr[2].as_f64().unwrap_or(0.0),
high: arr[3].as_f64().unwrap_or(0.0),
low: arr[4].as_f64().unwrap_or(0.0),
volume: arr.get(5).and_then(|v| v.as_f64()),
amount: None,
})
})
.collect();
if items.is_empty() {
return Err(Error::not_found("tencent index daily returned no data"));
}
Ok(items)
}
pub async fn stock_zh_index_spot_sina(&self) -> Result<Vec<IndexSpotSina>> {
let count_url = "https://vip.stock.finance.sina.com.cn/quotes_service/api/json_v2.php/Market_Center.getNameCount";
let count_resp = self
.get(count_url)
.query(&[("node", "hs_s")])
.send()
.await
.map_err(Error::from)?
.error_for_status()
.map_err(Error::from)?;
let count_text = count_resp.text().await.map_err(Error::from)?;
let total: i64 = count_text
.chars()
.filter(|c| c.is_ascii_digit())
.collect::<String>()
.parse()
.unwrap_or(0);
let page_count = ((total as f64) / 80.0).ceil() as i64;
let list_url = "https://vip.stock.finance.sina.com.cn/quotes_service/api/json_v2.php/Market_Center.getHQNodeData";
let mut all_indices = Vec::new();
for page in 1..=page_count.min(5) {
let page_str = page.to_string();
let response = self
.get(list_url)
.query(&[
("page", page_str.as_str()),
("num", "80"),
("sort", "symbol"),
("asc", "1"),
("node", "hs_s"),
("symbol", ""),
("_s_r_a", "page"),
])
.send()
.await
.map_err(Error::from)?
.error_for_status()
.map_err(Error::from)?;
let data: Vec<serde_json::Value> = response.json().await.map_err(Error::from)?;
for item in &data {
let symbol = item
.get("symbol")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let name = item
.get("name")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
all_indices.push(IndexSpotSina {
code: symbol,
name,
latest_price: parse_idx_f64(item, "trade"),
change_amount: parse_idx_f64(item, "pricechange"),
change_pct: parse_idx_f64(item, "changepercent"),
prev_close: parse_idx_f64(item, "settlement"),
open: parse_idx_f64(item, "open"),
high: parse_idx_f64(item, "high"),
low: parse_idx_f64(item, "low"),
volume: parse_idx_f64(item, "volume"),
amount: parse_idx_f64(item, "amount"),
});
}
}
if all_indices.is_empty() {
return Err(Error::not_found("sina returned no index spot data"));
}
Ok(all_indices)
}
pub async fn stock_zh_index_value_csindex(&self, symbol: &str) -> Result<Vec<CsIndexValue>> {
let url = "https://www.csindex.com.cn/csindex-home/perf/index-perf";
let start_date = "20000101";
let end_date = chrono::Utc::now().format("%Y%m%d").to_string();
let response = self
.get(url)
.query(&[
("indexCode", symbol),
("startDate", start_date),
("endDate", end_date.as_str()),
])
.send()
.await
.map_err(Error::from)?
.error_for_status()
.map_err(Error::from)?;
#[derive(Deserialize)]
struct Env {
data: Option<Vec<serde_json::Value>>,
}
let payload: Env = response.json().await.map_err(Error::from)?;
let data = payload
.data
.ok_or_else(|| Error::upstream("CSIndex value missing data"))?;
let items: Vec<CsIndexValue> = data
.iter()
.filter_map(|item| {
let date = item.get(0)?.as_str()?.to_string();
Some(CsIndexValue {
date,
index_code: item.get(1).and_then(|v| v.as_str()).map(|s| s.to_string()),
index_name: item.get(3).and_then(|v| v.as_str()).map(|s| s.to_string()),
open: item.get(6).and_then(|v| v.as_f64()),
high: item.get(7).and_then(|v| v.as_f64()),
low: item.get(8).and_then(|v| v.as_f64()),
close: item.get(9).and_then(|v| v.as_f64()),
change: item.get(10).and_then(|v| v.as_f64()),
change_pct: item.get(11).and_then(|v| v.as_f64()),
volume: item.get(12).and_then(|v| v.as_f64()),
amount: item.get(13).and_then(|v| v.as_f64()),
sample_count: item.get(14).and_then(|v| v.as_f64()),
pe_ttm: item.get(15).and_then(|v| v.as_f64()),
})
})
.collect();
if items.is_empty() {
return Err(Error::not_found("CSIndex value returned no data"));
}
Ok(items)
}
}
fn parse_idx_f64(item: &serde_json::Value, key: &str) -> Option<f64> {
item.get(key).and_then(|v| {
if let Some(n) = v.as_f64() {
Some(n)
} else if let Some(s) = v.as_str() {
s.parse().ok()
} else {
None
}
})
}