pub fn infer_periods_per_year(timestamps: &[NaiveDateTime]) -> Option<f64>Expand description
Bars a year implied by the spacing of timestamps, for
ResearchPipelineConfig::periods_per_year.
Takes the median gap Δ between consecutive strictly increasing timestamps (gaps that are
zero or negative are skipped, so ties and unordered pairs do not count) and maps it with the
convention of the module documentation:
- intraday,
Δunder 20 hours:252 * 390 / Δ_minutesbars a year, i.e. a 390-minute session on 252 days (one-minute bars: 98,280; hourly: 1,638). Bars that trade around the clock (futures, crypto) keep this session convention; pass an explicit value instead if you want a 24-hour one. - daily,
Δfrom 20 hours to under 4 days (so weekends and holidays inside a daily series do not matter): 252. - longer,
Δof 4 days or more:365.25 / Δ_days(weekly bars: 52.2, monthly bars of 30.44 days: 12).
The median makes it robust to overnight and weekend gaps in intraday data and to the odd missing bar. For bars sampled on activity (tick, volume or dollar bars) it gives the typical bar rate, which is only an approximation; pass the value yourself when you know it.
Returns None when fewer than two timestamps strictly increase, i.e. there is no gap to
measure.
§Example
use chrono::{Datelike, Duration, NaiveDate, Weekday};
use openquant::pipeline::{infer_periods_per_year, MINUTE_BARS_PER_YEAR};
let open = NaiveDate::from_ymd_opt(2024, 1, 2).unwrap().and_hms_opt(9, 30, 0).unwrap();
let minutes: Vec<_> = (0..390).map(|i| open + Duration::minutes(i)).collect();
assert_eq!(infer_periods_per_year(&minutes), Some(MINUTE_BARS_PER_YEAR));
assert_eq!(MINUTE_BARS_PER_YEAR, 98_280.0);
// Weekdays only: the weekend gaps are outliers the median ignores.
let days: Vec<_> = (0..14)
.map(|d| open + Duration::days(d))
.filter(|t| !matches!(t.weekday(), Weekday::Sat | Weekday::Sun))
.collect();
assert_eq!(infer_periods_per_year(&days), Some(252.0));
assert_eq!(infer_periods_per_year(&minutes[..1]), None);