use crate::client::AkShareClient;
use crate::error::{Error, Result};
use crate::types::MacroDataPoint;
impl AkShareClient {
pub async fn currency_latest(
&self,
base: &str,
symbols: &str,
api_key: &str,
) -> Result<Vec<MacroDataPoint>> {
let url = "https://api.currencyscoop.com/v1/latest";
let resp: serde_json::Value = self
.get(url)
.query(&[("base", base), ("symbols", symbols), ("api_key", api_key)])
.send()
.await?
.json()
.await?;
let response = resp
.get("response")
.ok_or_else(|| Error::decode("currency_latest: no response field"))?;
let rates = response
.get("rates")
.and_then(|r| r.as_object())
.ok_or_else(|| Error::decode("currency_latest: no rates"))?;
let date = response
.get("date")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let mut items = Vec::new();
for (currency, rate) in rates {
items.push(MacroDataPoint {
date: date.clone(),
value: rate.as_f64().unwrap_or(0.0),
name: format!("{}/{}", base, currency),
});
}
Ok(items)
}
pub async fn currency_history(
&self,
base: &str,
date: &str,
symbols: &str,
api_key: &str,
) -> Result<Vec<MacroDataPoint>> {
let url = "https://api.currencyscoop.com/v1/historical";
let resp: serde_json::Value = self
.get(url)
.query(&[
("base", base),
("date", date),
("symbols", symbols),
("api_key", api_key),
])
.send()
.await?
.json()
.await?;
let response = resp
.get("response")
.ok_or_else(|| Error::decode("currency_history: no response field"))?;
let rates = response
.get("rates")
.and_then(|r| r.as_object())
.ok_or_else(|| Error::decode("currency_history: no rates"))?;
let mut items = Vec::new();
for (currency, rate) in rates {
items.push(MacroDataPoint {
date: date.to_string(),
value: rate.as_f64().unwrap_or(0.0),
name: format!("{}/{}", base, currency),
});
}
Ok(items)
}
pub async fn currency_time_series(
&self,
base: &str,
start_date: &str,
end_date: &str,
symbols: &str,
api_key: &str,
) -> Result<Vec<MacroDataPoint>> {
let url = "https://api.currencyscoop.com/v1/timeseries";
let resp: serde_json::Value = self
.get(url)
.query(&[
("base", base),
("start_date", start_date),
("end_date", end_date),
("symbols", symbols),
("api_key", api_key),
])
.send()
.await?
.json()
.await?;
let response = resp
.get("response")
.ok_or_else(|| Error::decode("currency_time_series: no response"))?;
let rates = response
.get("rates")
.and_then(|r| r.as_object())
.ok_or_else(|| Error::decode("currency_time_series: no rates"))?;
let mut items = Vec::new();
for (date, day_rates) in rates {
if let Some(obj) = day_rates.as_object() {
for (currency, rate) in obj {
items.push(MacroDataPoint {
date: date.clone(),
value: rate.as_f64().unwrap_or(0.0),
name: format!("{}/{}", base, currency),
});
}
}
}
Ok(items)
}
pub async fn currency_currencies(
&self,
c_type: &str,
api_key: &str,
) -> Result<Vec<MacroDataPoint>> {
let url = "https://api.currencyscoop.com/v1/currencies";
let resp: serde_json::Value = self
.get(url)
.query(&[("type", c_type), ("api_key", api_key)])
.send()
.await?
.json()
.await?;
let response = resp
.get("response")
.and_then(|r| r.get("fiat"))
.or_else(|| resp.get("response"))
.ok_or_else(|| Error::decode("currency_currencies: no response"))?;
let mut items = Vec::new();
if let Some(obj) = response.as_object() {
for (code, info) in obj {
let name = info
.get("currency_name")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
items.push(MacroDataPoint {
date: code.clone(),
value: 0.0,
name,
});
}
}
Ok(items)
}
pub async fn currency_convert(
&self,
from: &str,
to: &str,
amount: f64,
api_key: &str,
) -> Result<Vec<MacroDataPoint>> {
let url = "https://api.currencyscoop.com/v1/convert";
let resp: serde_json::Value = self
.get(url)
.query(&[
("from", from),
("to", to),
("amount", &amount.to_string()),
("api_key", api_key),
])
.send()
.await?
.json()
.await?;
let response = resp
.get("response")
.ok_or_else(|| Error::decode("currency_convert: no response"))?;
let value = response
.get("value")
.and_then(|v| v.as_f64())
.unwrap_or(0.0);
let timestamp = response
.get("timestamp")
.and_then(|v| v.as_i64())
.unwrap_or(0);
let date = chrono::DateTime::from_timestamp(timestamp, 0)
.map(|dt| dt.format("%Y-%m-%d").to_string())
.unwrap_or_default();
Ok(vec![MacroDataPoint {
date,
value,
name: format!("{}/{}", from, to),
}])
}
pub async fn currency_boc_sina(
&self,
symbol: &str,
start_date: &str,
end_date: &str,
) -> Result<Vec<MacroDataPoint>> {
let money_code = match symbol {
"美元" => "USD",
"英镑" => "GBP",
"欧元" => "EUR",
"港币" => "HKD",
"日元" => "JPY",
"加拿大元" => "CAD",
"澳大利亚元" => "AUD",
"新西兰元" => "NZD",
"新加坡元" => "SGD",
"瑞士法郎" => "CHF",
"瑞典克朗" => "SEK",
"丹麦克朗" => "DKK",
"挪威克朗" => "NOK",
"澳门元" => "MOP",
"泰国铢" => "THB",
"菲律宾比索" => "PHP",
"韩国元" => "KRW",
_ => {
return Err(Error::invalid_input(format!(
"unknown currency: {}",
symbol
)));
}
};
let url = "http://biz.finance.sina.com.cn/forex/forex.php";
let formatted_start = format!(
"{}-{}-{}",
&start_date[..4],
&start_date[4..6],
&start_date[6..8]
);
let formatted_end = format!("{}-{}-{}", &end_date[..4], &end_date[4..6], &end_date[6..8]);
let body = self
.get(url)
.query(&[
("money_code", money_code),
("type", "0"),
("startdate", &formatted_start),
("enddate", &formatted_end),
("page", "1"),
("call_type", "ajax"),
])
.send()
.await?
.text()
.await?;
let mut items = Vec::new();
for line in body.lines() {
let trimmed = line.trim();
if trimmed.contains("<td") {
let cells = extract_table_cells(trimmed);
if cells.len() >= 5 {
let date = cells[0].clone();
let buy_rate: f64 = cells[1].parse().unwrap_or(0.0);
items.push(MacroDataPoint {
date,
value: buy_rate,
name: format!("BOC {}", symbol),
});
}
}
}
Ok(items)
}
pub async fn currency_boc_safe(&self) -> Result<Vec<MacroDataPoint>> {
let url = "https://www.safe.gov.cn/AppStructured/hlw/RMBQuery.do";
let body = self
.post(url)
.form(&[
("startDate", "2020-01-01"),
(
"endDate",
&chrono::Utc::now().format("%Y-%m-%d").to_string(),
),
("queryYN", "true"),
])
.send()
.await?
.text()
.await?;
let mut items = Vec::new();
for line in body.lines() {
let trimmed = line.trim();
if trimmed.contains("<td") {
let cells = extract_table_cells(trimmed);
if cells.len() >= 2 {
let date = cells[0].clone();
for (i, cell) in cells.iter().enumerate().skip(1) {
if let Ok(val) = cell.parse::<f64>() {
items.push(MacroDataPoint {
date: date.clone(),
value: val,
name: format!("SAFE Rate Col{}", i),
});
}
}
}
}
}
Ok(items)
}
}
fn extract_table_cells(html: &str) -> Vec<String> {
let mut cells = Vec::new();
let mut remaining = html;
while let Some(start) = remaining.find("<td") {
let after_td = &remaining[start..];
if let Some(content_start) = after_td.find('>') {
let content = &after_td[content_start + 1..];
if let Some(content_end) = content.find("</td>") {
let cell_text = content[..content_end].trim().to_string();
cells.push(cell_text);
remaining = &content[content_end + 5..];
} else {
break;
}
} else {
break;
}
}
cells
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_extract_table_cells() {
let html = "<td>2023-01-01</td><td>6.7234</td><td>6.7500</td>";
let cells = extract_table_cells(html);
assert_eq!(cells.len(), 3);
assert_eq!(cells[0], "2023-01-01");
assert_eq!(cells[1], "6.7234");
}
}