use crate::client::AkShareClient;
use crate::error::{Error, Result};
impl AkShareClient {
pub async fn index_stock_cons_sina(&self, symbol: &str) -> Result<Vec<serde_json::Value>> {
if symbol == "000300" {
let count_resp = self
.get("https://vip.stock.finance.sina.com.cn/quotes_service/api/json_v2.php/Market_Center.getHQNodeStockCountSimple")
.query(&[("node", "hs300")])
.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 count: i64 = count_text
.trim_matches(|c: char| !c.is_ascii_digit())
.parse()
.unwrap_or(0);
let pages = (count / 80) + 1;
let mut all = Vec::new();
for page in 1..=pages {
let resp = self
.get("https://vip.stock.finance.sina.com.cn/quotes_service/api/json_v2.php/Market_Center.getHQNodeData")
.query(&[
("page", page.to_string().as_str()),
("num", "80"),
("sort", "symbol"),
("asc", "1"),
("node", "hs300"),
("symbol", ""),
("_s_r_a", "init"),
])
.send()
.await
.map_err(Error::from)?
.error_for_status()
.map_err(Error::from)?;
let items: Vec<serde_json::Value> = resp.json().await.map_err(Error::from)?;
all.extend(items);
}
if all.is_empty() {
return Err(Error::not_found("sina returned no HS300 constituents"));
}
return Ok(all);
}
let response = self
.get("https://vip.stock.finance.sina.com.cn/quotes_service/api/json_v2.php/Market_Center.getHQNodeDataSimple")
.query(&[
("page", "1"),
("num", "3000"),
("sort", "symbol"),
("asc", "1"),
("node", &format!("zhishu_{symbol}")),
("_s_r_a", "setlen"),
])
.send()
.await
.map_err(Error::from)?
.error_for_status()
.map_err(Error::from)?;
let items: Vec<serde_json::Value> = response.json().await.map_err(Error::from)?;
if items.is_empty() {
return Err(Error::not_found(format!(
"sina returned no constituents for {symbol}"
)));
}
Ok(items)
}
pub async fn index_stock_info(&self) -> Result<Vec<IndexInfoItem>> {
let response = self
.get("https://www.joinquant.com/data/dict/indexData")
.send()
.await
.map_err(Error::from)?
.error_for_status()
.map_err(Error::from)?;
let html = response.text().await.map_err(Error::from)?;
let mut items = Vec::new();
for tr_match in html.split("<tr>").skip(1) {
let tds: Vec<&str> = tr_match.split("<td>").skip(1).collect();
if tds.len() >= 3 {
let code = tds[0].split('<').next().unwrap_or("").trim().to_string();
let name = tds[1].split('<').next().unwrap_or("").trim().to_string();
let date = tds[2].split('<').next().unwrap_or("").trim().to_string();
let code_clean = code.split('.').next().unwrap_or(&code).to_string();
if !code_clean.is_empty() && code_clean.chars().all(|c| c.is_ascii_digit()) {
items.push(IndexInfoItem {
code: code_clean,
name,
publish_date: date,
});
}
}
}
if items.is_empty() {
return Err(Error::not_found("joinquant returned no index info"));
}
Ok(items)
}
pub async fn index_stock_cons(&self, symbol: &str) -> Result<Vec<serde_json::Value>> {
Err(Error::upstream(format!(
"index_stock_cons for {symbol} requires HTML scraping with pagination; \
use index_stock_cons_sina() or a dedicated HTML parser"
)))
}
pub async fn index_stock_cons_csindex(&self, _symbol: &str) -> Result<Vec<serde_json::Value>> {
Err(Error::upstream(
"csindex constituents are returned as XLS — not supported in pure Rust",
))
}
pub async fn index_stock_cons_weight_csindex(
&self,
_symbol: &str,
) -> Result<Vec<serde_json::Value>> {
Err(Error::upstream(
"csindex weights are returned as XLS — not supported in pure Rust",
))
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct IndexInfoItem {
pub code: String,
pub name: String,
pub publish_date: String,
}
#[cfg(test)]
mod tests {
#[test]
fn test_placeholder() {
}
}