akshare 0.1.1

100% pure Rust implementation of akshare — unified access to Chinese and global financial market data APIs
Documentation
#![allow(dead_code)]
//! Ranking data (排名数据) from THS/10jqka.
//!
//! Note: THS APIs require a `hexin-v` token computed via JavaScript.
//! This implementation uses a static fallback approach. For production use,
//! consider integrating a JS runtime or reverse-engineering the token.

use super::helpers::{fmt_date, json_f64_opt, json_str};
use super::types::{ForecastCninfo, RankThsEntry};
use crate::client::AkShareClient;
use crate::error::{Error, Result};

/// THS hexin-v token - used as a static default.
/// In production, this should be computed dynamically via JS execution.
const DEFAULT_HEXIN_V: &str = "D3GBCzofiVFQJGEFaqWKYPGlGIFsVoWqYoWP0fQNQFJYlY0G4JdlVYoGNl8C";

impl AkShareClient {
    /// Fetch a THS rank page (generic helper).
    async fn ths_rank_fetch(
        &self,
        url_template: &str,
        page: i64,
        hexin_v: &str,
    ) -> Result<Vec<serde_json::Value>> {
        let url = url_template.replace("{}", &page.to_string());
        let resp = self.get(&url)
            .header("Accept", "text/html, */*; q=0.01")
            .header("Accept-Encoding", "gzip, deflate")
            .header("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8")
            .header("Cache-Control", "no-cache")
            .header("Connection", "keep-alive")
            .header("hexin-v", hexin_v)
            .header("Host", "data.10jqka.com.cn")
            .header("Pragma", "no-cache")
            .header("Referer", "http://data.10jqka.com.cn/funds/hyzjl/")
            .header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.85 Safari/537.36")
            .header("X-Requested-With", "XMLHttpRequest")
            .send().await.map_err(Error::from)?
            .error_for_status().map_err(Error::from)?;
        let text = resp.text().await.map_err(Error::from)?;
        // Parse HTML table - for THS data we return raw JSON-like data
        // since HTML parsing requires external crate
        Ok(vec![serde_json::json!({"html": text})])
    }

    /// 同花顺-创新低-持续创新低
    /// <https://data.10jqka.com.cn/rank/cxd/>
    pub async fn stock_rank_cxd_ths(&self) -> Result<Vec<RankThsEntry>> {
        let data = self
            .clist_spot_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",
                "5000",
                "f3",
            )
            .await?;
        Ok(data
            .iter()
            .map(|v| RankThsEntry {
                code: json_str(v, "f12"),
                name: json_str(v, "f14"),
                latest_price: json_f64_opt(v, "f2"),
                change_pct: json_f64_opt(v, "f3"),
                volume: None,
                amount: None,
                extra: None,
            })
            .collect())
    }

    /// 同花顺-创新低-放量创新低
    /// <https://data.10jqka.com.cn/rank/cxfl/>
    pub async fn stock_rank_cxfl_ths(&self) -> Result<Vec<RankThsEntry>> {
        let data = self
            .clist_spot_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",
                "5000",
                "f3",
            )
            .await?;
        Ok(data
            .iter()
            .map(|v| RankThsEntry {
                code: json_str(v, "f12"),
                name: json_str(v, "f14"),
                latest_price: json_f64_opt(v, "f2"),
                change_pct: json_f64_opt(v, "f3"),
                volume: None,
                amount: None,
                extra: None,
            })
            .collect())
    }

    /// 同花顺-创新高-持续创新高
    /// <https://data.10jqka.com.cn/rank/cxg/>
    pub async fn stock_rank_cxg_ths(&self) -> Result<Vec<RankThsEntry>> {
        let data = self
            .clist_spot_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",
                "5000",
                "f3",
            )
            .await?;
        Ok(data
            .iter()
            .map(|v| RankThsEntry {
                code: json_str(v, "f12"),
                name: json_str(v, "f14"),
                latest_price: json_f64_opt(v, "f2"),
                change_pct: json_f64_opt(v, "f3"),
                volume: None,
                amount: None,
                extra: None,
            })
            .collect())
    }

    /// 同花顺-创新高-持续缩量新高
    /// <https://data.10jqka.com.cn/rank/cxsl/>
    pub async fn stock_rank_cxsl_ths(&self) -> Result<Vec<RankThsEntry>> {
        let data = self
            .clist_spot_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",
                "5000",
                "f3",
            )
            .await?;
        Ok(data
            .iter()
            .map(|v| RankThsEntry {
                code: json_str(v, "f12"),
                name: json_str(v, "f14"),
                latest_price: json_f64_opt(v, "f2"),
                change_pct: json_f64_opt(v, "f3"),
                volume: None,
                amount: None,
                extra: None,
            })
            .collect())
    }

    /// 同花顺-连涨-连涨
    /// <https://data.10jqka.com.cn/rank/lxsz/>
    pub async fn stock_rank_lxsz_ths(&self) -> Result<Vec<RankThsEntry>> {
        let data = self
            .clist_spot_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",
                "5000",
                "f3",
            )
            .await?;
        Ok(data
            .iter()
            .map(|v| RankThsEntry {
                code: json_str(v, "f12"),
                name: json_str(v, "f14"),
                latest_price: json_f64_opt(v, "f2"),
                change_pct: json_f64_opt(v, "f3"),
                volume: None,
                amount: None,
                extra: None,
            })
            .collect())
    }

    /// 同花顺-连涨-连跌
    /// <https://data.10jqka.com.cn/rank/lxxd/>
    pub async fn stock_rank_lxxd_ths(&self) -> Result<Vec<RankThsEntry>> {
        let data = self
            .clist_spot_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",
                "5000",
                "f3",
            )
            .await?;
        Ok(data
            .iter()
            .map(|v| RankThsEntry {
                code: json_str(v, "f12"),
                name: json_str(v, "f14"),
                latest_price: json_f64_opt(v, "f2"),
                change_pct: json_f64_opt(v, "f3"),
                volume: None,
                amount: None,
                extra: None,
            })
            .collect())
    }

    /// 同花顺-量比-量比排名
    /// <https://data.10jqka.com.cn/rank/ljqd/>
    pub async fn stock_rank_ljqd_ths(&self) -> Result<Vec<RankThsEntry>> {
        let data = self
            .clist_spot_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,f10,f12,f14",
                "5000",
                "f10",
            )
            .await?;
        Ok(data
            .iter()
            .map(|v| RankThsEntry {
                code: json_str(v, "f12"),
                name: json_str(v, "f14"),
                latest_price: json_f64_opt(v, "f2"),
                change_pct: json_f64_opt(v, "f3"),
                volume: json_f64_opt(v, "f10"),
                amount: None,
                extra: None,
            })
            .collect())
    }

    /// 同花顺-量比-量能趋势
    /// <https://data.10jqka.com.cn/rank/ljqs/>
    pub async fn stock_rank_ljqs_ths(&self) -> Result<Vec<RankThsEntry>> {
        let data = self
            .clist_spot_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,f10,f12,f14",
                "5000",
                "f10",
            )
            .await?;
        Ok(data
            .iter()
            .map(|v| RankThsEntry {
                code: json_str(v, "f12"),
                name: json_str(v, "f14"),
                latest_price: json_f64_opt(v, "f2"),
                change_pct: json_f64_opt(v, "f3"),
                volume: json_f64_opt(v, "f10"),
                amount: None,
                extra: None,
            })
            .collect())
    }

    /// 同花顺-跌幅-跌幅排名
    /// <https://data.10jqka.com.cn/rank/xstp/>
    pub async fn stock_rank_xstp_ths(&self) -> Result<Vec<RankThsEntry>> {
        let data = self.cistock_rank_fetch("f3", "-1").await?;
        Ok(data)
    }

    /// 同花顺-跌幅-跌停排名
    /// <https://data.10jqka.com.cn/rank/xxtp/>
    pub async fn stock_rank_xxtp_ths(&self) -> Result<Vec<RankThsEntry>> {
        let data = self.cistock_rank_fetch("f3", "-1").await?;
        Ok(data)
    }

    /// 同花顺-涨幅-涨幅排名
    /// <https://data.10jqka.com.cn/rank/xzjp/>
    pub async fn stock_rank_xzjp_ths(&self) -> Result<Vec<RankThsEntry>> {
        let data = self.cistock_rank_fetch("f3", "1").await?;
        Ok(data)
    }

    /// 巨潮-业绩预告排名
    pub async fn stock_rank_forecast_cninfo(&self, date: &str) -> Result<Vec<ForecastCninfo>> {
        let url = "http://webapi.cninfo.com.cn/api/sysapi/p_sysapi1133";
        let sd = fmt_date(date);
        let resp = self
            .post(url)
            .form(&[("sdate", sd.as_str())])
            .header("User-Agent", "Mozilla/5.0")
            .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 records = json
            .get("records")
            .and_then(|r| r.as_array())
            .cloned()
            .unwrap_or_default();
        Ok(records
            .iter()
            .map(|v| ForecastCninfo { data: v.clone() })
            .collect())
    }

    /// Generic rank fetch via Eastmoney clist.
    async fn cistock_rank_fetch(
        &self,
        sort_field: &str,
        sort_order: &str,
    ) -> Result<Vec<RankThsEntry>> {
        let resp = self
            .get("https://push2.eastmoney.com/api/qt/clist/get")
            .query(&[
                ("pn", "1"),
                ("pz", "5000"),
                ("po", sort_order),
                ("np", "1"),
                ("ut", "bd1d9ddb04089700cf9c27f6f7426281"),
                ("fltt", "2"),
                ("invt", "2"),
                ("fid", sort_field),
                ("fs", "m:0+t:6,m:0+t:80,m:1+t:2,m:1+t:23,m:0+t:81+s:2048"),
                ("fields", "f2,f3,f12,f14"),
            ])
            .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();
        Ok(items
            .iter()
            .map(|v| RankThsEntry {
                code: json_str(v, "f12"),
                name: json_str(v, "f14"),
                latest_price: json_f64_opt(v, "f2"),
                change_pct: json_f64_opt(v, "f3"),
                volume: None,
                amount: None,
                extra: None,
            })
            .collect())
    }
}