#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum TimeframeUnit {
Ticks,
Seconds,
Minutes,
Daily,
Weekly,
Monthly,
}
impl TimeframeUnit {
pub fn suffix(self) -> &'static str {
match self {
TimeframeUnit::Ticks => "T",
TimeframeUnit::Seconds => "S",
TimeframeUnit::Minutes => "",
TimeframeUnit::Daily => "D",
TimeframeUnit::Weekly => "W",
TimeframeUnit::Monthly => "M",
}
}
pub fn from_suffix(suffix: &str) -> Option<Self> {
Some(match suffix {
"T" => TimeframeUnit::Ticks,
"S" => TimeframeUnit::Seconds,
"" => TimeframeUnit::Minutes,
"D" => TimeframeUnit::Daily,
"W" => TimeframeUnit::Weekly,
"M" => TimeframeUnit::Monthly,
_ => return None,
})
}
pub fn millis(self) -> Option<i64> {
Some(match self {
TimeframeUnit::Seconds => 1_000,
TimeframeUnit::Minutes => 60_000,
TimeframeUnit::Daily => 86_400_000,
TimeframeUnit::Weekly => 604_800_000,
TimeframeUnit::Ticks | TimeframeUnit::Monthly => return None,
})
}
}
#[derive(Clone, Debug)]
pub struct Timeframe {
pub multiplier: u32,
pub unit: TimeframeUnit,
}
impl Default for Timeframe {
fn default() -> Self {
Self {
multiplier: 1,
unit: TimeframeUnit::Daily,
}
}
}
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
#[error("unrecognised timeframe {0:?}")]
pub struct TimeframeError(pub String);
impl std::str::FromStr for Timeframe {
type Err = TimeframeError;
fn from_str(period: &str) -> Result<Self, Self::Err> {
let bad = || TimeframeError(period.to_string());
let split = period
.find(|c: char| !c.is_ascii_digit())
.unwrap_or(period.len());
let (count, suffix) = period.split_at(split);
Ok(Self {
multiplier: count.parse().ok().filter(|&m| m > 0).ok_or_else(bad)?,
unit: TimeframeUnit::from_suffix(suffix).ok_or_else(bad)?,
})
}
}
const REGULAR_UNITS: [TimeframeUnit; 4] = [
TimeframeUnit::Weekly,
TimeframeUnit::Daily,
TimeframeUnit::Minutes,
TimeframeUnit::Seconds,
];
impl Timeframe {
pub fn as_minutes(&self) -> Option<u32> {
let per_unit = match self.unit {
TimeframeUnit::Minutes => 1,
TimeframeUnit::Daily => 60 * 24,
TimeframeUnit::Weekly => 60 * 24 * 7,
TimeframeUnit::Ticks | TimeframeUnit::Seconds | TimeframeUnit::Monthly => return None,
};
self.multiplier.checked_mul(per_unit)
}
pub fn from_millis(millis: i64) -> Option<Self> {
if millis <= 0 {
return None;
}
REGULAR_UNITS
.into_iter()
.filter_map(|unit| Some((unit, unit.millis()?)))
.find(|(_, size)| millis % size == 0)
.map(|(unit, size)| Self {
multiplier: (millis / size) as u32,
unit,
})
}
pub fn period(&self) -> String {
format!("{}{}", self.multiplier, self.unit.suffix())
}
pub fn to_millis(&self) -> Option<i64> {
Some(self.unit.millis()? * i64::from(self.multiplier))
}
pub fn is_seconds(&self) -> bool {
self.unit == TimeframeUnit::Seconds
}
pub fn is_minutes(&self) -> bool {
self.unit == TimeframeUnit::Minutes
}
pub fn is_daily(&self) -> bool {
self.unit == TimeframeUnit::Daily
}
pub fn is_weekly(&self) -> bool {
self.unit == TimeframeUnit::Weekly
}
pub fn is_monthly(&self) -> bool {
self.unit == TimeframeUnit::Monthly
}
pub fn is_ticks(&self) -> bool {
self.unit == TimeframeUnit::Ticks
}
pub fn is_intraday(&self) -> bool {
self.is_seconds() || self.is_minutes()
}
pub fn is_dwm(&self) -> bool {
self.is_daily() || self.is_weekly() || self.is_monthly()
}
}
#[cfg(test)]
mod tests {
use super::Timeframe;
use std::str::FromStr;
#[test]
fn parses_pine_period_notation() {
for period in ["30S", "5", "60", "240", "1D", "1W", "1M"] {
assert_eq!(Timeframe::from_str(period).unwrap().period(), period);
}
assert!(Timeframe::from_str("5").unwrap().is_minutes());
}
#[test]
fn parses_millisecond_lengths() {
assert_eq!(
Timeframe::from_str("30S").unwrap().to_millis(),
Some(30_000)
);
assert_eq!(Timeframe::from_str("5").unwrap().to_millis(), Some(300_000));
assert_eq!(
Timeframe::from_str("1D").unwrap().to_millis(),
Some(86_400_000)
);
assert_eq!(Timeframe::from_str("1M").unwrap().to_millis(), None);
}
#[test]
fn an_unreadable_interval_is_an_error() {
assert!(Timeframe::from_str("").is_err());
assert!(Timeframe::from_str("hourly").is_err());
assert!(Timeframe::from_str("1y").is_err()); assert!(Timeframe::from_str("5m").is_err()); assert!(Timeframe::from_str("d1").is_err());
}
}