pub fn fmt_date(date: &str) -> String {
if date.len() >= 8 {
format!("{}-{}-{}", &date[0..4], &date[4..6], &date[6..8])
} else {
date.to_string()
}
}
pub fn normalize_trade_date(value: &str) -> String {
if value.len() >= 8 {
fmt_date(value)
} else {
value.get(0..10).unwrap_or(value).to_string()
}
}
pub fn parse_f64_safe(value: &str) -> f64 {
value.trim().parse::<f64>().unwrap_or(0.0)
}
pub fn parse_i64_safe(value: &str) -> i64 {
value.trim().parse::<i64>().unwrap_or(0)
}
pub fn today_yyyymmdd() -> String {
chrono::Utc::now().format("%Y%m%d").to_string()
}
pub fn today_iso() -> String {
chrono::Utc::now().format("%Y-%m-%d").to_string()
}
pub fn days_ago_yyyymmdd(days: i64) -> String {
(chrono::Utc::now() - chrono::Duration::days(days))
.format("%Y%m%d")
.to_string()
}
pub fn parse_csv_line(line: &str) -> Vec<&str> {
line.split(',').map(str::trim).collect()
}
pub fn parse_candle_line(line: &str) -> crate::Result<crate::types::CandlePoint> {
let f = parse_csv_line(line);
if f.len() < 11 {
return Err(crate::Error::decode(format!(
"unexpected eastmoney candle format: {line}"
)));
}
Ok(crate::types::CandlePoint {
trade_date: f[0].to_string(),
open: parse_f64_safe(f[1]),
close: parse_f64_safe(f[2]),
high: parse_f64_safe(f[3]),
low: parse_f64_safe(f[4]),
volume: parse_f64_safe(f[5]).round() as i64,
amount: parse_f64_safe(f[6]),
amplitude_pct: parse_f64_safe(f[7]),
change_pct: parse_f64_safe(f[8]),
change_amount: parse_f64_safe(f[9]),
turnover_pct: parse_f64_safe(f[10]),
})
}
pub fn apply_change_metrics(items: &mut [crate::types::CandlePoint]) {
for index in 1..items.len() {
let prev_close = items[index - 1].close;
if prev_close > 0.0 {
items[index].change_amount = items[index].close - prev_close;
items[index].change_pct = (items[index].change_amount / prev_close) * 100.0;
}
}
}
pub fn sort_and_limit(
mut items: Vec<crate::types::CandlePoint>,
limit: usize,
) -> Vec<crate::types::CandlePoint> {
items.sort_by(|a, b| a.trade_date.cmp(&b.trade_date));
if items.len() > limit {
items[items.len() - limit..].to_vec()
} else {
items
}
}
pub fn amplitude_pct(high: f64, low: f64) -> f64 {
if low > 0.0 {
((high - low) / low) * 100.0
} else {
0.0
}
}
pub(crate) async fn send_and_check(
builder: reqwest::RequestBuilder,
) -> crate::Result<reqwest::Response> {
builder
.send()
.await
.map_err(crate::Error::from)?
.error_for_status()
.map_err(crate::Error::from)
}
pub(crate) fn eastmoney_clist_params<'a>(
pz: &'a str,
extra: &[(&'a str, &'a str)],
) -> Vec<(&'a str, &'a str)> {
let mut params = vec![
("pn", "1"),
("pz", pz),
("po", "1"),
("np", "1"),
("ut", "bd1d9ddb04089700cf9c27f6f7426281"),
("fltt", "2"),
("invt", "2"),
];
params.extend_from_slice(extra);
params
}
pub fn eastmoney_datacenter_params<'a>(
sort_columns: &'a str,
extra: &[(&'a str, &'a str)],
) -> Vec<(&'a str, &'a str)> {
let mut params = vec![
("columns", "ALL"),
("pageNumber", "1"),
("pageSize", "500"),
("sortTypes", "-1"),
("sortColumns", sort_columns),
("source", "WEB"),
("client", "WEB"),
];
params.extend_from_slice(extra);
params
}
pub(crate) fn eastmoney_f10_params(source: &str) -> Vec<(&str, &str)> {
vec![("source", source), ("client", "PC")]
}
pub fn extract_table_lines(html: &str) -> Vec<String> {
let mut in_table = false;
let mut lines = Vec::new();
for line in html.lines() {
let trimmed = line.trim();
if trimmed.contains("<table") {
in_table = true;
continue;
}
if trimmed.contains("</table>") {
in_table = false;
continue;
}
if in_table {
lines.push(trimmed.to_string());
}
}
lines
}
pub fn extract_table_cells(html: &str) -> Vec<String> {
let mut cells = Vec::new();
let mut remaining = html;
while let Some(start) = remaining.find("<td") {
let after_td = &remaining[start..];
if let Some(content_start) = after_td.find('>') {
let content = &after_td[content_start + 1..];
if let Some(content_end) = content.find("</td>") {
let cell_text = content[..content_end].trim().to_string();
cells.push(cell_text);
remaining = &content[content_end + 5..];
} else {
break;
}
} else {
break;
}
}
cells
}