akshare 0.1.9

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

use super::super::MarketDataClient;
use super::super::wire::{
    EastmoneyAnnouncementContentEnvelope, EastmoneyAnnouncementsEnvelope,
    EastmoneyBillboardEntryItem, EastmoneyBillboardSeatItem, EastmoneyDatacenterEnvelope,
};
use crate::types::{AnnouncementDetail, AnnouncementItem, BillboardEntry, BillboardSeatDetail};

impl MarketDataClient {
    pub(crate) async fn fetch_a_share_announcement_detail(
        &self,
        art_code: &str,
    ) -> anyhow::Result<AnnouncementDetail> {
        let response = self
            .http
            .get("https://np-cnotice-stock.eastmoney.com/api/content/ann")
            .query(&[
                ("art_code", art_code),
                ("client_source", "web"),
                ("page_index", "1"),
            ])
            .send()
            .await
            .context("failed to fetch Eastmoney announcement detail")?
            .error_for_status()
            .context("eastmoney announcement detail request failed")?;
        let payload: EastmoneyAnnouncementContentEnvelope = response
            .json()
            .await
            .context("failed to decode eastmoney announcement detail response")?;
        let data = payload
            .data
            .context("eastmoney announcement detail missing data")?;

        Ok(AnnouncementDetail {
            art_code: data.art_code.unwrap_or_else(|| art_code.to_string()),
            title: data.notice_title.unwrap_or_else(|| "公司公告".to_string()),
            published_at: data.notice_date.unwrap_or_default(),
            content: data.notice_content.unwrap_or_default(),
            pdf_url: data.attach_url,
            source: "Eastmoney 公告".to_string(),
        })
    }

    pub(crate) async fn fetch_a_share_announcements(
        &self,
        ts_code: &str,
        limit: usize,
    ) -> anyhow::Result<Vec<AnnouncementItem>> {
        let symbol = ts_code
            .split_once('.')
            .map(|(code, _)| code)
            .unwrap_or(ts_code);
        let page_size = limit.clamp(1, 100).to_string();
        let response = self
            .http
            .get("https://np-anotice-stock.eastmoney.com/api/security/ann")
            .query(&[
                ("page_size", page_size.as_str()),
                ("page_index", "1"),
                ("ann_type", "A"),
                ("client_source", "web"),
                ("stock_list", symbol),
            ])
            .send()
            .await
            .context("failed to fetch Eastmoney announcements")?
            .error_for_status()
            .context("eastmoney announcements request failed")?;
        let payload: EastmoneyAnnouncementsEnvelope = response
            .json()
            .await
            .context("failed to decode eastmoney announcements response")?;
        let mut items = payload
            .data
            .and_then(|data| data.list)
            .unwrap_or_default()
            .into_iter()
            .map(|item| {
                let art_code = item.art_code.unwrap_or_default();
                AnnouncementItem {
                    url: (!art_code.is_empty()).then(|| {
                        format!(
                            "https://data.eastmoney.com/notices/detail/{symbol}/{art_code}.html"
                        )
                    }),
                    art_code,
                    symbol: symbol.to_string(),
                    title: item.title.unwrap_or_else(|| "公司公告".to_string()),
                    published_at: item.notice_date.unwrap_or_default(),
                    source: "Eastmoney 公告".to_string(),
                }
            })
            .collect::<Vec<_>>();
        if items.is_empty() {
            bail!("eastmoney returned no announcement items");
        }
        items.truncate(limit);
        Ok(items)
    }

    pub(crate) async fn fetch_a_share_billboard_entries(
        &self,
        symbol: &str,
        limit: usize,
    ) -> anyhow::Result<Vec<BillboardEntry>> {
        let symbol = symbol
            .trim()
            .trim_end_matches(".SH")
            .trim_end_matches(".SZ");
        let response = self
            .http
            .get("https://datacenter-web.eastmoney.com/api/data/v1/get")
            .query(&[
                ("reportName", "RPT_DAILYBILLBOARD_DETAILSNEW"),
                ("columns", "ALL"),
                ("filter", &format!("(SECURITY_CODE=\"{symbol}\")")),
                ("pageNumber", "1"),
                ("pageSize", &limit.to_string()),
                ("sortTypes", "-1"),
                ("sortColumns", "TRADE_DATE"),
                ("source", "WEB"),
                ("client", "WEB"),
            ])
            .send()
            .await
            .context("failed to fetch Eastmoney billboard entries")?
            .error_for_status()
            .context("eastmoney billboard entries request failed")?;
        let payload: EastmoneyDatacenterEnvelope<EastmoneyBillboardEntryItem> = response
            .json()
            .await
            .context("failed to decode eastmoney billboard entries response")?;
        let items = payload
            .result
            .and_then(|result| result.data)
            .unwrap_or_default()
            .into_iter()
            .map(|item| BillboardEntry {
                trade_date: item.trade_date.unwrap_or_default(),
                symbol: item.security_code.unwrap_or_else(|| symbol.to_string()),
                name: item.security_name.unwrap_or_else(|| "未知股票".to_string()),
                close_price: item.close_price.unwrap_or_default(),
                change_rate_pct: item.change_rate.unwrap_or_default(),
                turnover_rate_pct: item.turnover_rate,
                net_amount: item.net_amount,
                buy_amount: item.buy_amount,
                sell_amount: item.sell_amount,
                explanation: item.explanation,
                reason: item.explain,
            })
            .collect::<Vec<_>>();
        if items.is_empty() {
            bail!("eastmoney returned no billboard entries");
        }
        Ok(items)
    }

    pub(crate) async fn fetch_a_share_billboard_seats(
        &self,
        symbol: &str,
        side: &str,
        limit: usize,
    ) -> anyhow::Result<Vec<BillboardSeatDetail>> {
        let report_name = match side {
            "buy" => "RPT_BILLBOARD_DAILYDETAILSBUY",
            "sell" => "RPT_BILLBOARD_DAILYDETAILSSELL",
            other => bail!("unsupported billboard side: {}", other),
        };
        let symbol = symbol
            .trim()
            .trim_end_matches(".SH")
            .trim_end_matches(".SZ");
        let response = self
            .http
            .get("https://datacenter-web.eastmoney.com/api/data/v1/get")
            .query(&[
                ("reportName", report_name),
                ("columns", "ALL"),
                ("filter", &format!("(SECURITY_CODE=\"{symbol}\")")),
                ("pageNumber", "1"),
                ("pageSize", &limit.to_string()),
                ("sortTypes", "-1"),
                ("sortColumns", "TRADE_DATE"),
                ("source", "WEB"),
                ("client", "WEB"),
            ])
            .send()
            .await
            .context("failed to fetch Eastmoney billboard seats")?
            .error_for_status()
            .context("eastmoney billboard seats request failed")?;
        let payload: EastmoneyDatacenterEnvelope<EastmoneyBillboardSeatItem> = response
            .json()
            .await
            .context("failed to decode eastmoney billboard seats response")?;
        let items = payload
            .result
            .and_then(|result| result.data)
            .unwrap_or_default()
            .into_iter()
            .map(|item| BillboardSeatDetail {
                trade_date: item.trade_date.unwrap_or_default(),
                symbol: item.security_code.unwrap_or_else(|| symbol.to_string()),
                department_name: item
                    .department_name
                    .unwrap_or_else(|| "未知席位".to_string()),
                buy_amount: item.buy_amount,
                sell_amount: item.sell_amount,
                net_amount: item.net_amount,
                explanation: item.explanation,
            })
            .collect::<Vec<_>>();
        if items.is_empty() {
            bail!("eastmoney returned no billboard seat items");
        }
        Ok(items)
    }
}