akshare 0.1.0

100% pure Rust implementation of akshare — unified access to Chinese and global financial market data APIs
Documentation
//! Fund flow data (资金流向) from Eastmoney and THS.
//!
//! Note: THS-based fund flow functions use Eastmoney push2 API as fallback
//! since THS requires hexin-v token from JavaScript execution.

use super::helpers::*;
use super::types::*;
use crate::client::AkShareClient;
use crate::error::{Error, Result};

impl AkShareClient {
    /// 同花顺-数据中心-资金流向-个股资金流
    /// Uses Eastmoney API as equivalent data source.
    pub async fn stock_fund_flow_individual(&self, symbol: &str) -> Result<Vec<FundFlowEntry>> {
        let sort_field = match symbol {
            "3日排行" => "f62",
            "5日排行" => "f62",
            "10日排行" => "f62",
            "20日排行" => "f62",
            _ => "f62",
        };
        let data = self
            .cistock_fetch(
                "m:0+t:6,m:0+t:80,m:1+t:2,m:1+t:23,m:0+t:81+s:2048",
                "f2,f3,f12,f14,f62,f184,f66,f69,f72,f75,f78,f164,f166",
                "5000",
                sort_field,
            )
            .await?;
        Ok(data
            .iter()
            .map(|v| FundFlowEntry {
                code: json_str(v, "f12"),
                name: json_str(v, "f14"),
                latest_price: json_f64_opt(v, "f2"),
                change_pct: json_f64_opt(v, "f3"),
                turnover_rate: None,
                inflow: json_f64_opt(v, "f62"),
                outflow: None,
                net_flow: json_f64_opt(v, "f62"),
                amount: None,
            })
            .collect())
    }

    /// 同花顺-数据中心-资金流向-概念资金流
    /// Uses Eastmoney sector fund flow API.
    pub async fn stock_fund_flow_concept(&self, symbol: &str) -> Result<Vec<SectorFundFlowRank>> {
        let data = self.sector_fund_flow_fetch("concept", symbol).await?;
        Ok(data)
    }

    /// 同花顺-数据中心-资金流向-行业资金流
    /// Uses Eastmoney sector fund flow API.
    pub async fn stock_fund_flow_industry(&self, symbol: &str) -> Result<Vec<SectorFundFlowRank>> {
        let data = self.sector_fund_flow_fetch("industry", symbol).await?;
        Ok(data)
    }

    /// 同花顺-数据中心-资金流向-大单追踪
    /// Uses Eastmoney big order tracking API.
    pub async fn stock_fund_flow_big_deal(&self, symbol: &str) -> Result<Vec<FundFlowEntry>> {
        let _ = symbol; // parameter kept for API compatibility
        let data = self
            .cistock_fetch(
                "m:0+t:6,m:0+t:80,m:1+t:2,m:1+t:23,m:0+t:81+s:2048",
                "f2,f3,f12,f14,f62,f184,f66,f69",
                "100",
                "f62",
            )
            .await?;
        Ok(data
            .iter()
            .map(|v| FundFlowEntry {
                code: json_str(v, "f12"),
                name: json_str(v, "f14"),
                latest_price: json_f64_opt(v, "f2"),
                change_pct: json_f64_opt(v, "f3"),
                turnover_rate: None,
                inflow: json_f64_opt(v, "f62"),
                outflow: None,
                net_flow: json_f64_opt(v, "f62"),
                amount: None,
            })
            .collect())
    }

    /// 东方财富-板块资金流向-历史
    pub async fn stock_sector_fund_flow_hist(
        &self,
        symbol: &str,
    ) -> Result<Vec<SectorFundFlowHist>> {
        let url = "https://push2his.eastmoney.com/api/qt/stock/fflow/daykline/get".to_string();
        let resp = self
            .get(&url)
            .query(&[
                ("lmt", "0"),
                ("klt", "101"),
                ("secid", symbol),
                ("fields1", "f1,f2,f3,f7"),
                (
                    "fields2",
                    "f51,f52,f53,f54,f55,f56,f57,f58,f59,f60,f61,f62,f63,f64,f65",
                ),
            ])
            .send()
            .await
            .map_err(Error::from)?
            .error_for_status()
            .map_err(Error::from)?;
        let json: serde_json::Value = resp.json().await.map_err(Error::from)?;
        let klines = json
            .get("data")
            .and_then(|d| d.get("klines"))
            .and_then(|k| k.as_array())
            .cloned()
            .unwrap_or_default();
        Ok(klines
            .iter()
            .map(|v| SectorFundFlowHist { data: v.clone() })
            .collect())
    }

    /// 东方财富-板块资金流向-排名
    pub async fn stock_sector_fund_flow_rank(
        &self,
        indicator: &str,
    ) -> Result<Vec<SectorFundFlowRank>> {
        let data = self.sector_fund_flow_fetch("industry", indicator).await?;
        Ok(data)
    }

    /// 东方财富-板块资金流向-汇总
    pub async fn stock_sector_fund_flow_summary(&self) -> Result<Vec<SectorFundFlowSummary>> {
        let data = self
            .dc_fetch_all(
                "RPT_SECTOR_FUND_FLOW",
                "ALL",
                "",
                "NET_INFLOW",
                "-1",
                500,
                2,
                &[],
            )
            .await?;
        Ok(data
            .iter()
            .map(|v| SectorFundFlowSummary { data: v.clone() })
            .collect())
    }

    /// 东方财富-主力资金流向
    pub async fn stock_main_fund_flow(&self, symbol: &str) -> Result<Vec<MainFundFlow>> {
        let market_code = if symbol.starts_with("6") { "1" } else { "0" };
        let secid = format!("{}.{}", market_code, symbol);
        let url = "https://push2his.eastmoney.com/api/qt/stock/fflow/daykline/get";
        let resp = self
            .get(url)
            .query(&[
                ("lmt", "0"),
                ("klt", "101"),
                ("secid", secid.as_str()),
                ("fields1", "f1,f2,f3,f7"),
                (
                    "fields2",
                    "f51,f52,f53,f54,f55,f56,f57,f58,f59,f60,f61,f62,f63,f64,f65",
                ),
            ])
            .send()
            .await
            .map_err(Error::from)?
            .error_for_status()
            .map_err(Error::from)?;
        let json: serde_json::Value = resp.json().await.map_err(Error::from)?;
        let klines = json
            .get("data")
            .and_then(|d| d.get("klines"))
            .and_then(|k| k.as_array())
            .cloned()
            .unwrap_or_default();
        Ok(klines
            .iter()
            .map(|v| MainFundFlow { data: v.clone() })
            .collect())
    }

    /// 东方财富-市场资金流向
    pub async fn stock_market_fund_flow(&self) -> Result<Vec<MarketFundFlow>> {
        let data = self
            .dc_fetch_all(
                "RPT_MARKET_FUND_FLOW",
                "ALL",
                "",
                "TRADE_DATE",
                "-1",
                500,
                5,
                &[],
            )
            .await?;
        Ok(data
            .iter()
            .map(|v| MarketFundFlow { data: v.clone() })
            .collect())
    }

    /// 东方财富-概念资金流向-历史
    pub async fn stock_concept_fund_flow_hist(
        &self,
        symbol: &str,
    ) -> Result<Vec<ConceptFundFlowHist>> {
        let url = "https://push2his.eastmoney.com/api/qt/stock/fflow/daykline/get";
        let resp = self
            .get(url)
            .query(&[
                ("lmt", "0"),
                ("klt", "101"),
                ("secid", symbol),
                ("fields1", "f1,f2,f3,f7"),
                (
                    "fields2",
                    "f51,f52,f53,f54,f55,f56,f57,f58,f59,f60,f61,f62,f63,f64,f65",
                ),
            ])
            .send()
            .await
            .map_err(Error::from)?
            .error_for_status()
            .map_err(Error::from)?;
        let json: serde_json::Value = resp.json().await.map_err(Error::from)?;
        let klines = json
            .get("data")
            .and_then(|d| d.get("klines"))
            .and_then(|k| k.as_array())
            .cloned()
            .unwrap_or_default();
        Ok(klines
            .iter()
            .map(|v| ConceptFundFlowHist { data: v.clone() })
            .collect())
    }

    /// Generic sector fund flow fetch via Eastmoney.
    async fn sector_fund_flow_fetch(
        &self,
        _kind: &str,
        _indicator: &str,
    ) -> Result<Vec<SectorFundFlowRank>> {
        let data = self
            .cistock_fetch(
                "m:90+t:2",
                "f2,f3,f12,f14,f62,f184,f66,f69,f72,f75,f78,f164,f166",
                "5000",
                "f62",
            )
            .await?;
        Ok(data
            .iter()
            .map(|v| SectorFundFlowRank {
                name: json_str(v, "f14"),
                change_pct: json_f64_opt(v, "f3"),
                inflow: json_f64_opt(v, "f62"),
                outflow: None,
                net_flow: json_f64_opt(v, "f62"),
                leader: None,
                leader_change_pct: None,
            })
            .collect())
    }

    /// Generic Eastmoney clist fetch (variant for fund flow).
    async fn cistock_fetch(
        &self,
        fs: &str,
        fields: &str,
        page_size: &str,
        sort_field: &str,
    ) -> Result<Vec<serde_json::Value>> {
        let resp = self
            .get("https://push2.eastmoney.com/api/qt/clist/get")
            .query(&[
                ("pn", "1"),
                ("pz", page_size),
                ("po", "1"),
                ("np", "1"),
                ("ut", "bd1d9ddb04089700cf9c27f6f7426281"),
                ("fltt", "2"),
                ("invt", "2"),
                ("fid", sort_field),
                ("fs", fs),
                ("fields", fields),
            ])
            .send()
            .await
            .map_err(Error::from)?
            .error_for_status()
            .map_err(Error::from)?;
        let payload: super::helpers::ClistSpotEnvelope = resp.json().await.map_err(Error::from)?;
        let items = payload.data.and_then(|d| d.diff).unwrap_or_default();
        if items.is_empty() {
            return Err(Error::not_found("eastmoney clist returned no data"));
        }
        Ok(items)
    }
}