Skip to main content

finance_query/constants/enums/
time_range.rs

1use serde::{Deserialize, Serialize};
2
3use super::Interval;
4
5/// Time ranges for chart data
6///
7/// The `alias`es mirror the spellings [`FromStr`](std::str::FromStr) accepts, so
8/// deserializing (axum query extraction, JSON) takes the same spellings parsing does.
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
10pub enum TimeRange {
11    /// 1 day
12    #[serde(rename = "1d")]
13    OneDay,
14    /// 5 days
15    #[serde(rename = "5d", alias = "1wk")]
16    FiveDays,
17    /// 1 month
18    #[serde(rename = "1mo")]
19    OneMonth,
20    /// 3 months
21    #[serde(rename = "3mo")]
22    ThreeMonths,
23    /// 6 months
24    #[serde(rename = "6mo")]
25    SixMonths,
26    /// 1 year
27    #[serde(rename = "1y")]
28    OneYear,
29    /// 2 years
30    #[serde(rename = "2y")]
31    TwoYears,
32    /// 5 years
33    #[serde(rename = "5y")]
34    FiveYears,
35    /// 10 years
36    #[serde(rename = "10y")]
37    TenYears,
38    /// Year to date
39    #[serde(rename = "ytd")]
40    YearToDate,
41    /// Maximum available
42    #[serde(rename = "max")]
43    Max,
44}
45
46impl TimeRange {
47    /// Convert time range to Yahoo Finance API format
48    pub fn as_str(&self) -> &'static str {
49        match self {
50            TimeRange::OneDay => "1d",
51            TimeRange::FiveDays => "5d",
52            TimeRange::OneMonth => "1mo",
53            TimeRange::ThreeMonths => "3mo",
54            TimeRange::SixMonths => "6mo",
55            TimeRange::OneYear => "1y",
56            TimeRange::TwoYears => "2y",
57            TimeRange::FiveYears => "5y",
58            TimeRange::TenYears => "10y",
59            TimeRange::YearToDate => "ytd",
60            TimeRange::Max => "max",
61        }
62    }
63
64    /// A sensible default candle interval for this range, used by the
65    /// `history(range)` convenience on domain handles: finer granularity for
66    /// short ranges, coarser for long ones.
67    pub fn default_interval(&self) -> Interval {
68        match self {
69            TimeRange::OneDay => Interval::FiveMinutes,
70            TimeRange::FiveDays => Interval::FifteenMinutes,
71            TimeRange::OneMonth
72            | TimeRange::ThreeMonths
73            | TimeRange::SixMonths
74            | TimeRange::OneYear
75            | TimeRange::YearToDate => Interval::OneDay,
76            TimeRange::TwoYears | TimeRange::FiveYears => Interval::OneWeek,
77            TimeRange::TenYears | TimeRange::Max => Interval::OneMonth,
78        }
79    }
80
81    /// Approximate span of this range in seconds.
82    ///
83    /// Calendar approximations: a month is 30 days, a year 365. `YearToDate` is
84    /// approximated as one year and `Max` as a far-future horizon.
85    pub const fn approx_duration_secs(&self) -> i64 {
86        const DAY: i64 = 86_400;
87        match self {
88            TimeRange::OneDay => DAY,
89            TimeRange::FiveDays => 5 * DAY,
90            TimeRange::OneMonth => 30 * DAY,
91            TimeRange::ThreeMonths => 90 * DAY,
92            TimeRange::SixMonths => 180 * DAY,
93            TimeRange::OneYear | TimeRange::YearToDate => 365 * DAY,
94            TimeRange::TwoYears => 730 * DAY,
95            TimeRange::FiveYears => 1_825 * DAY,
96            TimeRange::TenYears => 3_650 * DAY,
97            TimeRange::Max => 36_500 * DAY,
98        }
99    }
100}
101
102impl std::fmt::Display for TimeRange {
103    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
104        f.write_str(self.as_str())
105    }
106}
107
108impl std::str::FromStr for TimeRange {
109    type Err = ();
110
111    /// Parses the same short codes returned by [`TimeRange::as_str`] (e.g. `"3mo"`,
112    /// `"ytd"`), case-insensitively. `"1wk"` is also accepted as an alias for
113    /// [`TimeRange::FiveDays`] (a trading week).
114    fn from_str(s: &str) -> Result<Self, Self::Err> {
115        match s.trim().to_lowercase().as_str() {
116            "1d" => Ok(TimeRange::OneDay),
117            "5d" | "1wk" => Ok(TimeRange::FiveDays),
118            "1mo" => Ok(TimeRange::OneMonth),
119            "3mo" => Ok(TimeRange::ThreeMonths),
120            "6mo" => Ok(TimeRange::SixMonths),
121            "1y" => Ok(TimeRange::OneYear),
122            "2y" => Ok(TimeRange::TwoYears),
123            "5y" => Ok(TimeRange::FiveYears),
124            "10y" => Ok(TimeRange::TenYears),
125            "ytd" => Ok(TimeRange::YearToDate),
126            "max" => Ok(TimeRange::Max),
127            _ => Err(()),
128        }
129    }
130}
131
132#[cfg(test)]
133mod tests {
134    use super::*;
135
136    #[test]
137    fn test_time_range_as_str() {
138        assert_eq!(TimeRange::OneDay.as_str(), "1d");
139        assert_eq!(TimeRange::OneMonth.as_str(), "1mo");
140        assert_eq!(TimeRange::OneYear.as_str(), "1y");
141        assert_eq!(TimeRange::Max.as_str(), "max");
142    }
143
144    #[test]
145    fn test_time_range_from_str_round_trips_as_str() {
146        for range in [
147            TimeRange::OneDay,
148            TimeRange::FiveDays,
149            TimeRange::OneMonth,
150            TimeRange::ThreeMonths,
151            TimeRange::SixMonths,
152            TimeRange::OneYear,
153            TimeRange::TwoYears,
154            TimeRange::FiveYears,
155            TimeRange::TenYears,
156            TimeRange::YearToDate,
157            TimeRange::Max,
158        ] {
159            assert_eq!(range.as_str().parse(), Ok(range));
160        }
161        assert_eq!("1wk".parse(), Ok(TimeRange::FiveDays));
162        assert_eq!("YTD".parse(), Ok(TimeRange::YearToDate));
163        assert_eq!("bogus".parse::<TimeRange>(), Err(()));
164    }
165
166    #[test]
167    fn test_default_interval_buckets() {
168        // Intraday ranges → sub-day candles; long ranges → coarse candles.
169        assert_eq!(TimeRange::OneDay.default_interval(), Interval::FiveMinutes);
170        assert_eq!(
171            TimeRange::FiveDays.default_interval(),
172            Interval::FifteenMinutes
173        );
174        assert_eq!(TimeRange::OneMonth.default_interval(), Interval::OneDay);
175        assert_eq!(TimeRange::OneYear.default_interval(), Interval::OneDay);
176        assert_eq!(TimeRange::TwoYears.default_interval(), Interval::OneWeek);
177        assert_eq!(TimeRange::Max.default_interval(), Interval::OneMonth);
178    }
179}