1use chrono::Duration;
22use serde::{Deserialize, Serialize};
23use std::borrow::Cow;
24use std::fmt;
25use std::num::{NonZeroU32, NonZeroU64};
26use std::str::FromStr;
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
58#[non_exhaustive]
59pub enum Timeframe {
60 Ms100,
63 Ms250,
65 Ms500,
67
68 Sec1,
71 Sec2,
73 Sec5,
75 Sec10,
77 Sec30,
79
80 Min1,
83 Min5,
85 Min15,
87 Min30,
89
90 Hour1,
93 Hour4,
95
96 Day1,
99 Week1,
101 Month1,
103
104 Custom(NonZeroU64),
107
108 Ticks(NonZeroU64),
110 Volume(NonZeroU64),
112 Range(NonZeroU64),
115 Delta(NonZeroU64),
118
119 Weeks(NonZeroU32),
121 Months(NonZeroU32),
124}
125
126impl Timeframe {
127 pub fn as_str(&self) -> Cow<'static, str> {
129 match self {
130 Timeframe::Weeks(count) => Cow::Owned(format!("{count}W")),
131 Timeframe::Months(count) => Cow::Owned(format!("{count}M")),
132 Timeframe::Ticks(count) => Cow::Owned(format!("{count}t")),
133 Timeframe::Volume(size) => Cow::Owned(format!("{size}v")),
134 Timeframe::Range(ticks) => Cow::Owned(format!("{ticks}r")),
135 Timeframe::Delta(size) => Cow::Owned(format!("{size}delta")),
136 Timeframe::Ms100 => Cow::Borrowed("100ms"),
137 Timeframe::Ms250 => Cow::Borrowed("250ms"),
138 Timeframe::Ms500 => Cow::Borrowed("500ms"),
139 Timeframe::Sec1 => Cow::Borrowed("1s"),
140 Timeframe::Sec2 => Cow::Borrowed("2s"),
141 Timeframe::Sec5 => Cow::Borrowed("5s"),
142 Timeframe::Sec10 => Cow::Borrowed("10s"),
143 Timeframe::Sec30 => Cow::Borrowed("30s"),
144 Timeframe::Min1 => Cow::Borrowed("1min"),
145 Timeframe::Min5 => Cow::Borrowed("5min"),
146 Timeframe::Min15 => Cow::Borrowed("15min"),
147 Timeframe::Min30 => Cow::Borrowed("30min"),
148 Timeframe::Hour1 => Cow::Borrowed("1h"),
149 Timeframe::Hour4 => Cow::Borrowed("4h"),
150 Timeframe::Day1 => Cow::Borrowed("1D"),
151 Timeframe::Week1 => Cow::Borrowed("1W"),
152 Timeframe::Month1 => Cow::Borrowed("1M"),
153 Timeframe::Custom(seconds) => Cow::Owned(format_custom_seconds(seconds.get())),
154 }
155 }
156
157 pub fn is_trade_based(self) -> bool {
159 matches!(
160 self,
161 Self::Ticks(_) | Self::Volume(_) | Self::Range(_) | Self::Delta(_)
162 )
163 }
164
165 pub fn all() -> Vec<Timeframe> {
167 vec![
168 Timeframe::Ms100,
169 Timeframe::Ms250,
170 Timeframe::Ms500,
171 Timeframe::Sec1,
172 Timeframe::Sec2,
173 Timeframe::Sec5,
174 Timeframe::Sec10,
175 Timeframe::Sec30,
176 Timeframe::Min1,
177 Timeframe::Min5,
178 Timeframe::Min15,
179 Timeframe::Min30,
180 Timeframe::Hour1,
181 Timeframe::Hour4,
182 Timeframe::Day1,
183 Timeframe::Week1,
184 Timeframe::Month1,
185 Timeframe::Months(NonZeroU32::new(3).unwrap()),
186 Timeframe::Months(NonZeroU32::new(6).unwrap()),
187 Timeframe::Months(NonZeroU32::new(12).unwrap()),
188 ]
189 }
190
191 pub fn duration_ms(&self) -> i64 {
195 match self {
196 Timeframe::Ticks(_)
197 | Timeframe::Volume(_)
198 | Timeframe::Range(_)
199 | Timeframe::Delta(_) => 0,
200 Timeframe::Ms100 => 100,
201 Timeframe::Ms250 => 250,
202 Timeframe::Ms500 => 500,
203 Timeframe::Sec1 => 1000,
204 Timeframe::Sec2 => 2000,
205 Timeframe::Sec5 => 5000,
206 Timeframe::Sec10 => 10000,
207 Timeframe::Sec30 => 30000,
208 Timeframe::Min1 => 60_000,
209 Timeframe::Min5 => 300_000,
210 Timeframe::Min15 => 900_000,
211 Timeframe::Min30 => 1_800_000,
212 Timeframe::Hour1 => 3_600_000,
213 Timeframe::Hour4 => 14_400_000,
214 Timeframe::Day1 => 86_400_000,
215 Timeframe::Week1 => 604_800_000,
216 Timeframe::Month1 => 2_592_000_000, Timeframe::Weeks(count) => i64::from(count.get()) * 604_800_000,
218 Timeframe::Months(count) => i64::from(count.get()).saturating_mul(2_592_000_000),
219 Timeframe::Custom(seconds) => {
220 seconds.get().saturating_mul(1000).min(i64::MAX as u64) as i64
221 }
222 }
223 }
224
225 pub fn duration(&self) -> Duration {
227 Duration::milliseconds(self.duration_ms())
228 }
229
230 pub fn is_custom(&self) -> bool {
232 match self {
233 Timeframe::Months(count) => !matches!(count.get(), 3 | 6 | 12),
234 _ => matches!(self, Timeframe::Custom(_) | Timeframe::Weeks(_)),
235 }
236 }
237
238 pub fn from_resolution(resolution: &str) -> Option<Self> {
240 if let Ok(timeframe) = resolution.parse::<Self>()
241 && (timeframe.is_trade_based() || matches!(timeframe, Self::Weeks(_) | Self::Months(_)))
242 {
243 return Some(timeframe);
244 }
245 match resolution {
246 "1" => Some(Timeframe::Min1),
247 "5" => Some(Timeframe::Min5),
248 "15" => Some(Timeframe::Min15),
249 "30" => Some(Timeframe::Min30),
250 "60" | "1H" => Some(Timeframe::Hour1),
251 "240" | "4H" => Some(Timeframe::Hour4),
252 "1D" | "D" => Some(Timeframe::Day1),
253 "1W" | "W" => Some(Timeframe::Week1),
254 "1M" | "M" => Some(Timeframe::Month1),
255 _ => None,
256 }
257 }
258}
259
260#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
263#[non_exhaustive]
264pub enum TimeframeUnit {
265 Tick,
267 Volume,
269 Range,
271 Delta,
273 Millisecond,
275 Second,
277 Minute,
279 Hour,
281 Day,
283 Week,
285 Month,
287}
288
289impl TimeframeUnit {
290 pub fn short_name(&self) -> &'static str {
292 match self {
293 TimeframeUnit::Tick => "T",
294 TimeframeUnit::Volume => "V",
295 TimeframeUnit::Range => "R",
296 TimeframeUnit::Delta => "delta",
297 TimeframeUnit::Millisecond => "ms",
298 TimeframeUnit::Second => "S",
299 TimeframeUnit::Minute => "m",
300 TimeframeUnit::Hour => "h",
301 TimeframeUnit::Day => "D",
302 TimeframeUnit::Week => "W",
303 TimeframeUnit::Month => "M",
304 }
305 }
306
307 pub fn name(&self) -> &'static str {
309 match self {
310 TimeframeUnit::Tick => "ticks",
311 TimeframeUnit::Volume => "volume units",
312 TimeframeUnit::Range => "range ticks",
313 TimeframeUnit::Delta => "delta units",
314 TimeframeUnit::Millisecond => "milliseconds",
315 TimeframeUnit::Second => "seconds",
316 TimeframeUnit::Minute => "minutes",
317 TimeframeUnit::Hour => "hours",
318 TimeframeUnit::Day => "days",
319 TimeframeUnit::Week => "weeks",
320 TimeframeUnit::Month => "months",
321 }
322 }
323}
324
325fn format_custom_seconds(seconds: u64) -> String {
328 if seconds >= 86400 && seconds.is_multiple_of(86400) {
329 format!("{}D", seconds / 86400)
330 } else if seconds >= 3600 && seconds.is_multiple_of(3600) {
331 format!("{}h", seconds / 3600)
332 } else if seconds >= 60 && seconds.is_multiple_of(60) {
333 format!("{}min", seconds / 60)
334 } else {
335 format!("{seconds}s")
336 }
337}
338
339impl Default for Timeframe {
340 fn default() -> Self {
342 Timeframe::Min1
343 }
344}
345
346impl fmt::Display for Timeframe {
347 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
348 write!(f, "{}", self.as_str())
349 }
350}
351
352impl FromStr for Timeframe {
353 type Err = super::ParseError;
354
355 fn from_str(s: &str) -> Result<Self, Self::Err> {
356 for (suffix, make) in [
357 ("delta", Timeframe::Delta as fn(NonZeroU64) -> Timeframe),
358 ("v", Timeframe::Volume),
359 ("r", Timeframe::Range),
360 ] {
361 if let Some(value) = s.to_lowercase().strip_suffix(suffix)
362 && value.chars().all(|c| c.is_ascii_digit())
363 {
364 return value.parse::<NonZeroU64>().map(make).map_err(|_| {
365 super::ParseError::new(
366 "timeframe",
367 s,
368 "trade interval must be a positive integer",
369 )
370 });
371 }
372 }
373 if let Some(count) = s.to_lowercase().strip_suffix('t') {
374 return count
375 .parse::<NonZeroU64>()
376 .map(Timeframe::Ticks)
377 .map_err(|_| {
378 super::ParseError::new("timeframe", s, "tick count must be a positive integer")
379 });
380 }
381 match s.to_lowercase().as_str() {
383 "100ms" => return Ok(Timeframe::Ms100),
384 "250ms" => return Ok(Timeframe::Ms250),
385 "500ms" => return Ok(Timeframe::Ms500),
386 "1s" | "1sec" => return Ok(Timeframe::Sec1),
387 "2s" | "2sec" => return Ok(Timeframe::Sec2),
388 "5s" | "5sec" => return Ok(Timeframe::Sec5),
389 "10s" | "10sec" => return Ok(Timeframe::Sec10),
390 "30s" | "30sec" => return Ok(Timeframe::Sec30),
391 "1min" => return Ok(Timeframe::Min1),
392 "5min" => return Ok(Timeframe::Min5),
393 "15min" => return Ok(Timeframe::Min15),
394 "30min" => return Ok(Timeframe::Min30),
395 "1h" | "1hour" => return Ok(Timeframe::Hour1),
396 "4h" | "4hour" => return Ok(Timeframe::Hour4),
397 "1d" | "1day" => return Ok(Timeframe::Day1),
398 "1w" | "1week" => return Ok(Timeframe::Week1),
399 "1m" | "1month" => return Ok(Timeframe::Month1),
400 _ => {}
401 }
402
403 for (suffix, make) in [
404 ("month", Timeframe::Months as fn(NonZeroU32) -> Timeframe),
405 ("m", Timeframe::Months),
406 ("week", Timeframe::Weeks),
407 ("w", Timeframe::Weeks),
408 ] {
409 if let Some(value) = s.to_lowercase().strip_suffix(suffix) {
410 return value.trim().parse::<NonZeroU32>().map(make).map_err(|_| {
411 super::ParseError::new(
412 "timeframe",
413 s,
414 "calendar interval must be a positive 32-bit integer",
415 )
416 });
417 }
418 }
419
420 let lower = s.to_lowercase();
422 if let Some(seconds) = parse_custom_timeframe(&lower) {
423 return NonZeroU64::new(seconds)
424 .map(Timeframe::Custom)
425 .ok_or_else(|| {
426 super::ParseError::new(
427 "timeframe",
428 s,
429 "custom timeframe must be greater than zero",
430 )
431 });
432 }
433
434 Err(super::ParseError::new(
435 "timeframe",
436 s,
437 "valid formats are 100ms, 250ms, 500ms, 1s-30s, 1min-30min, 1h, 4h, 1D, 1W, 1M, or custom (e.g. 45s, 3min, 2h)",
438 ))
439 }
440}
441
442fn parse_custom_timeframe(s: &str) -> Option<u64> {
448 let suffixes: &[(&str, u64)] = &[
449 ("hour", 3600),
450 ("min", 60),
451 ("sec", 1),
452 ("day", 86400),
453 ("s", 1),
454 ("h", 3600),
455 ("d", 86400),
456 ];
457
458 for &(suffix, multiplier) in suffixes {
459 if let Some(num_str) = s.strip_suffix(suffix) {
460 let num: u64 = num_str.trim().parse().ok()?;
461 return num.checked_mul(multiplier);
462 }
463 }
464
465 let num: u64 = s.trim().parse().ok()?;
467 num.checked_mul(60)
468}
469
470#[cfg(test)]
471mod tests {
472 use super::*;
473
474 fn custom(seconds: u64) -> Timeframe {
475 Timeframe::Custom(NonZeroU64::new(seconds).unwrap())
476 }
477
478 #[test]
479 fn test_preset_as_str() {
480 assert_eq!(&*Timeframe::Min1.as_str(), "1min");
481 assert_eq!(&*Timeframe::Hour4.as_str(), "4h");
482 assert_eq!(&*Timeframe::Day1.as_str(), "1D");
483 }
484
485 #[test]
486 fn test_custom_as_str() {
487 assert_eq!(&*custom(45).as_str(), "45s");
488 assert_eq!(&*custom(180).as_str(), "3min");
489 assert_eq!(&*custom(7200).as_str(), "2h");
490 assert_eq!(&*custom(172800).as_str(), "2D");
491 }
492
493 #[test]
494 fn test_custom_duration_ms() {
495 assert_eq!(custom(45).duration_ms(), 45_000);
496 assert_eq!(custom(180).duration_ms(), 180_000);
497 assert_eq!(custom(7200).duration_ms(), 7_200_000);
498 assert_eq!(custom(10_000_000_000_000_000).duration_ms(), i64::MAX);
500 assert_eq!(
501 custom(u64::MAX).duration(),
502 Duration::milliseconds(i64::MAX)
503 );
504 assert!("40000000000000w".parse::<Timeframe>().is_err());
505 }
506
507 #[test]
508 fn test_is_custom() {
509 assert!(!Timeframe::Min1.is_custom());
510 assert!(!Timeframe::Day1.is_custom());
511 assert!(custom(45).is_custom());
512 assert!(custom(180).is_custom());
513 for (text, expected) in [("2w", true), ("2month", true), ("3month", false)] {
514 assert_eq!(text.parse::<Timeframe>().unwrap().is_custom(), expected);
515 }
516 }
517
518 #[test]
519 fn test_from_str_presets() {
520 assert_eq!("1s".parse::<Timeframe>(), Ok(Timeframe::Sec1));
521 assert_eq!("5min".parse::<Timeframe>(), Ok(Timeframe::Min5));
522 assert_eq!("1min".parse::<Timeframe>(), Ok(Timeframe::Min1));
523 let ticks = Timeframe::Ticks(NonZeroU64::new(10).unwrap());
524 assert_eq!("10t".parse::<Timeframe>(), Ok(ticks));
525 assert_eq!(ticks.to_string(), "10t");
526 assert_eq!(Timeframe::from_resolution("10T"), Some(ticks));
527 for text in ["100v", "10r", "100delta"] {
528 let timeframe: Timeframe = text.parse().unwrap();
529 assert_eq!(timeframe.to_string(), text);
530 assert_eq!(
531 Timeframe::from_resolution(&text.to_uppercase()),
532 Some(timeframe)
533 );
534 }
535 assert!("0t".parse::<Timeframe>().is_err());
536 assert!(serde_json::from_str::<Timeframe>(r#"{"Ticks":0}"#).is_err());
537 assert_eq!("1h".parse::<Timeframe>(), Ok(Timeframe::Hour1));
538 assert_eq!("1d".parse::<Timeframe>(), Ok(Timeframe::Day1));
539 assert_eq!("1w".parse::<Timeframe>(), Ok(Timeframe::Week1));
540 assert_eq!("1m".parse::<Timeframe>(), Ok(Timeframe::Month1));
541 }
542
543 #[test]
544 fn test_from_str_custom() {
545 assert_eq!("45s".parse::<Timeframe>(), Ok(custom(45)));
546 assert_eq!("45sec".parse::<Timeframe>(), Ok(custom(45)));
547 assert_eq!("3min".parse::<Timeframe>(), Ok(custom(180)));
548 assert_eq!("2h".parse::<Timeframe>(), Ok(custom(7200)));
549 assert_eq!("2hour".parse::<Timeframe>(), Ok(custom(7200)));
550 assert_eq!("2d".parse::<Timeframe>(), Ok(custom(172800)));
551 assert_eq!("2day".parse::<Timeframe>(), Ok(custom(172800)));
552 for (text, label, saved) in [
553 ("2w", "2W", r#"{"Weeks":2}"#),
554 ("3month", "3M", r#"{"Months":3}"#),
555 ] {
556 let timeframe: Timeframe = text.parse().unwrap();
557 assert_eq!(serde_json::to_string(&timeframe).unwrap(), saved);
558 assert_eq!(timeframe.to_string(), label);
559 assert_eq!(label.parse::<Timeframe>(), Ok(timeframe));
560 assert_eq!(Timeframe::from_resolution(label), Some(timeframe));
561 assert_eq!(serde_json::from_str::<Timeframe>(saved).unwrap(), timeframe);
562 }
563 for text in [
564 "0s",
565 "0min",
566 "xyz",
567 "",
568 "0w",
569 "0month",
570 "-1month",
571 "4294967296month",
572 ] {
573 assert!(text.parse::<Timeframe>().is_err(), "{text:?}");
574 }
575 for text in [r#"{"Weeks":0}"#, r#"{"Months":0}"#] {
576 assert!(serde_json::from_str::<Timeframe>(text).is_err());
577 }
578 }
579
580 #[test]
581 fn deserialize_rejects_zero_length_custom_timeframe() {
582 assert!(serde_json::from_str::<Timeframe>(r#"{"Custom":0}"#).is_err());
583 let tf: Timeframe = serde_json::from_str(r#"{"Custom":45}"#).unwrap();
584 assert_eq!(tf.duration_ms(), 45_000);
585 for (index, expected) in [
587 (17_u32, custom(45)),
588 (18, Timeframe::Ticks(NonZeroU64::new(45).unwrap())),
589 (19, Timeframe::Volume(NonZeroU64::new(45).unwrap())),
590 (20, Timeframe::Range(NonZeroU64::new(45).unwrap())),
591 (21, Timeframe::Delta(NonZeroU64::new(45).unwrap())),
592 ] {
593 let map = serde::de::value::MapDeserializer::<_, serde::de::value::Error>::new(
594 [(index, 45_u64)].into_iter(),
595 );
596 assert_eq!(
597 Timeframe::deserialize(serde::de::value::MapAccessDeserializer::new(map)).unwrap(),
598 expected,
599 );
600 }
601 }
602}