Skip to main content

finance_query/constants/enums/
interval.rs

1use serde::{Deserialize, Serialize};
2
3/// Chart intervals
4///
5/// The `alias`es mirror the spellings [`FromStr`](std::str::FromStr) accepts, so
6/// deserializing (axum query extraction, JSON) takes the same spellings parsing does.
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
8pub enum Interval {
9    /// 1 minute
10    #[serde(rename = "1m")]
11    OneMinute,
12    /// 2 minutes
13    #[serde(rename = "2m")]
14    TwoMinutes,
15    /// 5 minutes
16    #[serde(rename = "5m")]
17    FiveMinutes,
18    /// 15 minutes
19    #[serde(rename = "15m")]
20    FifteenMinutes,
21    /// 30 minutes
22    #[serde(rename = "30m")]
23    ThirtyMinutes,
24    /// 1 hour
25    #[serde(rename = "1h", alias = "60m")]
26    OneHour,
27    /// 90 minutes
28    #[serde(rename = "90m")]
29    NinetyMinutes,
30    /// 1 day
31    #[serde(rename = "1d")]
32    OneDay,
33    /// 5 days
34    #[serde(rename = "5d")]
35    FiveDays,
36    /// 1 week
37    #[serde(rename = "1wk")]
38    OneWeek,
39    /// 1 month
40    #[serde(rename = "1mo")]
41    OneMonth,
42    /// 3 months
43    #[serde(rename = "3mo")]
44    ThreeMonths,
45}
46
47impl Interval {
48    /// Convert interval to Yahoo Finance API format
49    pub fn as_str(&self) -> &'static str {
50        match self {
51            Interval::OneMinute => "1m",
52            Interval::TwoMinutes => "2m",
53            Interval::FiveMinutes => "5m",
54            Interval::FifteenMinutes => "15m",
55            Interval::ThirtyMinutes => "30m",
56            Interval::OneHour => "1h",
57            Interval::NinetyMinutes => "90m",
58            Interval::OneDay => "1d",
59            Interval::FiveDays => "5d",
60            Interval::OneWeek => "1wk",
61            Interval::OneMonth => "1mo",
62            Interval::ThreeMonths => "3mo",
63        }
64    }
65
66    /// Bars of this interval in one trading year, for annualising returns and
67    /// prorating financing rates.
68    ///
69    /// Intraday counts assume a 6.5-hour US session over 252 trading days;
70    /// daily and coarser follow the calendar.
71    #[cfg(feature = "backtesting")]
72    pub const fn bars_per_year(self) -> f64 {
73        match self {
74            Interval::OneMinute => 98_280.0,
75            Interval::TwoMinutes => 49_140.0,
76            Interval::FiveMinutes => 19_656.0,
77            Interval::FifteenMinutes => 6_552.0,
78            Interval::ThirtyMinutes => 3_276.0,
79            Interval::OneHour => 1_638.0,
80            Interval::NinetyMinutes => 1_092.0,
81            Interval::OneDay => 252.0,
82            Interval::FiveDays => 50.4,
83            Interval::OneWeek => 52.0,
84            Interval::OneMonth => 12.0,
85            Interval::ThreeMonths => 4.0,
86        }
87    }
88
89    /// Approximate span one candle of this interval covers, in seconds.
90    ///
91    /// Calendar approximations match [`TimeRange::approx_duration_secs`](super::TimeRange::approx_duration_secs): a
92    /// month is 30 days, so a quarter is 90.
93    #[cfg(any(feature = "backtesting", feature = "binance", feature = "kraken"))]
94    pub(crate) const fn duration_secs(self) -> i64 {
95        match self {
96            Interval::OneMinute => 60,
97            Interval::TwoMinutes => 120,
98            Interval::FiveMinutes => 300,
99            Interval::FifteenMinutes => 900,
100            Interval::ThirtyMinutes => 1_800,
101            Interval::OneHour => 3_600,
102            Interval::NinetyMinutes => 5_400,
103            Interval::OneDay => 86_400,
104            Interval::FiveDays => 432_000,
105            Interval::OneWeek => 604_800,
106            Interval::OneMonth => 2_592_000,
107            Interval::ThreeMonths => 7_776_000,
108        }
109    }
110}
111
112impl std::fmt::Display for Interval {
113    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
114        f.write_str(self.as_str())
115    }
116}
117
118impl std::str::FromStr for Interval {
119    type Err = ();
120
121    /// Parses the same short codes returned by [`Interval::as_str`] (e.g. `"1d"`,
122    /// `"1wk"`), case-insensitively. `"60m"` is also accepted as an alias for
123    /// [`Interval::OneHour`] (Yahoo itself normalizes `60m` to `1h`).
124    fn from_str(s: &str) -> Result<Self, Self::Err> {
125        match s.trim().to_lowercase().as_str() {
126            "1m" => Ok(Interval::OneMinute),
127            "2m" => Ok(Interval::TwoMinutes),
128            "5m" => Ok(Interval::FiveMinutes),
129            "15m" => Ok(Interval::FifteenMinutes),
130            "30m" => Ok(Interval::ThirtyMinutes),
131            "1h" | "60m" => Ok(Interval::OneHour),
132            "90m" => Ok(Interval::NinetyMinutes),
133            "1d" => Ok(Interval::OneDay),
134            "5d" => Ok(Interval::FiveDays),
135            "1wk" => Ok(Interval::OneWeek),
136            "1mo" => Ok(Interval::OneMonth),
137            "3mo" => Ok(Interval::ThreeMonths),
138            _ => Err(()),
139        }
140    }
141}
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146
147    #[test]
148    fn test_interval_as_str() {
149        assert_eq!(Interval::OneMinute.as_str(), "1m");
150        assert_eq!(Interval::FiveMinutes.as_str(), "5m");
151        assert_eq!(Interval::OneDay.as_str(), "1d");
152        assert_eq!(Interval::OneWeek.as_str(), "1wk");
153    }
154
155    #[test]
156    fn test_interval_from_str_round_trips_as_str() {
157        for interval in [
158            Interval::OneMinute,
159            Interval::TwoMinutes,
160            Interval::FiveMinutes,
161            Interval::FifteenMinutes,
162            Interval::ThirtyMinutes,
163            Interval::OneHour,
164            Interval::NinetyMinutes,
165            Interval::OneDay,
166            Interval::FiveDays,
167            Interval::OneWeek,
168            Interval::OneMonth,
169            Interval::ThreeMonths,
170        ] {
171            assert_eq!(interval.as_str().parse(), Ok(interval));
172        }
173        assert_eq!("1D".parse(), Ok(Interval::OneDay));
174        assert_eq!(" 1d ".parse(), Ok(Interval::OneDay));
175        assert_eq!("bogus".parse::<Interval>(), Err(()));
176    }
177
178    #[cfg(any(feature = "backtesting", feature = "binance", feature = "kraken"))]
179    #[test]
180    fn test_interval_duration_secs() {
181        use super::super::TimeRange;
182
183        assert_eq!(Interval::OneMinute.duration_secs(), 60);
184        assert_eq!(Interval::OneHour.duration_secs(), 3_600);
185        assert_eq!(Interval::OneDay.duration_secs(), 86_400);
186        assert_eq!(Interval::OneWeek.duration_secs(), 604_800);
187        // Calendar approximations agree with TimeRange's: 30-day months.
188        assert_eq!(
189            Interval::OneMonth.duration_secs(),
190            TimeRange::OneMonth.approx_duration_secs()
191        );
192        assert_eq!(
193            Interval::ThreeMonths.duration_secs(),
194            TimeRange::ThreeMonths.approx_duration_secs()
195        );
196    }
197}