use crate::client::AkShareClient;
use crate::error::{Error, Result};
use crate::types::{CandlePoint, Row};
use crate::util::{amplitude_pct, apply_change_metrics};
impl AkShareClient {
pub async fn futures_main_sina(&self, symbol: &str, limit: usize) -> Result<Vec<CandlePoint>> {
let url = format!(
"https://stock2.finance.sina.com.cn/futures/api/jsonp.php/var%20_=/InnerFuturesNewService.getDailyKLine?symbol={symbol}&_={}",
chrono::Utc::now().timestamp_millis()
);
let body = self
.get(&url)
.header("Referer", "https://finance.sina.com.cn")
.send()
.await?
.text()
.await?;
let json_str = body
.find("[[")
.and_then(|start| {
body[start..]
.rfind("]]")
.map(|end| &body[start..start + end + 2])
})
.ok_or_else(|| Error::decode("sina futures: invalid JSONP response"))?;
let rows: Vec<Vec<serde_json::Value>> = serde_json::from_str(json_str).unwrap_or_default();
let mut items = Vec::with_capacity(rows.len());
for row in &rows {
if row.len() < 6 {
continue;
}
let date = row[0].as_str().unwrap_or("").to_string();
let open = row[1].as_f64().unwrap_or(0.0);
let high = row[2].as_f64().unwrap_or(0.0);
let low = row[3].as_f64().unwrap_or(0.0);
let close = row[4].as_f64().unwrap_or(0.0);
let volume = row[5].as_f64().unwrap_or(0.0) as i64;
items.push(CandlePoint {
trade_date: date,
open,
close,
high,
low,
volume,
amount: 0.0,
amplitude_pct: amplitude_pct(high, low),
change_pct: 0.0,
change_amount: 0.0,
turnover_pct: 0.0,
});
}
apply_change_metrics(&mut items);
if items.len() > limit {
items = items[items.len() - limit..].to_vec();
}
if items.is_empty() {
return Err(Error::upstream("sina futures: no data returned"));
}
Ok(items)
}
pub async fn futures_symbol_mark(&self) -> Result<Vec<Row>> {
let url = "https://vip.stock.finance.sina.com.cn/quotes_service/view/js/qihuohangqing.js";
let body = self
.get(url)
.header("Referer", "https://vip.stock.finance.sina.com.cn/")
.send()
.await?
.text()
.await?;
let json_str = body
.find('{')
.and_then(|start| {
body[start..]
.find('}')
.map(|end| &body[start..=start + end])
})
.ok_or_else(|| Error::decode("sina symbol mark: invalid JS response"))?;
let data: serde_json::Value = serde_json::from_str(json_str)
.map_err(|_| Error::decode("sina symbol mark: JSON parse error"))?;
let mut items = Vec::new();
for exchange in &["czce", "dce", "shfe", "cffex", "gfex"] {
let Some(arr) = data[exchange].as_array() else {
continue;
};
if arr.len() < 2 {
continue;
}
for entry in arr.iter().skip(1) {
let Some(arr_entry) = entry.as_array() else {
continue;
};
if arr_entry.len() < 2 {
continue;
}
let mut r = Row::new();
r.insert("exchange".into(), serde_json::json!(exchange));
r.insert("symbol".into(), arr_entry[0].clone());
r.insert("mark".into(), arr_entry[1].clone());
items.push(r);
}
}
Ok(items)
}
pub async fn futures_zh_realtime(&self, symbol: &str) -> Result<Vec<Row>> {
let marks = self.futures_symbol_mark().await?;
let mark = marks
.iter()
.find(|r| r.get("symbol").and_then(|v| v.as_str()) == Some(symbol))
.and_then(|r| {
r.get("mark")
.and_then(|v| v.as_str().map(std::string::ToString::to_string))
})
.ok_or_else(|| Error::not_found(format!("sina: unknown futures symbol: {symbol}")))?;
let url = "https://vip.stock.finance.sina.com.cn/quotes_service/api/json_v2.php/Market_Center.getHQFuturesData";
let body = self
.get(url)
.query(&[
("page", "1"),
("sort", "position"),
("asc", "0"),
("node", mark.as_str()),
("base", "futures"),
])
.header("Referer", "https://vip.stock.finance.sina.com.cn/")
.send()
.await?
.text()
.await?;
let data: Vec<serde_json::Value> = serde_json::from_str(&body)
.map_err(|_| Error::decode("sina realtime: JSON parse error"))?;
let mut items = Vec::new();
for entry in &data {
let mut r = Row::new();
let empty = serde_json::Map::new();
for (key, val) in entry.as_object().unwrap_or(&empty) {
r.insert(key.clone(), val.clone());
}
items.push(r);
}
Ok(items)
}
pub async fn futures_zh_spot(&self, symbols: &str, market: &str) -> Result<Vec<Row>> {
let subscribe_list: Vec<String> = symbols
.split(',')
.map(|s| format!("nf_{}", s.trim()))
.collect();
let list_str = subscribe_list.join(",");
let rn = format!("{:x}", chrono::Utc::now().timestamp_millis());
let url = format!("https://hq.sinajs.cn/rn={rn}&list={list_str}");
let body = self
.get(&url)
.header("Referer", "https://vip.stock.finance.sina.com.cn/")
.header("User-Agent", "Mozilla/5.0")
.send()
.await?
.text()
.await?;
let mut items = Vec::new();
for line in body.split(';') {
let line = line.trim();
if line.is_empty() || !line.contains('=') {
continue;
}
let symbol_part = line.split('=').next().unwrap_or("");
let symbol_name = symbol_part.split("hq_str_").last().unwrap_or("");
let nf_symbol = symbol_name.strip_prefix("nf_").unwrap_or(symbol_name);
let value_part = line.split('=').nth(1).unwrap_or("");
let values: Vec<&str> = value_part.trim_matches('"').split(',').collect();
if values.len() < 15 {
continue;
}
let mut r = Row::new();
r.insert("symbol".into(), serde_json::json!(nf_symbol));
if market == "CF" {
r.insert("time".into(), serde_json::json!(values[0]));
r.insert("open".into(), serde_json::json!(values[1]));
r.insert("high".into(), serde_json::json!(values[2]));
r.insert("low".into(), serde_json::json!(values[3]));
r.insert("last_close".into(), serde_json::json!(values[4]));
r.insert("bid_price".into(), serde_json::json!(values[5]));
r.insert("ask_price".into(), serde_json::json!(values[6]));
r.insert("current_price".into(), serde_json::json!(values[7]));
r.insert("avg_price".into(), serde_json::json!(values[8]));
r.insert("last_settle_price".into(), serde_json::json!(values[9]));
r.insert("buy_vol".into(), serde_json::json!(values[10]));
r.insert("sell_vol".into(), serde_json::json!(values[11]));
r.insert("hold".into(), serde_json::json!(values[12]));
r.insert("volume".into(), serde_json::json!(values[13]));
} else {
r.insert("open".into(), serde_json::json!(values[0]));
r.insert("high".into(), serde_json::json!(values[1]));
r.insert("low".into(), serde_json::json!(values[2]));
r.insert("current_price".into(), serde_json::json!(values[3]));
r.insert("volume".into(), serde_json::json!(values[4]));
r.insert("amount".into(), serde_json::json!(values[5]));
r.insert("hold".into(), serde_json::json!(values[6]));
if values.len() > 32 {
r.insert("time".into(), serde_json::json!(values[32]));
}
}
items.push(r);
}
Ok(items)
}
pub async fn futures_zh_minute_sina(&self, symbol: &str, period: &str) -> Result<Vec<Row>> {
let url = "https://stock2.finance.sina.com.cn/futures/api/jsonp.php/=/InnerFuturesNewService.getFewMinLine";
let body = self
.get(url)
.query(&[("symbol", symbol), ("type", period)])
.header("Referer", "https://finance.sina.com.cn")
.send()
.await?
.text()
.await?;
let json_str = body
.find("=(")
.and_then(|start| {
body[start..]
.rfind(");")
.map(|end| &body[start + 2..start + end])
})
.ok_or_else(|| Error::decode("sina minute kline: invalid JSONP response"))?;
let data: Vec<Vec<serde_json::Value>> = serde_json::from_str(json_str)
.map_err(|_| Error::decode("sina minute kline: JSON parse error"))?;
let mut items = Vec::new();
for row in &data {
if row.len() < 7 {
continue;
}
let mut r = Row::new();
r.insert("datetime".into(), row[0].clone());
r.insert("open".into(), row[1].clone());
r.insert("high".into(), row[2].clone());
r.insert("low".into(), row[3].clone());
r.insert("close".into(), row[4].clone());
r.insert("volume".into(), row[5].clone());
r.insert("hold".into(), row[6].clone());
items.push(r);
}
Ok(items)
}
pub async fn futures_zh_daily_sina(&self, symbol: &str) -> Result<Vec<Row>> {
let date = chrono::Utc::now().format("%Y%m%d").to_string();
let date_formatted = format!("{}_{}_{}", &date[..4], &date[4..6], &date[6..8]);
let url = "https://stock2.finance.sina.com.cn/futures/api/jsonp.php/var%20_dummy=/InnerFuturesNewService.getDailyKLine";
let body = self
.get(url)
.query(&[("symbol", symbol), ("type", &date_formatted)])
.header("Referer", "https://finance.sina.com.cn")
.send()
.await?
.text()
.await?;
let json_str = body
.find("=(")
.and_then(|start| {
body[start..]
.rfind(");")
.map(|end| &body[start + 2..start + end])
})
.ok_or_else(|| Error::decode("sina daily kline: invalid JSONP response"))?;
let data: Vec<Vec<serde_json::Value>> = serde_json::from_str(json_str)
.map_err(|_| Error::decode("sina daily kline: JSON parse error"))?;
let mut items = Vec::new();
for row in &data {
if row.len() < 8 {
continue;
}
let mut r = Row::new();
r.insert("date".into(), row[0].clone());
r.insert("open".into(), row[1].clone());
r.insert("high".into(), row[2].clone());
r.insert("low".into(), row[3].clone());
r.insert("close".into(), row[4].clone());
r.insert("volume".into(), row[5].clone());
r.insert("hold".into(), row[6].clone());
r.insert("settle".into(), row[7].clone());
items.push(r);
}
Ok(items)
}
}