akshare 0.1.12

100% pure Rust implementation of akshare — unified access to Chinese and global financial market data APIs
Documentation
use anyhow::{Context, bail};

use super::{DataError, DataErrorKind, MarketDataClient, QuoteSnapshot, wire};

impl MarketDataClient {
    pub(super) fn parse_quote_csv(symbol: &str, csv: &str) -> anyhow::Result<QuoteSnapshot> {
        let line = csv.trim();
        let parts: Vec<&str> = line.split(',').collect();
        if parts.len() < 8 {
            bail!("unexpected stooq csv format: {line}");
        }

        Ok(QuoteSnapshot {
            symbol: symbol.to_uppercase(),
            date: parts[1].to_string(),
            open: parts[3].parse().context("invalid open value")?,
            high: parts[4].parse().context("invalid high value")?,
            low: parts[5].parse().context("invalid low value")?,
            close: parts[6].parse().context("invalid close value")?,
            volume: parts[7].parse().context("invalid volume value")?,
        })
    }

    pub(super) fn eastmoney_secid(&self, symbol: &str) -> anyhow::Result<String> {
        let normalized = self
            .normalize_a_share_symbol(symbol)
            .context("invalid A-share symbol")?;
        let (code, suffix) = normalized
            .split_once('.')
            .context("invalid normalized ts_code")?;
        let market = match suffix {
            "SH" => "1",
            "SZ" | "BJ" => "0",
            _ => bail!("unsupported A-share suffix {}", suffix),
        };
        Ok(format!("{market}.{code}"))
    }

    pub(super) async fn tushare_query(
        &self,
        api_name: &str,
        params: serde_json::Value,
        fields: &str,
    ) -> anyhow::Result<Vec<wire::TushareRow>> {
        let token = self.tushare_token.as_deref().ok_or_else(|| {
            DataError::new(
                DataErrorKind::MissingCredentials,
                "tushare token missing; set TUSHARE_TOKEN",
            )
        })?;
        let response = self
            .http
            .post("https://api.tushare.pro")
            .json(&serde_json::json!({
                "api_name": api_name,
                "token": token,
                "params": params,
                "fields": fields
            }))
            .send()
            .await
            .with_context(|| format!("failed to call tushare api {api_name}"))?
            .error_for_status()
            .with_context(|| format!("tushare http request failed for {api_name}"))?;

        let payload: wire::TushareResponse = response
            .json()
            .await
            .with_context(|| format!("failed to decode tushare response for {api_name}"))?;
        if payload.code != 0 {
            let kind = match payload.code {
                40203 => DataErrorKind::PermissionDenied,
                _ => DataErrorKind::Upstream,
            };
            return Err(DataError::new(
                kind,
                format!(
                    "tushare api {} failed with code {}: {}",
                    api_name,
                    payload.code,
                    payload.msg.unwrap_or_default()
                ),
            )
            .into());
        }
        let data = payload
            .data
            .context("tushare response missing data payload")?;
        Ok(data
            .items
            .into_iter()
            .map(|item| wire::TushareRow::new(&data.fields, item))
            .collect())
    }
}