use serde::Deserialize;
use crate::client::AkShareClient;
use crate::error::{Error, Result};
use crate::types::{SwAnalysisPoint, SwResearchComponent, SwResearchHistPoint, SwResearchRealtime};
#[derive(Debug, Deserialize)]
struct SwTrendEnvelope {
data: Option<Vec<SwTrendItem>>,
}
#[derive(Debug, Deserialize)]
struct SwTrendItem {
#[serde(default)]
swindexcode: String,
#[serde(default)]
bargaindate: String,
#[serde(default)]
openindex: Option<f64>,
#[serde(default)]
maxindex: Option<f64>,
#[serde(default)]
minindex: Option<f64>,
#[serde(default)]
closeindex: Option<f64>,
#[serde(default)]
bargainamount: Option<f64>,
#[serde(default)]
bargainsum: Option<f64>,
}
#[derive(Debug, Deserialize)]
struct SwComponentEnvelope {
data: Option<SwComponentData>,
}
#[derive(Debug, Deserialize)]
struct SwComponentData {
results: Option<Vec<SwComponentItem>>,
}
#[derive(Debug, Deserialize)]
struct SwComponentItem {
#[serde(default)]
stockcode: String,
#[serde(default)]
stockname: String,
#[serde(default)]
newweight: Option<f64>,
#[serde(default)]
beginningdate: String,
}
#[derive(Debug, Deserialize)]
struct SwCurrentEnvelope {
data: Option<SwCurrentData>,
}
#[derive(Debug, Deserialize)]
struct SwCurrentData {
#[allow(dead_code)]
count: Option<i64>,
results: Option<Vec<Vec<serde_json::Value>>>,
}
#[derive(Debug, Deserialize)]
struct SwAnalysisEnvelope {
data: Option<SwAnalysisData>,
}
#[derive(Debug, Deserialize)]
struct SwAnalysisData {
#[allow(dead_code)]
count: Option<i64>,
results: Option<Vec<SwAnalysisItem>>,
}
#[derive(Debug, Deserialize)]
struct SwAnalysisItem {
#[serde(default)]
swindexcode: String,
#[serde(default)]
swindexname: String,
#[serde(default)]
bargaindate: String,
#[serde(default)]
closeindex: Option<f64>,
#[serde(default)]
bargainamount: Option<f64>,
#[serde(default)]
markup: Option<f64>,
#[serde(default)]
turnoverrate: Option<f64>,
#[serde(default)]
pe: Option<f64>,
#[serde(default)]
pb: Option<f64>,
#[serde(default)]
meanprice: Option<f64>,
#[serde(default)]
bargainsumrate: Option<f64>,
#[serde(default)]
negotiablessharesum1: Option<f64>,
#[serde(default)]
negotiablessharesum2: Option<f64>,
#[serde(default)]
dp: Option<f64>,
}
#[derive(Debug, Deserialize)]
struct SwPageListEnvelope {
data: Option<SwPageListData>,
}
#[derive(Debug, Deserialize)]
struct SwPageListData {
list: Option<Vec<SwPageListItem>>,
}
#[derive(Debug, Deserialize)]
#[allow(non_snake_case)]
struct SwPageListItem {
#[serde(default)]
swIndexCode: String,
#[serde(default)]
swIndexName: String,
#[serde(default)]
lastCloseIndex: Option<f64>,
#[serde(default)]
lastMarkup: Option<f64>,
#[serde(default)]
yearMarkup: Option<f64>,
}
impl AkShareClient {
pub async fn index_hist_sw(
&self,
symbol: &str,
period: &str,
) -> Result<Vec<SwResearchHistPoint>> {
let period_upper = match period {
"day" => "DAY",
"week" => "WEEK",
"month" => "MONTH",
_ => {
return Err(Error::invalid_input(format!(
"unsupported period: {period}"
)));
}
};
let response = self
.get("https://www.swsresearch.com/institute-sw/api/index_publish/trend/")
.query(&[("swindexcode", symbol), ("period", period_upper)])
.send()
.await
.map_err(Error::from)?
.error_for_status()
.map_err(Error::from)?;
let payload: SwTrendEnvelope = response.json().await.map_err(Error::from)?;
let items = payload.data.unwrap_or_default();
let points: Vec<SwResearchHistPoint> = items
.into_iter()
.map(|item| SwResearchHistPoint {
code: item.swindexcode,
date: item.bargaindate,
open: item.openindex.unwrap_or(0.0),
close: item.closeindex.unwrap_or(0.0),
high: item.maxindex.unwrap_or(0.0),
low: item.minindex.unwrap_or(0.0),
volume: item.bargainamount.unwrap_or(0.0),
amount: item.bargainsum.unwrap_or(0.0),
})
.collect();
if points.is_empty() {
return Err(Error::not_found("swsresearch returned no history data"));
}
Ok(points)
}
pub async fn index_min_sw(&self, symbol: &str) -> Result<Vec<SwMinPoint>> {
#[derive(Deserialize)]
struct MinEnvelope {
data: Option<Vec<MinItem>>,
}
#[derive(Deserialize)]
struct MinItem {
#[serde(default)]
l1: String,
#[serde(default)]
l2: String,
#[serde(default)]
l8: Option<f64>,
#[serde(default)]
trading_date: String,
#[serde(default)]
trading_time: String,
}
let response = self
.get("https://www.swsresearch.com/institute-sw/api/index_publish/details/timelines/")
.query(&[("swindexcode", symbol)])
.send()
.await
.map_err(Error::from)?
.error_for_status()
.map_err(Error::from)?;
let payload: MinEnvelope = response.json().await.map_err(Error::from)?;
let items = payload.data.unwrap_or_default();
let points: Vec<SwMinPoint> = items
.into_iter()
.map(|item| SwMinPoint {
code: item.l1,
name: item.l2,
price: item.l8.unwrap_or(0.0),
date: item.trading_date,
time: item.trading_time,
})
.collect();
if points.is_empty() {
return Err(Error::not_found("swsresearch returned no intraday data"));
}
Ok(points)
}
pub async fn index_component_sw(&self, symbol: &str) -> Result<Vec<SwResearchComponent>> {
let response = self
.get("https://www.swsresearch.com/institute-sw/api/index_publish/details/component_stocks/")
.query(&[("swindexcode", symbol), ("page", "1"), ("page_size", "10000")])
.send()
.await
.map_err(Error::from)?
.error_for_status()
.map_err(Error::from)?;
let payload: SwComponentEnvelope = response.json().await.map_err(Error::from)?;
let items = payload.data.and_then(|d| d.results).unwrap_or_default();
let components: Vec<SwResearchComponent> = items
.into_iter()
.map(|item| SwResearchComponent {
symbol: item.stockcode,
name: item.stockname,
weight: item.newweight.unwrap_or(0.0),
date: item.beginningdate,
})
.collect();
if components.is_empty() {
return Err(Error::not_found("swsresearch returned no component data"));
}
Ok(components)
}
pub async fn index_realtime_sw(&self, symbol: &str) -> Result<Vec<SwResearchRealtime>> {
if symbol == "大类风格指数" || symbol == "金创指数" {
return self.index_realtime_sw_page_list(symbol).await;
}
let response = self
.get("https://www.swsresearch.com/institute-sw/api/index_publish/current/")
.query(&[("page", "1"), ("page_size", "200"), ("indextype", symbol)])
.send()
.await
.map_err(Error::from)?
.error_for_status()
.map_err(Error::from)?;
let payload: SwCurrentEnvelope = response.json().await.map_err(Error::from)?;
let results = payload.data.and_then(|d| d.results).unwrap_or_default();
let items: Vec<SwResearchRealtime> = results
.into_iter()
.filter_map(|row| {
if row.len() < 9 {
return None;
}
Some(SwResearchRealtime {
code: row[0].as_str()?.to_string(),
name: row[1].as_str().unwrap_or("").to_string(),
prev_close: row[2].as_f64().unwrap_or(0.0),
change_pct: 0.0, year_change_pct: 0.0,
})
})
.collect();
if items.is_empty() {
return Err(Error::not_found("swsresearch returned no realtime data"));
}
Ok(items)
}
pub async fn index_analysis_daily_sw(
&self,
symbol: &str,
start_date: &str,
end_date: &str,
) -> Result<Vec<SwAnalysisPoint>> {
let sd = format_dash_date(start_date);
let ed = format_dash_date(end_date);
let response = self
.get("https://www.swsresearch.com/institute-sw/api/index_analysis/index_analysis_report/")
.query(&[
("page", "1"),
("page_size", "50"),
("index_type", symbol),
("start_date", sd.as_str()),
("end_date", ed.as_str()),
("type", "DAY"),
("swindexcode", "all"),
])
.send()
.await
.map_err(Error::from)?
.error_for_status()
.map_err(Error::from)?;
fetch_sw_analysis(self, &response.text().await.map_err(Error::from)?).await
}
pub async fn index_analysis_week_month_sw(&self, symbol: &str) -> Result<Vec<String>> {
let period = symbol.to_uppercase();
let response = self
.get("https://www.swsresearch.com/institute-sw/api/index_analysis/week_month_datetime/")
.query(&[("type", period.as_str())])
.send()
.await
.map_err(Error::from)?
.error_for_status()
.map_err(Error::from)?;
#[derive(Deserialize)]
struct DateEnvelope {
data: Option<Vec<DateItem>>,
}
#[derive(Deserialize)]
struct DateItem {
#[serde(default)]
bargaindate: String,
}
let payload: DateEnvelope = response.json().await.map_err(Error::from)?;
let dates: Vec<String> = payload
.data
.unwrap_or_default()
.into_iter()
.map(|d| d.bargaindate)
.collect();
if dates.is_empty() {
return Err(Error::not_found("swsresearch returned no date series"));
}
Ok(dates)
}
pub async fn index_analysis_weekly_sw(
&self,
symbol: &str,
date: &str,
) -> Result<Vec<SwAnalysisPoint>> {
let d = format_dash_date(date);
let response = self
.get("https://www.swsresearch.com/institute-sw/api/index_analysis/index_analysis_reports/")
.query(&[
("page", "1"),
("page_size", "50"),
("index_type", symbol),
("bargaindate", d.as_str()),
("type", "WEEK"),
("swindexcode", "all"),
])
.send()
.await
.map_err(Error::from)?
.error_for_status()
.map_err(Error::from)?;
fetch_sw_analysis(self, &response.text().await.map_err(Error::from)?).await
}
pub async fn index_analysis_monthly_sw(
&self,
symbol: &str,
date: &str,
) -> Result<Vec<SwAnalysisPoint>> {
let d = format_dash_date(date);
let response = self
.get("https://www.swsresearch.com/institute-sw/api/index_analysis/index_analysis_reports/")
.query(&[
("page", "1"),
("page_size", "50"),
("index_type", symbol),
("bargaindate", d.as_str()),
("type", "MONTH"),
("swindexcode", "all"),
])
.send()
.await
.map_err(Error::from)?
.error_for_status()
.map_err(Error::from)?;
fetch_sw_analysis(self, &response.text().await.map_err(Error::from)?).await
}
async fn index_realtime_sw_page_list(&self, symbol: &str) -> Result<Vec<SwResearchRealtime>> {
let response = self
.post("https://www.swsresearch.com/insWechatSw/dflgOrJcIndex/pageList")
.json(&serde_json::json!({
"pageNo": 1,
"pageSize": 50,
"indexTypeName": symbol,
"sortField": "",
"rule": "",
"indexType": 1,
}))
.send()
.await
.map_err(Error::from)?
.error_for_status()
.map_err(Error::from)?;
let payload: SwPageListEnvelope = response.json().await.map_err(Error::from)?;
let items = payload.data.and_then(|d| d.list).unwrap_or_default();
let result: Vec<SwResearchRealtime> = items
.into_iter()
.map(|item| SwResearchRealtime {
code: item.swIndexCode,
name: item.swIndexName,
prev_close: item.lastCloseIndex.unwrap_or(0.0),
change_pct: item.lastMarkup.unwrap_or(0.0),
year_change_pct: item.yearMarkup.unwrap_or(0.0),
})
.collect();
if result.is_empty() {
return Err(Error::not_found("swsresearch returned no page list data"));
}
Ok(result)
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct SwMinPoint {
pub code: String,
pub name: String,
pub price: f64,
pub date: String,
pub time: String,
}
async fn fetch_sw_analysis(_client: &AkShareClient, body: &str) -> Result<Vec<SwAnalysisPoint>> {
let payload: SwAnalysisEnvelope = serde_json::from_str(body)?;
let items = payload.data.and_then(|d| d.results).unwrap_or_default();
let points: Vec<SwAnalysisPoint> = items
.into_iter()
.map(|item| SwAnalysisPoint {
code: item.swindexcode,
name: item.swindexname,
date: item.bargaindate,
close: item.closeindex.unwrap_or(0.0),
volume: item.bargainamount.unwrap_or(0.0),
change_pct: item.markup.unwrap_or(0.0),
turnover_rate: item.turnoverrate.unwrap_or(0.0),
pe: item.pe.unwrap_or(0.0),
pb: item.pb.unwrap_or(0.0),
avg_price: item.meanprice.unwrap_or(0.0),
amount_ratio: item.bargainsumrate.unwrap_or(0.0),
circ_mv: item.negotiablessharesum1.unwrap_or(0.0),
avg_circ_mv: item.negotiablessharesum2.unwrap_or(0.0),
dividend_yield: item.dp.unwrap_or(0.0),
})
.collect();
if points.is_empty() {
return Err(Error::not_found("swsresearch returned no analysis data"));
}
Ok(points)
}
fn format_dash_date(d: &str) -> String {
let d = d.trim();
if d.len() == 8 && d.chars().all(|c| c.is_ascii_digit()) {
format!("{}-{}-{}", &d[..4], &d[4..6], &d[6..8])
} else {
d.to_string()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_format_dash_date() {
assert_eq!(format_dash_date("20241025"), "2024-10-25");
assert_eq!(format_dash_date("2024-10-25"), "2024-10-25");
}
}