akshare 0.1.12

100% pure Rust implementation of akshare — unified access to Chinese and global financial market data APIs
Documentation
//! Comparison and valuation data — Dupont, scale, Baidu valuation, Baidu vote.
//!
//! Covers Python functions:
//! - `stock_zh_dupont_comparison` — Dupont comparison from Eastmoney
//! - `stock_zh_scale_comparison` — Scale comparison from Eastmoney
//! - `stock_zh_valuation` — Baidu valuation data
//! - `stock_zh_vote` — Baidu vote data

use crate::client::AkShareClient;
use crate::error::{Error, Result};
use crate::types::value_ext::ValueExt;

use serde::{Deserialize, Serialize};

// ---------------------------------------------------------------------------
// Public types
// ---------------------------------------------------------------------------

/// Dupont comparison entry.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DupontComparison {
    #[serde(default)]
    pub code: Option<String>,
    #[serde(default)]
    pub name: Option<String>,
    #[serde(default)]
    pub roe: Option<f64>,
    #[serde(default)]
    pub net_profit_margin: Option<f64>,
    #[serde(default)]
    pub asset_turnover: Option<f64>,
    #[serde(default)]
    pub equity_multiplier: Option<f64>,
    #[serde(flatten)]
    pub extra: std::collections::HashMap<String, serde_json::Value>,
}

/// Scale comparison entry.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ScaleComparison {
    #[serde(default)]
    pub code: Option<String>,
    #[serde(default)]
    pub name: Option<String>,
    #[serde(default)]
    pub total_market_cap: Option<f64>,
    #[serde(default)]
    pub circulating_market_cap: Option<f64>,
    #[serde(default)]
    pub total_shares: Option<f64>,
    #[serde(default)]
    pub circulating_shares: Option<f64>,
    #[serde(flatten)]
    pub extra: std::collections::HashMap<String, serde_json::Value>,
}

/// Baidu valuation data point.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BaiduValuation {
    pub date: String,
    pub value: f64,
}

/// Baidu vote data.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BaiduVote {
    pub period: String,
    #[serde(default)]
    pub bullish: Option<i64>,
    #[serde(default)]
    pub bearish: Option<i64>,
    #[serde(default)]
    pub bullish_ratio: Option<f64>,
    #[serde(default)]
    pub bearish_ratio: Option<f64>,
}

// ---------------------------------------------------------------------------
// Implementation
// ---------------------------------------------------------------------------

impl AkShareClient {
    /// Get Dupont comparison data from Eastmoney.
    ///
    /// Python equivalent: `stock_zh_dupont_comparison(symbol)`
    ///
    /// - `symbol`: stock code like "SZ000895"
    pub async fn stock_zh_dupont_comparison(&self, symbol: &str) -> Result<Vec<DupontComparison>> {
        let filter = if symbol.len() >= 2 {
            let (prefix, code) = symbol.split_at(2);
            format!("(SECUCODE=\"{code}.{prefix}\")")
        } else {
            return Err(Error::invalid_input("symbol must be like SZ000895"));
        };

        let url = "https://datacenter.eastmoney.com/securities/api/data/v1/get";
        let response = crate::util::send_and_check(
            self.get(url).query(
                {
                    let mut p = vec![
                        ("reportName", "RPT_PCF10_INDUSTRY_DUPONT"),
                        ("columns", "ALL"),
                        ("quoteColumns", ""),
                        ("filter", filter.as_str()),
                        ("pageNumber", ""),
                        ("pageSize", ""),
                        ("sortTypes", "1"),
                        ("sortColumns", "PAIMING"),
                    ];
                    p.extend_from_slice(&crate::util::eastmoney_f10_params("HSF10"));
                    p
                }
                .as_slice(),
            ),
        )
        .await?;

        parse_datacenter_response(response).await
    }

    /// Get scale comparison data from Eastmoney.
    ///
    /// Python equivalent: `stock_zh_scale_comparison(symbol)`
    ///
    /// - `symbol`: stock code like "SZ000895"
    pub async fn stock_zh_scale_comparison(&self, symbol: &str) -> Result<Vec<ScaleComparison>> {
        let filter = if symbol.len() >= 2 {
            let (prefix, code) = symbol.split_at(2);
            format!("(SECUCODE=\"{code}.{prefix}\")")
        } else {
            return Err(Error::invalid_input("symbol must be like SZ000895"));
        };

        let url = "https://datacenter.eastmoney.com/securities/api/data/v1/get";
        let response = crate::util::send_and_check(
            self.get(url).query(
                {
                    let mut p = vec![
                        ("reportName", "RPT_PCF10_INDUSTRY_SCALE"),
                        ("columns", "ALL"),
                        ("quoteColumns", ""),
                        ("filter", filter.as_str()),
                        ("pageNumber", ""),
                        ("pageSize", ""),
                        ("sortTypes", "1"),
                        ("sortColumns", "PAIMING"),
                    ];
                    p.extend_from_slice(&crate::util::eastmoney_f10_params("HSF10"));
                    p
                }
                .as_slice(),
            ),
        )
        .await?;

        parse_datacenter_response(response).await
    }

    /// Get Baidu valuation data.
    ///
    /// Python equivalent: `stock_zh_valuation(symbol, indicator, period)`
    ///
    /// - `symbol`: stock code like "002044"
    /// - `indicator`: "总市值", "市盈率(TTM)", "市盈率(静)", "市净率", "市现率"
    /// - `period`: "近一年", "近三年", "近五年", "近十年", "全部"
    pub async fn stock_zh_valuation(
        &self,
        symbol: &str,
        indicator: &str,
        period: &str,
    ) -> Result<Vec<BaiduValuation>> {
        let url = "https://gushitong.baidu.com/opendata";
        let response = crate::util::send_and_check(self.get(url).query(&[
            ("openapi", "1"),
            ("dspName", "iphone"),
            ("tn", "tangram"),
            ("client", "app"),
            ("query", indicator),
            ("code", symbol),
            ("word", ""),
            ("resource_id", "51171"),
            ("market", "ab"),
            ("tag", indicator),
            ("chart_select", period),
            ("industry_select", ""),
            ("skip_industry", "1"),
            ("finClientType", "pc"),
        ]))
        .await?;

        parse_baidu_valuation(response).await
    }

    /// Get Baidu vote data.
    ///
    /// Python equivalent: `stock_zh_vote(symbol, indicator)`
    ///
    /// - `symbol`: stock code like "000001"
    /// - `indicator`: "指数" or "股票"
    pub async fn stock_zh_vote(&self, symbol: &str, indicator: &str) -> Result<Vec<BaiduVote>> {
        let finance_type = match indicator {
            "股票" => "stock",
            "指数" => "index",
            _ => {
                return Err(Error::invalid_input(format!(
                    "invalid indicator: {indicator}"
                )));
            }
        };

        let url = "https://finance.pae.baidu.com/vapi/v1/stockvoterecords";
        let mut votes = Vec::new();

        for period in &["day", "week", "month", "year"] {
            let response = crate::util::send_and_check(self.get(url).query(&[
                ("code", symbol),
                ("market", "ab"),
                ("finance_type", finance_type),
                ("select_type", period),
                ("from_smart_app", "0"),
                ("method", "query"),
                ("finClientType", "pc"),
            ]))
            .await?;

            let data: serde_json::Value = response.json().await.map_err(Error::from)?;

            let vote_res = data
                .get("Result")
                .and_then(|r| r.get("voteRecords"))
                .and_then(|v| v.get("voteRes"))
                .and_then(|v| v.as_array());

            if let Some(arr) = vote_res {
                for item in arr {
                    let item_type = item.str_or(&["type"], "");
                    if item_type == *period {
                        votes.push(BaiduVote {
                            period: period.to_string(),
                            bullish: item.i64_field(&["bullCount"]),
                            bearish: item.i64_field(&["bearCount"]),
                            bullish_ratio: item.f64_field(&["bullRatio"]),
                            bearish_ratio: item.f64_field(&["bearRatio"]),
                        });
                    }
                }
            }
        }

        if votes.is_empty() {
            return Err(Error::not_found("baidu vote returned no data"));
        }
        Ok(votes)
    }
}

/// Parse Eastmoney datacenter generic response.
async fn parse_datacenter_response<T: serde::de::DeserializeOwned>(
    response: reqwest::Response,
) -> Result<Vec<T>> {
    let payload: serde_json::Value = response.json().await.map_err(Error::from)?;
    let data_arr = payload
        .get("result")
        .and_then(|r| r.get("data"))
        .and_then(|d| d.as_array())
        .ok_or_else(|| Error::upstream("datacenter returned no data"))?;

    let mut items = Vec::new();
    for val in data_arr {
        if let Ok(item) = serde_json::from_value::<T>(val.clone()) {
            items.push(item);
        }
    }

    if items.is_empty() {
        return Err(Error::not_found("datacenter returned empty data"));
    }
    Ok(items)
}

/// Parse Baidu valuation response.
async fn parse_baidu_valuation(response: reqwest::Response) -> Result<Vec<BaiduValuation>> {
    let data: serde_json::Value = response.json().await.map_err(Error::from)?;

    let body = data
        .get("Result")
        .and_then(|r| r.get(0))
        .and_then(|r| r.get("DisplayData"))
        .and_then(|d| d.get("resultData"))
        .and_then(|r| r.get("tplData"))
        .and_then(|t| t.get("result"))
        .and_then(|r| r.get("chartInfo"))
        .and_then(|c| c.get(0))
        .and_then(|c| c.get("body"))
        .and_then(|b| b.as_array())
        .ok_or_else(|| Error::upstream("baidu valuation missing chart data"))?;

    let items: Vec<BaiduValuation> = body
        .iter()
        .filter_map(|item| {
            let arr = item.as_array()?;
            if arr.len() < 2 {
                return None;
            }
            let date = arr[0].as_str().unwrap_or("").to_string();
            let value = arr[1].as_f64().unwrap_or(0.0);
            Some(BaiduValuation { date, value })
        })
        .collect();

    if items.is_empty() {
        return Err(Error::not_found("baidu valuation returned no data"));
    }
    Ok(items)
}