use crate::client::AkShareClient;
use crate::error::{Error, Result};
use crate::types::BondSnapshot;
impl AkShareClient {
pub async fn bond_spot_deal(&self) -> Result<Vec<crate::types::Row>> {
let url = "https://www.chinamoney.com.cn/ags/ms/cm-u-bk-currency/SptDl?lang=CN&pageNo=1&pageSize=200";
let body = self
.get(url)
.header("User-Agent", "Mozilla/5.0")
.send()
.await?
.text()
.await?;
let resp: serde_json::Value = serde_json::from_str(&body)?;
let records = resp["records"].as_array().cloned().unwrap_or_default();
let mut items = Vec::new();
let empty_map = serde_json::Map::new();
for rec in &records {
let mut row = crate::types::Row::new();
for (k, v) in rec.as_object().unwrap_or(&empty_map) {
row.insert(k.clone(), v.clone());
}
if !row.is_empty() {
items.push(row);
}
}
Ok(items)
}
pub async fn bond_spot_quote(&self) -> Result<Vec<crate::types::Row>> {
let url = "https://www.chinamoney.com.cn/ags/ms/cm-u-bk-currency/SptQut?lang=CN&pageNo=1&pageSize=200";
let body = self
.get(url)
.header("User-Agent", "Mozilla/5.0")
.send()
.await?
.text()
.await?;
let resp: serde_json::Value = serde_json::from_str(&body)?;
let records = resp["records"].as_array().cloned().unwrap_or_default();
let mut items = Vec::new();
let empty_map = serde_json::Map::new();
for rec in &records {
let mut row = crate::types::Row::new();
for (k, v) in rec.as_object().unwrap_or(&empty_map) {
row.insert(k.clone(), v.clone());
}
if !row.is_empty() {
items.push(row);
}
}
Ok(items)
}
pub async fn bond_spot_rates(&self, limit: usize) -> Result<Vec<BondSnapshot>> {
let page_size = limit.clamp(1, 500).to_string();
let response = self
.get("https://datacenter.eastmoney.com/api/data/get")
.query(&[
("type", "RPTA_WEB_TREASURYYIELD"),
("sty", "ALL"),
("st", "SOLAR_DATE"),
("sr", "-1"),
("token", "894050c76af8597a853f5b408b759f5d"),
("p", "1"),
("ps", page_size.as_str()),
("pageNo", "1"),
("pageNum", "1"),
])
.send()
.await
.map_err(Error::from)?
.error_for_status()
.map_err(Error::from)?;
let payload: serde_json::Value = response.json().await.map_err(Error::from)?;
let data = payload
.get("result")
.and_then(|r| r.get("data"))
.and_then(|d| d.as_array())
.cloned()
.unwrap_or_default();
if data.is_empty() {
return Err(Error::not_found(
"eastmoney returned no bond yield curve data",
));
}
let tenors: [(&str, &str); 4] = [
("EMM00588704", "2Y"),
("EMM00166462", "5Y"),
("EMM00166466", "10Y"),
("EMM00166469", "30Y"),
];
let mut items = Vec::new();
for v in &data {
let date = v.get("SOLAR_DATE").and_then(|x| x.as_str()).unwrap_or("");
if date.is_empty() {
continue;
}
let date_short = date.get(..10).unwrap_or(date).to_string();
for (field, tenor_label) in &tenors {
let yield_rate = v.get(*field).and_then(serde_json::Value::as_f64);
if let Some(rate) = yield_rate {
items.push(BondSnapshot {
symbol: format!("CNBD{tenor_label}"),
name: format!("China Bond {tenor_label}"),
date: date_short.clone(),
close: 0.0,
change_pct: 0.0,
yield_rate: Some(rate),
credit_rating: None,
});
}
}
}
if items.is_empty() {
return Err(Error::not_found(
"eastmoney returned no bond spot rate items",
));
}
Ok(items)
}
}
#[cfg(test)]
mod tests {
use serde_json::json;
#[test]
fn test_yield_extraction() {
let row = json!({
"SOLAR_DATE": "2025-01-15 00:00:00",
"EMM00588704": 1.85,
"EMM00166462": 2.10,
"EMM00166466": 2.45,
"EMM00166469": 2.80
});
let date = row.get("SOLAR_DATE").and_then(|x| x.as_str()).unwrap_or("");
assert_eq!(date.get(..10).unwrap_or(date), "2025-01-15");
let tenors = [
("EMM00588704", "2Y"),
("EMM00166462", "5Y"),
("EMM00166466", "10Y"),
("EMM00166469", "30Y"),
];
for (field, label) in &tenors {
let rate = row.get(*field).and_then(serde_json::Value::as_f64);
assert!(rate.is_some(), "missing yield for {label}");
assert!(rate.unwrap() > 0.0);
}
}
#[test]
fn test_missing_date_skipped() {
let row = json!({
"SOLAR_DATE": "",
"EMM00588704": 1.85
});
let date = row.get("SOLAR_DATE").and_then(|x| x.as_str()).unwrap_or("");
assert!(date.is_empty());
}
}