use eyre::{Result, WrapErr};
use serde::{Deserialize, Deserializer, Serialize};
pub fn get(page_size: u16, page_number: u16) -> Result<String> {
let url = format!(
"http://56.push2.eastmoney.com/api/qt/clist/get?cb=jQuery112407375845698232317_1631693257414&\
pn={page_number}&pz={page_size}&po=0&np=1&ut=bd1d9ddb04089700cf9c27f6f7426281&fltt=2&\
invt=2&fid=f12&fs=m:0+t:6,m:0+t:80,m:1+t:2,m:1+t:23&fields=f18,f16,f12,f17,f15,f2,f6,f5&_=1631693257534"
);
info!("Get: {url}");
Ok(ureq::get(&url)
.call()
.wrap_err_with(|| format!("获取东财股票数据失败,网址为\n`{url:?}`"))?
.into_string()?)
}
pub fn parse(text: &str) -> Result<EastMarket> {
serde_json::from_str(&text[42..text.len() - 2])
.wrap_err_with(|| format!("解析东财股票数据失败,返回的文本为\n{text:?}"))
}
pub fn fetch(max: Option<u16>) -> Result<EastMarket> {
const N: u16 = 100;
let mut done = 0u16;
let mut total;
let mut v = Vec::with_capacity(6000);
loop {
let page_number = done / 100 + 1;
let txt = get(N, page_number)?;
let data = parse(&txt)?;
total = data.data.total;
let len = data.data.diff.len();
done += len as u16;
v.push(data);
info!("page_number={page_number} len={len} done={done} max={max:?} total={total}");
if done >= max.unwrap_or(total) {
break;
}
}
let mut diff = Vec::with_capacity(total as usize);
for each in v {
let dtotal = each.data.total;
ensure!(
dtotal == total,
"data.total {dtotal} should equal to {total}"
);
diff.extend(each.data.diff);
}
if max.is_none() {
ensure!(
diff.len() as u16 == total,
"Aggregated res.len() {} should equal to {total}",
diff.len()
);
}
Ok(EastMarket {
data: EastData { diff, total },
})
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Day {
#[serde(skip_deserializing, default = "default_date")]
pub date: String,
#[serde(rename(deserialize = "f12"))]
pub code: String,
#[serde(rename(deserialize = "f17"), deserialize_with = "deser_opt_f32")]
pub open: F32,
#[serde(rename(deserialize = "f15"), deserialize_with = "deser_opt_f32")]
pub high: F32,
#[serde(rename(deserialize = "f16"), deserialize_with = "deser_opt_f32")]
pub low: F32,
#[serde(rename(deserialize = "f2"), deserialize_with = "deser_opt_f32")]
pub close: F32,
#[serde(rename(deserialize = "f6"), deserialize_with = "deser_opt_f32")]
pub amount: F32,
#[serde(rename(deserialize = "f5"), deserialize_with = "deser_opt_f32")]
pub vol: F32,
#[serde(rename(deserialize = "f18"), deserialize_with = "deser_opt_f32")]
pub preclose: F32,
#[serde(skip_deserializing, default)]
pub factor: f64,
}
fn deser_opt_f32<'de, D: Deserializer<'de>>(deserializer: D) -> Result<F32, D::Error> {
Ok(f32::deserialize(deserializer).ok())
}
pub type F32 = Option<f32>;
fn default_date() -> String {
chrono::Local::now().format("%Y-%m-%d").to_string()
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EastMarket {
pub data: EastData,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EastData {
pub diff: Vec<Day>,
pub total: u16,
}