use chrono::{DateTime, Datelike, Duration, NaiveDate, NaiveTime, Timelike, Utc};
use czsc_core::czsc_bail;
use czsc_core::objects::{bar::RawBar, freq::Freq, market::Market};
use hashbrown::HashMap;
use once_cell::sync::Lazy;
use polars::{frame::DataFrame, io::SerReader, prelude::IpcReader};
use std::{io::Cursor, str::FromStr};
use crate::errors::UtilsError;
static MINUTES_SPLIT_DF: Lazy<DataFrame> = Lazy::new(|| {
const MINUTES_SPLIT_BYTES: &[u8] = include_bytes!("../data/minutes_split.feather");
let cursor = Cursor::new(MINUTES_SPLIT_BYTES);
IpcReader::new(cursor)
.finish()
.expect("failed to read minutes_split.feather")
});
static FREQ_EDT_MAP: Lazy<HashMap<(Market, Freq), HashMap<NaiveTime, NaiveTime>>> =
Lazy::new(|| {
let mut result: HashMap<(Market, Freq), HashMap<NaiveTime, NaiveTime>> = HashMap::new();
let format = "%H:%M";
let groups = MINUTES_SPLIT_DF
.partition_by(["market"], true)
.expect("failed tp groupby markets");
for g in groups {
let market_type = g
.column("market")
.expect("failed to get market col")
.str()
.expect("failed to convert market col into str")
.get(0)
.expect("failed to get the first row for market col");
let market_type = Market::from_str(market_type).expect("unregistered market type");
for minute in MINUTES_SPLIT_DF
.get_column_names()
.iter()
.filter(|&col| col.contains("分钟"))
{
let time_col = g
.column("time")
.expect("failed to get time col")
.str()
.expect("failed to convert time col into str");
let freq_of_time_col = g
.column(minute)
.expect("failed to get minute col")
.str()
.expect("failed to convert minute col into str");
let mut time_map = HashMap::new();
for idx in 0..g.height() {
let time = time_col.get(idx).expect("failed to get idx of time col");
let freq_of_time = freq_of_time_col
.get(idx)
.expect("failed to get idx of minute col");
let time =
NaiveTime::parse_from_str(time, format).expect("failed to parse time str");
let freq_of_time = NaiveTime::parse_from_str(freq_of_time, format)
.expect("failed to parse time str");
time_map.insert(time, freq_of_time);
}
let minute_freq = Freq::from_str(minute).expect("unregistered freq");
result.insert((market_type, minute_freq), time_map);
}
}
result
});
fn freq_market_times(freq: Freq, market: Market) -> Option<Vec<NaiveTime>> {
let time_map = FREQ_EDT_MAP.get(&(market, freq))?;
let mut times: Vec<NaiveTime> = time_map.keys().cloned().collect();
times.sort();
Some(times)
}
pub fn infer_market_from_bars(bars: &[RawBar], freq: Freq) -> Market {
if !freq.is_minute_freq() {
return Market::Default;
}
let mut time_seq: Vec<NaiveTime> = bars.iter().rev().take(2000).map(|b| b.dt.time()).collect();
time_seq.sort();
time_seq.dedup();
if time_seq.len() < 2 {
return Market::Default;
}
let min_time = *time_seq.first().unwrap();
let max_time = *time_seq.last().unwrap();
for market in [Market::AShare, Market::Futures, Market::Default] {
let Some(times) = freq_market_times(freq, market) else {
continue;
};
let sub_times: Vec<NaiveTime> = times
.into_iter()
.filter(|t| *t >= min_time && *t <= max_time)
.collect();
if sub_times == time_seq {
return market;
}
}
Market::Default
}
fn freq_end_date(dt: NaiveDate, freq: Freq) -> Result<NaiveDate, UtilsError> {
match freq {
Freq::D => Ok(dt),
Freq::W => {
let weekday = dt.weekday().number_from_monday();
let days_to_add = if weekday <= 5 {
5 - weekday
} else {
12 - weekday
};
Ok(dt + Duration::days(days_to_add as i64))
}
Freq::Y => {
NaiveDate::from_ymd_opt(dt.year(), 12, 31).ok_or_else(|| {
UtilsError::InvalidFreqEndDate(format!("Y freq: year={}", dt.year()))
})
}
Freq::M => {
let year = dt.year();
let month = dt.month();
let (next_year, next_month) = if month == 12 {
(year + 1, 1)
} else {
(year, month + 1)
};
NaiveDate::from_ymd_opt(next_year, next_month, 1)
.ok_or_else(|| {
UtilsError::InvalidFreqEndDate(format!(
"M freq: next_year={next_year}, next_month={next_month}"
))
})?
.pred_opt()
.ok_or_else(|| {
UtilsError::InvalidFreqEndDate("M freq: failed to get previous day".to_string())
})
}
Freq::S => {
let year = dt.year();
let month = dt.month();
let (next_quarter_year, next_quarter_month) = match month {
1..=3 => (year, 4), 4..=6 => (year, 7), 7..=9 => (year, 10), 10..=12 => (year + 1, 1), _ => unreachable!(),
};
NaiveDate::from_ymd_opt(next_quarter_year, next_quarter_month, 1)
.ok_or_else(|| {
UtilsError::InvalidFreqEndDate(format!(
"S freq: next_quarter_year={next_quarter_year}, next_quarter_month={next_quarter_month}"
))
})?
.pred_opt()
.ok_or_else(|| {
UtilsError::InvalidFreqEndDate("S freq: failed to get previous day".to_string())
})
}
_ => Ok(dt),
}
}
pub fn freq_end_time(
dt: DateTime<Utc>,
freq: Freq,
market: Market,
) -> Result<DateTime<Utc>, UtilsError> {
let dt = if dt.second() > 0 || dt.nanosecond() > 0 {
dt.with_second(0).unwrap().with_nanosecond(0).unwrap() + Duration::minutes(1)
} else {
dt
};
let hm_str = dt.format("%H:%M").to_string();
let utc_time = dt.time();
if freq.is_minute_freq() {
if let Some(time_map) = FREQ_EDT_MAP.get(&(market, freq))
&& let Some(end_time) = time_map.get(&utc_time)
{
let edt = dt
.with_hour(end_time.hour())
.ok_or(UtilsError::InvalidDateTime)?
.with_minute(end_time.minute())
.ok_or(UtilsError::InvalidDateTime)?;
if end_time.hour() == 0
&& end_time.minute() == 0
&& freq != Freq::F1
&& hm_str != "00:00"
{
return Ok(edt + Duration::days(1));
}
return Ok(edt);
}
if let Some(time_map) = FREQ_EDT_MAP.get(&(market, freq)) {
if let Ok(parsed_time) = NaiveTime::parse_from_str(&hm_str, "%H:%M")
&& let Some(end_time) = time_map.get(&parsed_time)
{
let edt = dt
.with_hour(end_time.hour())
.ok_or(UtilsError::InvalidDateTime)?
.with_minute(end_time.minute())
.ok_or(UtilsError::InvalidDateTime)?;
if end_time.hour() == 0
&& end_time.minute() == 0
&& freq != Freq::F1
&& hm_str != "00:00"
{
return Ok(edt + Duration::days(1));
}
return Ok(edt);
}
}
if let Some(time_map) = FREQ_EDT_MAP.get(&(market, freq)) {
let mut available_times: Vec<_> = time_map.keys().collect();
available_times.sort();
if let Ok(current_time) = NaiveTime::parse_from_str(&hm_str, "%H:%M") {
for &next_time in &available_times {
if next_time > ¤t_time
&& let Some(end_time) = time_map.get(next_time)
{
let edt = dt
.with_hour(end_time.hour())
.ok_or(UtilsError::InvalidDateTime)?
.with_minute(end_time.minute())
.ok_or(UtilsError::InvalidDateTime)?;
if end_time.hour() == 0
&& end_time.minute() == 0
&& freq != Freq::F1
&& hm_str != "00:00"
{
return Ok(edt + Duration::days(1));
}
return Ok(edt);
}
}
if let Some(&first_time) = available_times.first()
&& let Some(end_time) = time_map.get(first_time)
{
let next_day = dt + Duration::days(1);
let edt = next_day
.with_hour(end_time.hour())
.ok_or(UtilsError::InvalidDateTime)?
.with_minute(end_time.minute())
.ok_or(UtilsError::InvalidDateTime)?;
return Ok(edt);
}
}
}
czsc_bail!(
"无法找到对应的结束时间: 时间={}, 频率={:?}, 市场={:?}",
hm_str,
freq,
market
)
}
let utc_date = freq_end_date(dt.date_naive(), freq)?;
let edt = utc_date
.and_hms_opt(0, 0, 0)
.ok_or(UtilsError::InvalidDateTime)?
.and_utc();
Ok(edt)
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::{NaiveDateTime, TimeZone};
#[test]
fn test_daily_freq_end_time() {
let test_cases = vec![
("2025-08-31 23:45:00", "2025-08-31 00:00:00"),
("2025-09-01 00:00:00", "2025-09-01 00:00:00"),
("2025-09-01 00:15:00", "2025-09-01 00:00:00"),
("2025-09-01 00:30:00", "2025-09-01 00:00:00"),
("2025-09-01 01:00:00", "2025-09-01 00:00:00"),
("2025-09-01 12:00:00", "2025-09-01 00:00:00"),
("2025-09-01 23:45:00", "2025-09-01 00:00:00"),
];
for (input_str, expected_str) in test_cases {
let input_dt = Utc.from_utc_datetime(
&NaiveDateTime::parse_from_str(input_str, "%Y-%m-%d %H:%M:%S").unwrap(),
);
let expected_dt = Utc.from_utc_datetime(
&NaiveDateTime::parse_from_str(expected_str, "%Y-%m-%d %H:%M:%S").unwrap(),
);
let result = freq_end_time(input_dt, Freq::D, Market::AShare).unwrap();
assert_eq!(
result, expected_dt,
"\n输入: {input_str}\n期望: {expected_str}\n实际: {result}"
);
}
println!("✅ 所有日线 freq_end_time 测试通过");
}
trait TestDateTime {
fn to_dt_str(&self) -> String;
}
impl TestDateTime for DateTime<Utc> {
fn to_dt_str(&self) -> String {
self.format("%Y-%m-%d %H:%M:%S").to_string()
}
}
#[test]
fn test_freq_minute() {
let dt = Utc.from_utc_datetime(
&NaiveDateTime::parse_from_str("2024-12-12 10:01:00", "%Y-%m-%d %H:%M:%S").unwrap(),
);
assert_eq!(
freq_end_time(dt, Freq::F1, Market::AShare)
.unwrap()
.to_dt_str(),
"2024-12-12 10:01:00"
);
assert_eq!(
freq_end_time(dt, Freq::F5, Market::AShare)
.unwrap()
.to_dt_str(),
"2024-12-12 10:05:00"
);
assert_eq!(
freq_end_time(dt, Freq::F30, Market::AShare)
.unwrap()
.to_dt_str(),
"2024-12-12 10:30:00"
);
assert_eq!(
freq_end_time(dt, Freq::F60, Market::AShare)
.unwrap()
.to_dt_str(),
"2024-12-12 10:30:00"
);
}
#[test]
fn test_freq_year() {
let dt = Utc.from_utc_datetime(
&NaiveDateTime::parse_from_str("2024-12-12 10:01:00", "%Y-%m-%d %H:%M:%S").unwrap(),
);
let res = freq_end_time(dt, Freq::Y, Market::AShare)
.unwrap()
.to_dt_str();
assert_eq!(res, "2024-12-31 00:00:00");
}
}