akshare 0.1.2

100% pure Rust implementation of akshare — unified access to Chinese and global financial market data APIs
Documentation
//! Tencent Finance API helpers.

use crate::client::AkShareClient;
use crate::error::{Error, Result};
use crate::types::{CandlePoint, QuoteSnapshot};
use crate::util::{
    amplitude_pct, apply_change_metrics, normalize_trade_date, parse_f64_safe, parse_i64_safe,
};

impl AkShareClient {
    /// Tencent A-share realtime quote.
    pub(crate) async fn tencent_a_share_quote(&self, symbol: &str) -> Result<QuoteSnapshot> {
        let ts = crate::market::tencent_market_symbol(symbol)?;
        let url = format!("https://qt.gtimg.cn/q={ts}");
        let body = self.get(&url).send().await?.text().await?;
        let line = body
            .lines()
            .find(|l| l.contains("v_"))
            .ok_or_else(|| Error::upstream("empty tencent response"))?;
        let data = line
            .split_once('=')
            .and_then(|(_, r)| r.trim_matches('"').split_once(';'))
            .map_or("", |(s, _)| s);
        let p: Vec<&str> = data.split('~').collect();
        if p.len() < 45 {
            return Err(Error::decode("tencent quote: insufficient fields"));
        }
        Ok(QuoteSnapshot {
            symbol: symbol.to_string(),
            date: normalize_trade_date(p[30]),
            open: parse_f64_safe(p[5]),
            high: parse_f64_safe(p[33]),
            low: parse_f64_safe(p[34]),
            close: parse_f64_safe(p[3]),
            volume: parse_i64_safe(p[6]),
        })
    }

    /// Tencent A-share kline (candlestick).
    pub(crate) async fn tencent_a_share_candles(
        &self,
        symbol: &str,
        adjust: &str,
        limit: usize,
    ) -> Result<Vec<CandlePoint>> {
        #[derive(serde::Deserialize)]
        struct Resp {
            data: Option<serde_json::Value>,
        }

        let ts = crate::market::tencent_market_symbol(symbol)?;
        let period = "day";
        let url = format!(
            "https://web.ifzq.gtimg.cn/appstock/app/fqkline/get?param={ts},{period},,,{limit},{adjust}"
        );

        let resp: Resp = self.get(&url).send().await?.json().await?;
        let data = resp
            .data
            .ok_or_else(|| Error::upstream("empty tencent kline data"))?;

        let symbol_lower = ts.to_lowercase();
        let kline_key = match adjust {
            "qfq" => "qfqday",
            "hfq" => "hfqday",
            _ => "day",
        };
        let klines = data
            .get(&symbol_lower)
            .and_then(|v| v.get(kline_key).or_else(|| v.get("day")))
            .and_then(|v| v.as_array())
            .cloned()
            .unwrap_or_default();

        let mut items = Vec::with_capacity(klines.len());
        for entry in &klines {
            let Some(arr) = entry.as_array() else {
                continue;
            };
            if arr.len() < 6 {
                continue;
            }
            let trade_date = arr[0].as_str().unwrap_or("").to_string();
            let open = arr[1].as_f64().unwrap_or(0.0);
            let close = arr[2].as_f64().unwrap_or(0.0);
            let high = arr[3].as_f64().unwrap_or(0.0);
            let low = arr[4].as_f64().unwrap_or(0.0);
            let volume = arr[5].as_f64().unwrap_or(0.0) as i64;
            items.push(CandlePoint {
                trade_date,
                open,
                close,
                high,
                low,
                volume,
                amount: 0.0,
                amplitude_pct: amplitude_pct(high, low),
                change_pct: 0.0,
                change_amount: 0.0,
                turnover_pct: 0.0,
            });
        }
        apply_change_metrics(&mut items);
        Ok(items)
    }

    /// Tencent HK realtime quote.
    pub(crate) async fn tencent_hk_quote(&self, symbol: &str) -> Result<QuoteSnapshot> {
        let code = crate::market::normalize_hk_symbol(symbol)
            .ok_or_else(|| Error::invalid_input(format!("invalid HK symbol: {symbol}")))?;
        let url = format!("https://qt.gtimg.cn/q=r_hk{code}");
        let body = self.get(&url).send().await?.text().await?;
        let line = body
            .lines()
            .find(|l| l.contains("v_"))
            .ok_or_else(|| Error::upstream("empty tencent HK response"))?;
        let data = line
            .split_once('=')
            .and_then(|(_, r)| r.trim_matches('"').split_once(';'))
            .map_or("", |(s, _)| s);
        let p: Vec<&str> = data.split('~').collect();
        if p.len() < 30 {
            return Err(Error::decode("tencent HK quote: insufficient fields"));
        }
        Ok(QuoteSnapshot {
            symbol: symbol.to_string(),
            date: normalize_trade_date(p.get(17).unwrap_or(&"")),
            open: parse_f64_safe(p.get(2).unwrap_or(&"")),
            high: parse_f64_safe(p.get(3).unwrap_or(&"")),
            low: parse_f64_safe(p.get(4).unwrap_or(&"")),
            close: parse_f64_safe(p.get(1).unwrap_or(&"")),
            volume: parse_i64_safe(p.get(6).unwrap_or(&"")),
        })
    }

    /// Tencent HK kline (candlestick).
    pub(crate) async fn tencent_hk_candles(
        &self,
        symbol: &str,
        limit: usize,
    ) -> Result<Vec<CandlePoint>> {
        #[derive(serde::Deserialize)]
        struct Resp {
            data: Option<serde_json::Value>,
        }

        let code = crate::market::normalize_hk_symbol(symbol)
            .ok_or_else(|| Error::invalid_input(format!("invalid HK symbol: {symbol}")))?;
        let hk_symbol = format!("r_hk{code}");
        let url = format!(
            "https://web.ifzq.gtimg.cn/appstock/app/fqkline/get?param={hk_symbol},day,,,{limit},qfq"
        );

        let resp: Resp = self.get(&url).send().await?.json().await?;
        let data = resp
            .data
            .ok_or_else(|| Error::upstream("empty tencent HK kline data"))?;

        let klines = data
            .get(&hk_symbol)
            .and_then(|v| v.get("qfqday").or_else(|| v.get("day")))
            .and_then(|v| v.as_array())
            .cloned()
            .unwrap_or_default();

        let mut items = Vec::with_capacity(klines.len());
        for entry in &klines {
            let Some(arr) = entry.as_array() else {
                continue;
            };
            if arr.len() < 6 {
                continue;
            }
            items.push(CandlePoint {
                trade_date: arr[0].as_str().unwrap_or("").to_string(),
                open: arr[1].as_f64().unwrap_or(0.0),
                close: arr[2].as_f64().unwrap_or(0.0),
                high: arr[3].as_f64().unwrap_or(0.0),
                low: arr[4].as_f64().unwrap_or(0.0),
                volume: arr[5].as_f64().unwrap_or(0.0) as i64,
                amount: 0.0,
                amplitude_pct: 0.0,
                change_pct: 0.0,
                change_amount: 0.0,
                turnover_pct: 0.0,
            });
        }
        apply_change_metrics(&mut items);
        Ok(items)
    }
}