#![allow(dead_code)]
use std::sync::LazyLock;
use serde::{Deserialize, Serialize};
use crate::client::AkShareClient;
use crate::error::{Error, Result};
static RE_TR: LazyLock<regex::Regex> =
LazyLock::new(|| regex::Regex::new(r"<tr[^>]*>([\s\S]*?)</tr>").unwrap());
static RE_TD: LazyLock<regex::Regex> =
LazyLock::new(|| regex::Regex::new(r"<td[^>]*>([\s\S]*?)</td>").unwrap());
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HkSpotQuote {
pub code: String,
pub chinese_name: String,
#[serde(default)]
pub english_name: Option<String>,
#[serde(default)]
pub latest_price: Option<f64>,
#[serde(default)]
pub change_amount: Option<f64>,
#[serde(default)]
pub change_pct: Option<f64>,
#[serde(default)]
pub prev_close: Option<f64>,
#[serde(default)]
pub open: Option<f64>,
#[serde(default)]
pub high: Option<f64>,
#[serde(default)]
pub low: Option<f64>,
#[serde(default)]
pub volume: Option<f64>,
#[serde(default)]
pub amount: Option<f64>,
#[serde(default)]
pub buy_price: Option<f64>,
#[serde(default)]
pub sell_price: Option<f64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HkDailyCandle {
pub date: String,
pub open: f64,
pub high: f64,
pub low: f64,
pub close: f64,
pub volume: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HkFamousStock {
pub code: String,
pub name: String,
#[serde(default)]
pub latest_price: Option<f64>,
#[serde(default)]
pub change_pct: Option<f64>,
#[serde(default)]
pub change_amount: Option<f64>,
#[serde(default)]
pub volume: Option<f64>,
#[serde(default)]
pub amount: Option<f64>,
#[serde(default)]
pub open: Option<f64>,
#[serde(default)]
pub high: Option<f64>,
#[serde(default)]
pub low: Option<f64>,
#[serde(default)]
pub prev_close: Option<f64>,
#[serde(default)]
pub market_cap: Option<f64>,
#[serde(default)]
pub pe_ratio: Option<f64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HkIndexDailyCandle {
pub date: String,
pub open: f64,
pub high: f64,
pub low: f64,
pub close: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HkIndexSpotEm {
pub code: String,
pub internal_id: String,
pub name: String,
#[serde(default)]
pub latest_price: Option<f64>,
#[serde(default)]
pub change_pct: Option<f64>,
#[serde(default)]
pub change_amount: Option<f64>,
#[serde(default)]
pub open: Option<f64>,
#[serde(default)]
pub high: Option<f64>,
#[serde(default)]
pub low: Option<f64>,
#[serde(default)]
pub prev_close: Option<f64>,
#[serde(default)]
pub volume: Option<f64>,
#[serde(default)]
pub amount: Option<f64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HkIndexSpotSina {
pub code: String,
pub name: String,
#[serde(default)]
pub latest_price: Option<f64>,
#[serde(default)]
pub change_amount: Option<f64>,
#[serde(default)]
pub change_pct: Option<f64>,
#[serde(default)]
pub prev_close: Option<f64>,
#[serde(default)]
pub open: Option<f64>,
#[serde(default)]
pub high: Option<f64>,
#[serde(default)]
pub low: Option<f64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HkHotRank {
#[serde(default)]
pub rank: Option<i64>,
pub code: String,
pub name: String,
#[serde(default)]
pub latest_price: Option<f64>,
#[serde(default)]
pub change_pct: Option<f64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HkHotRankDetail {
pub time: String,
pub rank: i64,
#[serde(default)]
pub code: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HkValuationBaidu {
pub date: String,
pub value: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HkGxlLg {
pub date: String,
pub dividend_yield: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HkFhpxDetailThs {
pub announce_date: String,
#[serde(default)]
pub scheme: Option<String>,
#[serde(default)]
pub ex_date: Option<String>,
#[serde(default)]
pub payout_date: Option<String>,
#[serde(default)]
pub transfer_start: Option<String>,
#[serde(default)]
pub transfer_end: Option<String>,
#[serde(default)]
pub dividend_type: Option<String>,
#[serde(default)]
pub progress: Option<String>,
#[serde(default)]
pub stock_dividend: Option<String>,
}
impl AkShareClient {
pub async fn stock_hk_spot(&self) -> Result<Vec<HkSpotQuote>> {
let url = "https://vip.stock.finance.sina.com.cn/quotes_service/api/json_v2.php/Market_Center.getHKStockData";
let mut all_quotes = Vec::new();
for page in 1..100 {
let page_str = page.to_string();
let response = self
.get(url)
.query(&[
("page", page_str.as_str()),
("num", "60"),
("sort", "symbol"),
("asc", "1"),
("node", "qbgg_hk"),
("_s_r_a", "init"),
])
.send()
.await
.map_err(Error::from)?
.error_for_status()
.map_err(Error::from)?;
let data: Vec<serde_json::Value> = response.json().await.map_err(Error::from)?;
if data.is_empty() {
break;
}
for item in &data {
let code = item
.get("symbol")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let cname = item
.get("cname")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let ename = item
.get("name")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
all_quotes.push(HkSpotQuote {
code,
chinese_name: cname,
english_name: ename,
latest_price: parse_hk_f64(item, "trade"),
change_amount: parse_hk_f64(item, "pricechange"),
change_pct: parse_hk_f64(item, "changepercent"),
prev_close: parse_hk_f64(item, "settlement"),
open: parse_hk_f64(item, "open"),
high: parse_hk_f64(item, "high"),
low: parse_hk_f64(item, "low"),
volume: parse_hk_f64(item, "volume"),
amount: parse_hk_f64(item, "amount"),
buy_price: parse_hk_f64(item, "buy"),
sell_price: parse_hk_f64(item, "sell"),
});
}
}
if all_quotes.is_empty() {
return Err(Error::not_found("sina returned no HK spot data"));
}
Ok(all_quotes)
}
pub async fn stock_hk_daily(
&self,
symbol: &str,
start_date: &str,
end_date: &str,
adjust: &str,
) -> Result<Vec<HkDailyCandle>> {
let code = symbol.trim_start_matches('0');
let code = if code.is_empty() { "0" } else { code };
let secid = format!("116.{}", code);
let fqt = match adjust {
"" => "0",
"qfq" => "1",
"hfq" => "2",
_ => return Err(Error::invalid_input(format!("invalid adjust: {adjust}"))),
};
let response = self
.get("https://push2his.eastmoney.com/api/qt/stock/kline/get")
.query(&[
("secid", secid.as_str()),
("fields1", "f1,f2,f3,f4,f5,f6"),
("fields2", "f51,f52,f53,f54,f55,f56,f57,f58,f59,f60,f61"),
("klt", "101"),
("fqt", fqt),
("beg", start_date),
("end", end_date),
("lmt", "1000000"),
])
.send()
.await
.map_err(Error::from)?
.error_for_status()
.map_err(Error::from)?;
#[derive(Deserialize)]
struct Env {
data: Option<EnvData>,
}
#[derive(Deserialize)]
struct EnvData {
klines: Option<Vec<String>>,
}
let payload: Env = response.json().await.map_err(Error::from)?;
let klines = payload
.data
.and_then(|d| d.klines)
.ok_or_else(|| Error::upstream("HK daily kline missing data"))?;
let items: Vec<HkDailyCandle> = klines
.iter()
.filter_map(|line| {
let parts: Vec<&str> = line.split(',').collect();
if parts.len() < 6 {
return None;
}
Some(HkDailyCandle {
date: parts[0].to_string(),
open: parts[1].parse().unwrap_or(0.0),
close: parts[2].parse().unwrap_or(0.0),
high: parts[3].parse().unwrap_or(0.0),
low: parts[4].parse().unwrap_or(0.0),
volume: parts[5].parse().unwrap_or(0.0),
})
})
.collect();
if items.is_empty() {
return Err(Error::not_found("HK daily returned no data"));
}
Ok(items)
}
pub async fn stock_hk_famous_spot_em(&self) -> Result<Vec<HkFamousStock>> {
let response = self
.get("https://push2.eastmoney.com/api/qt/clist/get")
.query(&[
("pn", "1"),
("pz", "5000"),
("po", "1"),
("np", "2"),
("ut", "bd1d9ddb04089700cf9c27f6f7426281"),
("fltt", "2"),
("invt", "2"),
("fid", "f3"),
("fs", "b:DLMK0106"),
("fields", "f2,f3,f4,f5,f6,f9,f12,f14,f15,f16,f17,f18,f20"),
])
.send()
.await
.map_err(Error::from)?
.error_for_status()
.map_err(Error::from)?;
#[derive(Deserialize)]
struct Env {
data: Option<EnvData>,
}
#[derive(Deserialize)]
struct EnvData {
diff: Option<serde_json::Value>,
}
let payload: Env = response.json().await.map_err(Error::from)?;
let diff = payload
.data
.and_then(|d| d.diff)
.ok_or_else(|| Error::upstream("HK famous stocks missing data"))?;
let mut items = Vec::new();
let entries: Vec<&serde_json::Value> = if let Some(arr) = diff.as_array() {
arr.iter().collect()
} else if let Some(obj) = diff.as_object() {
obj.values().collect()
} else {
vec![]
};
for val in entries {
if let (Some(code), Some(name)) = (
val.get("f12").and_then(|v| v.as_str()),
val.get("f14").and_then(|v| v.as_str()),
) {
items.push(HkFamousStock {
code: code.to_string(),
name: name.to_string(),
latest_price: val.get("f2").and_then(|v| v.as_f64()),
change_pct: val.get("f3").and_then(|v| v.as_f64()),
change_amount: val.get("f4").and_then(|v| v.as_f64()),
volume: val.get("f5").and_then(|v| v.as_f64()),
amount: val.get("f6").and_then(|v| v.as_f64()),
open: val.get("f17").and_then(|v| v.as_f64()),
high: val.get("f15").and_then(|v| v.as_f64()),
low: val.get("f16").and_then(|v| v.as_f64()),
prev_close: val.get("f18").and_then(|v| v.as_f64()),
market_cap: val.get("f20").and_then(|v| v.as_f64()),
pe_ratio: val.get("f9").and_then(|v| v.as_f64()),
});
}
}
if items.is_empty() {
return Err(Error::not_found("HK famous stocks returned no data"));
}
Ok(items)
}
pub async fn stock_hk_index_daily_em(&self, symbol: &str) -> Result<Vec<HkIndexDailyCandle>> {
let secid = format!("100.{}", symbol);
let response = self
.get("https://push2his.eastmoney.com/api/qt/stock/kline/get")
.query(&[
("secid", secid.as_str()),
("klt", "101"),
("fqt", "1"),
("lmt", "10000"),
("end", "20500000"),
("iscca", "1"),
("fields1", "f1,f2,f3,f4,f5,f6,f7,f8"),
(
"fields2",
"f51,f52,f53,f54,f55,f56,f57,f58,f59,f60,f61,f62,f63,f64",
),
("ut", "f057cbcbce2a86e2866ab8877db1d059"),
("forcect", "1"),
])
.send()
.await
.map_err(Error::from)?
.error_for_status()
.map_err(Error::from)?;
#[derive(Deserialize)]
struct Env {
data: Option<EnvData>,
}
#[derive(Deserialize)]
struct EnvData {
klines: Option<Vec<String>>,
}
let payload: Env = response.json().await.map_err(Error::from)?;
let klines = payload
.data
.and_then(|d| d.klines)
.ok_or_else(|| Error::upstream("HK index daily missing data"))?;
let items: Vec<HkIndexDailyCandle> = klines
.iter()
.filter_map(|line| {
let parts: Vec<&str> = line.split(',').collect();
if parts.len() < 5 {
return None;
}
Some(HkIndexDailyCandle {
date: parts[0].to_string(),
open: parts[1].parse().unwrap_or(0.0),
high: parts[3].parse().unwrap_or(0.0),
low: parts[4].parse().unwrap_or(0.0),
close: parts[2].parse().unwrap_or(0.0),
})
})
.collect();
if items.is_empty() {
return Err(Error::not_found("HK index daily returned no data"));
}
Ok(items)
}
pub async fn stock_hk_index_daily_sina(&self, symbol: &str) -> Result<Vec<HkIndexDailyCandle>> {
let url = format!(
"https://finance.sina.com.cn/stock/hkstock/{}/klc2_kl.js",
symbol
);
let response = self
.get(&url)
.query(&[("d", "2023_5_01")])
.send()
.await
.map_err(Error::from)?
.error_for_status()
.map_err(Error::from)?;
let text = response.text().await.map_err(Error::from)?;
let json_start = text
.find("=(")
.ok_or_else(|| Error::decode("invalid JS response"))?
+ 2;
let json_end = text
.find(");")
.ok_or_else(|| Error::decode("invalid JS response"))?;
let json_text = &text[json_start..json_end];
let data: Vec<serde_json::Value> = serde_json::from_str(json_text)
.map_err(|e| Error::decode(format!("JSON parse error: {e}")))?;
let items: Vec<HkIndexDailyCandle> = data
.iter()
.filter_map(|item| {
let date = item.get("date")?.as_str()?.to_string();
let open = item.get("open")?.as_f64()?;
let close = item.get("close")?.as_f64()?;
let high = item.get("high")?.as_f64()?;
let low = item.get("low")?.as_f64()?;
Some(HkIndexDailyCandle {
date,
open,
high,
low,
close,
})
})
.collect();
if items.is_empty() {
return Err(Error::not_found("sina HK index daily returned no data"));
}
Ok(items)
}
pub async fn stock_hk_index_spot_em(&self) -> Result<Vec<HkIndexSpotEm>> {
let response = self
.get("https://push2.eastmoney.com/api/qt/clist/get")
.query(&[
("pn", "1"),
("pz", "5000"),
("po", "1"),
("np", "1"),
("ut", "bd1d9ddb04089700cf9c27f6f7426281"),
("fltt", "2"),
("invt", "2"),
("fid", "f3"),
("fs", "m:124,m:125,m:305"),
("fields", "f2,f3,f4,f5,f6,f12,f13,f14,f15,f16,f17,f18"),
])
.send()
.await
.map_err(Error::from)?
.error_for_status()
.map_err(Error::from)?;
#[derive(Deserialize)]
struct Env {
data: Option<EnvData>,
}
#[derive(Deserialize)]
struct EnvData {
diff: Option<Vec<serde_json::Value>>,
}
let payload: Env = response.json().await.map_err(Error::from)?;
let diff = payload
.data
.and_then(|d| d.diff)
.ok_or_else(|| Error::upstream("HK index spot missing data"))?;
let items: Vec<HkIndexSpotEm> = diff
.iter()
.filter_map(|item| {
let code = item.get("f12")?.as_str()?.to_string();
let internal_id = item
.get("f13")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let name = item.get("f14")?.as_str()?.to_string();
Some(HkIndexSpotEm {
code,
internal_id,
name,
latest_price: item.get("f2").and_then(|v| v.as_f64()),
change_pct: item.get("f3").and_then(|v| v.as_f64()),
change_amount: item.get("f4").and_then(|v| v.as_f64()),
volume: item.get("f5").and_then(|v| v.as_f64()),
amount: item.get("f6").and_then(|v| v.as_f64()),
high: item.get("f15").and_then(|v| v.as_f64()),
low: item.get("f16").and_then(|v| v.as_f64()),
open: item.get("f17").and_then(|v| v.as_f64()),
prev_close: item.get("f18").and_then(|v| v.as_f64()),
})
})
.collect();
if items.is_empty() {
return Err(Error::not_found("eastmoney returned no HK index spot"));
}
Ok(items)
}
pub async fn stock_hk_index_spot_sina(&self) -> Result<Vec<HkIndexSpotSina>> {
let url = "https://hq.sinajs.cn/rn=mtf2t&list=hkCES100,hkCES120,hkCES280,hkCES300,hkCESA80,hkCESG10,hkCESHKM,hkCSCMC,hkCSHK100,hkCSHKDIV,hkCSHKLC,hkCSHKLRE,hkCSHKMCS,hkCSHKME,hkCSHKPE,hkCSHKSE,hkCSI300,hkCSRHK50,hkGEM,hkHKL,hkHSCCI,hkHSCEI,hkHSI,hkHSMBI,hkHSMOGI,hkHSMPI,hkHSTECH,hkSSE180,hkSSE180GV,hkSSE380,hkSSE50,hkSSECEQT,hkSSECOMP,hkSSEDIV,hkSSEITOP,hkSSEMCAP,hkSSEMEGA,hkVHSI";
let response = self
.get(url)
.header("Referer", "https://vip.stock.finance.sina.com.cn/")
.send()
.await
.map_err(Error::from)?
.error_for_status()
.map_err(Error::from)?;
let text = response.text().await.map_err(Error::from)?;
let mut items = Vec::new();
for line in text.lines() {
let parts: Vec<&str> = line.split('"').collect();
if parts.len() < 2 {
continue;
}
let data_str = parts[1];
let fields: Vec<&str> = data_str.split(',').collect();
if fields.len() < 9 {
continue;
}
let var_part = line.split('=').next().unwrap_or("");
let code = var_part.split('_').next_back().unwrap_or("");
items.push(HkIndexSpotSina {
code: code.to_string(),
name: fields[0].to_string(),
latest_price: fields[6].parse().ok(),
change_amount: fields[7].parse().ok(),
change_pct: fields[8].parse().ok(),
prev_close: fields[3].parse().ok(),
open: fields[2].parse().ok(),
high: fields[4].parse().ok(),
low: fields[5].parse().ok(),
});
}
if items.is_empty() {
return Err(Error::not_found("sina returned no HK index spot"));
}
Ok(items)
}
pub async fn stock_hk_hot_rank_em(&self) -> Result<Vec<HkHotRank>> {
let url = "https://emappdata.eastmoney.com/stockrank/getAllCurrHkUsList";
let payload = serde_json::json!({
"appId": "appId01",
"globalId": "786e4c21-70dc-435a-93bb-38",
"marketType": "000003",
"pageNo": 1,
"pageSize": 100,
});
let response = self
.post(url)
.json(&payload)
.send()
.await
.map_err(Error::from)?
.error_for_status()
.map_err(Error::from)?;
#[derive(Deserialize)]
struct Env {
data: Option<Vec<RankItem>>,
}
#[derive(Deserialize)]
struct RankItem {
sc: Option<String>,
rk: Option<i64>,
}
let env: Env = response.json().await.map_err(Error::from)?;
let rank_data = env
.data
.ok_or_else(|| Error::upstream("HK hot rank missing data"))?;
let mut items = Vec::new();
for item in &rank_data {
let sc = item.sc.as_deref().unwrap_or("");
let parts: Vec<&str> = sc.split('|').collect();
let code = parts.get(1).unwrap_or(&"").to_string();
let _mark = format!("116.{}", &sc[3..]);
items.push(HkHotRank {
rank: item.rk,
code,
name: String::new(), latest_price: None,
change_pct: None,
});
}
if items.is_empty() {
return Err(Error::not_found("HK hot rank returned no data"));
}
Ok(items)
}
pub async fn stock_hk_hot_rank_latest_em(&self, symbol: &str) -> Result<Vec<HkHotRankDetail>> {
let url = "https://emappdata.eastmoney.com/stockrank/getCurrentHkUsLatest";
let payload = serde_json::json!({
"appId": "appId01",
"globalId": "786e4c21-70dc-435a-93bb-38",
"marketType": "000003",
"srcSecurityCode": format!("HK|{}", symbol),
});
let response = self
.post(url)
.json(&payload)
.send()
.await
.map_err(Error::from)?
.error_for_status()
.map_err(Error::from)?;
let data: serde_json::Value = response.json().await.map_err(Error::from)?;
let obj = data
.get("data")
.and_then(|d| d.as_object())
.ok_or_else(|| Error::upstream("HK hot rank latest missing data"))?;
let mut items = Vec::new();
for (key, val) in obj {
let rank = val.as_i64().unwrap_or(0);
items.push(HkHotRankDetail {
time: key.clone(),
rank,
code: Some(symbol.to_string()),
});
}
if items.is_empty() {
return Err(Error::not_found("HK hot rank latest returned no data"));
}
Ok(items)
}
pub async fn stock_hk_hot_rank_detail_em(&self, symbol: &str) -> Result<Vec<HkHotRankDetail>> {
let url = "https://emappdata.eastmoney.com/stockrank/getHisHkUsList";
let payload = serde_json::json!({
"appId": "appId01",
"globalId": "786e4c21-70dc-435a-93bb-38",
"marketType": "000003",
"srcSecurityCode": format!("HK|{}", symbol),
});
let response = self
.post(url)
.json(&payload)
.send()
.await
.map_err(Error::from)?
.error_for_status()
.map_err(Error::from)?;
#[derive(Deserialize)]
struct Env {
data: Option<Vec<RankDetailItem>>,
}
#[derive(Deserialize)]
struct RankDetailItem {
dt: Option<String>,
rk: Option<i64>,
}
let env: Env = response.json().await.map_err(Error::from)?;
let data = env
.data
.ok_or_else(|| Error::upstream("HK hot rank detail missing data"))?;
let items: Vec<HkHotRankDetail> = data
.iter()
.map(|item| HkHotRankDetail {
time: item.dt.clone().unwrap_or_default(),
rank: item.rk.unwrap_or(0),
code: Some(symbol.to_string()),
})
.collect();
if items.is_empty() {
return Err(Error::not_found("HK hot rank detail returned no data"));
}
Ok(items)
}
pub async fn stock_hk_hot_rank_detail_realtime_em(
&self,
symbol: &str,
) -> Result<Vec<HkHotRankDetail>> {
let url = "https://emappdata.eastmoney.com/stockrank/getCurrentHkUsList";
let payload = serde_json::json!({
"appId": "appId01",
"globalId": "786e4c21-70dc-435a-93bb-38",
"marketType": "000003",
"srcSecurityCode": format!("HK|{}", symbol),
});
let response = self
.post(url)
.json(&payload)
.send()
.await
.map_err(Error::from)?
.error_for_status()
.map_err(Error::from)?;
#[derive(Deserialize)]
struct Env {
data: Option<Vec<RankDetailItem>>,
}
#[derive(Deserialize)]
struct RankDetailItem {
dt: Option<String>,
rk: Option<i64>,
}
let env: Env = response.json().await.map_err(Error::from)?;
let data = env
.data
.ok_or_else(|| Error::upstream("HK realtime hot rank missing data"))?;
let items: Vec<HkHotRankDetail> = data
.iter()
.map(|item| HkHotRankDetail {
time: item.dt.clone().unwrap_or_default(),
rank: item.rk.unwrap_or(0),
code: Some(symbol.to_string()),
})
.collect();
if items.is_empty() {
return Err(Error::not_found("HK realtime hot rank returned no data"));
}
Ok(items)
}
pub async fn stock_hk_valuation_baidu(
&self,
symbol: &str,
indicator: &str,
period: &str,
) -> Result<Vec<HkValuationBaidu>> {
let url = "https://finance.baidu.com/opendata";
let response = self
.get(url)
.query(&[
("openapi", "1"),
("dspName", "iphone"),
("tn", "tangram"),
("client", "app"),
("query", indicator),
("code", symbol),
("word", ""),
("resource_id", "51171"),
("market", "hk"),
("tag", indicator),
("chart_select", period),
("industry_select", ""),
("skip_industry", "1"),
("finClientType", "pc"),
])
.send()
.await
.map_err(Error::from)?
.error_for_status()
.map_err(Error::from)?;
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 HK valuation missing chart data"))?;
let items: Vec<HkValuationBaidu> = body
.iter()
.filter_map(|item| {
let arr = item.as_array()?;
if arr.len() < 2 {
return None;
}
Some(HkValuationBaidu {
date: arr[0].as_str().unwrap_or("").to_string(),
value: arr[1].as_f64().unwrap_or(0.0),
})
})
.collect();
if items.is_empty() {
return Err(Error::not_found("baidu HK valuation returned no data"));
}
Ok(items)
}
pub async fn stock_hk_scale_comparison_em(
&self,
symbol: &str,
) -> Result<Vec<serde_json::Value>> {
let filter = format!("(SECUCODE=\"{}.HK\")", symbol);
let url = "https://datacenter.eastmoney.com/securities/api/data/v1/get";
let response = self
.get(url)
.query(&[
("reportName", "RPT_PCF10_INDUSTRY_HKSCALE"),
("columns", "ALL"),
("quoteColumns", ""),
("filter", filter.as_str()),
("pageNumber", "1"),
("pageSize", ""),
("sortTypes", ""),
("sortColumns", ""),
("source", "F10"),
("client", "PC"),
])
.send()
.await
.map_err(Error::from)?
.error_for_status()
.map_err(Error::from)?;
#[derive(Deserialize)]
struct Env {
result: Option<EnvResult>,
}
#[derive(Deserialize)]
struct EnvResult {
data: Option<Vec<serde_json::Value>>,
}
let payload: Env = response.json().await.map_err(Error::from)?;
let data = payload
.result
.and_then(|r| r.data)
.ok_or_else(|| Error::upstream("HK scale comparison missing data"))?;
if data.is_empty() {
return Err(Error::not_found("HK scale comparison returned no data"));
}
Ok(data)
}
pub async fn stock_hk_dividend_payout_em(
&self,
symbol: &str,
) -> Result<Vec<serde_json::Value>> {
let filter = format!("(SECURITY_CODE=\"{}\")", symbol);
let url = "https://datacenter.eastmoney.com/securities/api/data/v1/get";
let response = self
.get(url)
.query(&[
("reportName", "RPT_HKF10_FN_DIVIDEND"),
("columns", "ALL"),
("quoteColumns", ""),
("filter", filter.as_str()),
("pageNumber", "1"),
("pageSize", ""),
("sortTypes", "-1"),
("sortColumns", "EX_DIVIDEND_DATE"),
("source", "F10"),
("client", "PC"),
])
.send()
.await
.map_err(Error::from)?
.error_for_status()
.map_err(Error::from)?;
#[derive(Deserialize)]
struct Env {
result: Option<EnvResult>,
}
#[derive(Deserialize)]
struct EnvResult {
data: Option<Vec<serde_json::Value>>,
}
let payload: Env = response.json().await.map_err(Error::from)?;
let data = payload
.result
.and_then(|r| r.data)
.ok_or_else(|| Error::upstream("HK dividend payout missing data"))?;
if data.is_empty() {
return Err(Error::not_found("HK dividend payout returned no data"));
}
Ok(data)
}
pub async fn stock_hk_fhpx_detail_ths(&self, symbol: &str) -> Result<Vec<HkFhpxDetailThs>> {
let url = format!("https://basic.10jqka.com.cn/176/HK{}/bonus.html", symbol);
let response = self
.get(&url)
.header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36")
.send()
.await
.map_err(Error::from)?
.error_for_status()
.map_err(Error::from)?;
let html = response.text().await.map_err(Error::from)?;
let mut items = Vec::new();
for tr_cap in RE_TR.captures_iter(&html) {
let tr_html = &tr_cap[1];
let tds: Vec<String> = RE_TD
.captures_iter(tr_html)
.map(|c| {
c[1].replace("<br>", "/")
.replace("<br/>", "/")
.trim()
.to_string()
})
.collect();
if tds.len() >= 7 {
items.push(HkFhpxDetailThs {
announce_date: tds[0].clone(),
scheme: Some(tds[1].clone()),
ex_date: Some(tds[2].clone()),
payout_date: Some(tds[3].clone()),
transfer_start: Some(tds[4].clone()),
transfer_end: Some(tds[5].clone()),
dividend_type: Some(tds[6].clone()),
progress: tds.get(7).cloned(),
stock_dividend: tds.get(8).cloned(),
});
}
}
if items.is_empty() {
return Err(Error::not_found("THS HK fhpx returned no data"));
}
Ok(items)
}
pub async fn stock_hk_financial_indicator_em(
&self,
symbol: &str,
) -> Result<Vec<serde_json::Value>> {
let filter = format!("(SECURITY_CODE=\"{}\")", symbol);
let url = "https://datacenter.eastmoney.com/securities/api/data/v1/get";
let response = self
.get(url)
.query(&[
("reportName", "RPT_HKF10_FN_MAINFINADATA"),
("columns", "ALL"),
("quoteColumns", ""),
("filter", filter.as_str()),
("pageNumber", "1"),
("pageSize", ""),
("sortTypes", "-1"),
("sortColumns", "REPORT_DATE"),
("source", "F10"),
("client", "PC"),
])
.send()
.await
.map_err(Error::from)?
.error_for_status()
.map_err(Error::from)?;
#[derive(Deserialize)]
struct Env {
result: Option<EnvResult>,
}
#[derive(Deserialize)]
struct EnvResult {
data: Option<Vec<serde_json::Value>>,
}
let payload: Env = response.json().await.map_err(Error::from)?;
let data = payload
.result
.and_then(|r| r.data)
.ok_or_else(|| Error::upstream("HK financial indicators missing data"))?;
if data.is_empty() {
return Err(Error::not_found("HK financial indicators returned no data"));
}
Ok(data)
}
pub async fn stock_hk_gxl_lg(&self) -> Result<Vec<HkGxlLg>> {
let url = "https://legulegu.com/api/stockdata/hs";
let response = self
.get(url)
.query(&[("indexCode", "HSI")])
.send()
.await
.map_err(Error::from)?
.error_for_status()
.map_err(Error::from)?;
let data: serde_json::Value = response.json().await.map_err(Error::from)?;
let arr = data
.as_array()
.ok_or_else(|| Error::decode("legulegu response is not an array"))?;
let items: Vec<HkGxlLg> = arr
.iter()
.filter_map(|item| {
let date = item.get("date")?.as_str()?.to_string();
let dv_ratio = item.get("dvRatio")?.as_f64()?;
Some(HkGxlLg {
date,
dividend_yield: dv_ratio,
})
})
.collect();
if items.is_empty() {
return Err(Error::not_found(
"legulegu HK dividend yield returned no data",
));
}
Ok(items)
}
pub async fn stock_hk_indicator_eniu(&self, _symbol: &str) -> Result<Vec<serde_json::Value>> {
Err(Error::unsupported_market(
"stock_hk_indicator_eniu requires Eniu API authentication, not yet implemented",
))
}
}
fn parse_hk_f64(item: &serde_json::Value, key: &str) -> Option<f64> {
item.get(key).and_then(|v| {
if let Some(n) = v.as_f64() {
Some(n)
} else if let Some(s) = v.as_str() {
s.parse().ok()
} else {
None
}
})
}
async fn parse_em_stock_list<T: serde::de::DeserializeOwned>(
response: reqwest::Response,
context: &str,
) -> Result<Vec<T>> {
#[derive(Deserialize)]
struct Env {
data: Option<EnvData>,
}
#[derive(Deserialize)]
struct EnvData {
diff: Option<serde_json::Value>,
}
let payload: Env = response.json().await.map_err(Error::from)?;
let diff = payload
.data
.and_then(|d| d.diff)
.ok_or_else(|| Error::upstream(format!("{} missing data", context)))?;
let mut items = Vec::new();
if let Some(arr) = diff.as_array() {
for val in arr {
if let Ok(item) = serde_json::from_value::<T>(val.clone()) {
items.push(item);
}
}
} else if let Some(obj) = diff.as_object() {
for (_, val) in obj {
if let Ok(item) = serde_json::from_value::<T>(val.clone()) {
items.push(item);
}
}
}
if items.is_empty() {
return Err(Error::not_found(format!("{} returned no data", context)));
}
Ok(items)
}