├── .gitignore ├── src ├── lib.rs ├── helpers.rs ├── traits.rs ├── errors.rs ├── indicators │ ├── mod.rs │ ├── minimum.rs │ ├── simple_moving_average.rs │ ├── rate_of_change.rs │ ├── mean_absolute_deviation.rs │ ├── maximum.rs │ ├── bollinger_bands.rs │ ├── max_drawdown.rs │ ├── standard_deviation.rs │ ├── max_drawup.rs │ ├── relative_strength_index.rs │ ├── exponential_moving_average.rs │ └── adaptive.rs ├── test_helper.rs └── data_item.rs ├── .travis.yml ├── tests ├── test.rs └── test_adaptive_replacement.rs ├── LICENSE ├── Cargo.toml ├── CHANGELOG.md └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | **/*.rs.bk 3 | Cargo.lock 4 | .vscode -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | #[macro_use] 3 | mod test_helper; 4 | 5 | mod helpers; 6 | 7 | pub mod errors; 8 | pub mod indicators; 9 | 10 | mod traits; 11 | pub use crate::traits::*; 12 | 13 | mod data_item; 14 | pub use crate::data_item::DataItem; 15 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | cache: cargo # cache dependencies to reduce compilation times 3 | rust: 4 | - stable 5 | install: 6 | - rustup component add rustfmt 7 | - rustup component add clippy 8 | script: 9 | - cargo fmt -- --check 10 | # - cargo clippy -- -D warnings 11 | - cargo test 12 | - cargo test --features serde 13 | - cargo package 14 | -------------------------------------------------------------------------------- /src/helpers.rs: -------------------------------------------------------------------------------- 1 | /// Returns the largest of 3 given numbers. 2 | pub fn max3(a: f64, b: f64, c: f64) -> f64 { 3 | a.max(b).max(c) 4 | } 5 | 6 | #[cfg(test)] 7 | mod tests { 8 | use super::*; 9 | 10 | #[test] 11 | fn test_max3() { 12 | assert_eq!(max3(3.0, 2.0, 1.0), 3.0); 13 | assert_eq!(max3(2.0, 3.0, 1.0), 3.0); 14 | assert_eq!(max3(2.0, 1.0, 3.0), 3.0); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/traits.rs: -------------------------------------------------------------------------------- 1 | // Indicator traits 2 | 3 | use chrono::{DateTime, Utc}; 4 | 5 | /// Resets an indicator to the initial state. 6 | pub trait Reset { 7 | fn reset(&mut self); 8 | } 9 | 10 | /// Consumes a data item of type `T` and returns `Output`. 11 | /// 12 | /// Typically `T` can be `f64` or a struct similar to [DataItem](struct.DataItem.html), that implements 13 | /// traits necessary to calculate value of a particular indicator. 14 | /// 15 | /// In most cases `Output` is `f64`, but sometimes it can be different. For example for 16 | /// [MACD](indicators/struct.MovingAverageConvergenceDivergence.html) it is `(f64, f64, f64)` since 17 | /// MACD returns 3 values. 18 | /// 19 | pub trait Next { 20 | type Output; 21 | fn next(&mut self, input: (DateTime, T)) -> Self::Output; 22 | } 23 | 24 | 25 | -------------------------------------------------------------------------------- /tests/test.rs: -------------------------------------------------------------------------------- 1 | extern crate csv; 2 | extern crate ta; 3 | 4 | // TODO: implement some integration tests 5 | 6 | #[cfg(test)] 7 | mod test { 8 | #[cfg(feature = "serde")] 9 | mod serde { 10 | use chrono::Utc; 11 | use std::time::Duration; 12 | use ta::indicators::SimpleMovingAverage; 13 | use ta::Next; 14 | 15 | // Simple smoke test that serde works (not sure if this is really necessary) 16 | #[test] 17 | fn test_serde() { 18 | let mut sma = SimpleMovingAverage::new(Duration::from_secs(20)).unwrap(); 19 | let bytes = bincode::serialize(&sma).unwrap(); 20 | let mut deserialized: SimpleMovingAverage = bincode::deserialize(&bytes).unwrap(); 21 | 22 | let now = Utc::now(); 23 | assert_eq!(deserialized.next((now, 2.0)), sma.next((now, 2.0))); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/errors.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error; 2 | use std::fmt::{Display, Formatter}; 3 | 4 | pub type Result = std::result::Result; 5 | 6 | #[derive(Debug, PartialEq, Eq, Clone)] 7 | pub enum TaError { 8 | InvalidParameter, 9 | DataItemIncomplete, 10 | DataItemInvalid, 11 | } 12 | 13 | impl Display for TaError { 14 | fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { 15 | match *self { 16 | TaError::InvalidParameter => write!(f, "invalid parameter"), 17 | TaError::DataItemIncomplete => write!(f, "data item is incomplete"), 18 | TaError::DataItemInvalid => write!(f, "data item is invalid"), 19 | } 20 | } 21 | } 22 | 23 | impl Error for TaError { 24 | fn source(&self) -> Option<&(dyn Error + 'static)> { 25 | match *self { 26 | TaError::InvalidParameter => None, 27 | TaError::DataItemIncomplete => None, 28 | TaError::DataItemInvalid => None, 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/indicators/mod.rs: -------------------------------------------------------------------------------- 1 | mod exponential_moving_average; 2 | pub use self::exponential_moving_average::ExponentialMovingAverage; 3 | 4 | mod simple_moving_average; 5 | pub use self::simple_moving_average::SimpleMovingAverage; 6 | 7 | mod standard_deviation; 8 | pub use self::standard_deviation::StandardDeviation; 9 | 10 | mod mean_absolute_deviation; 11 | pub use self::mean_absolute_deviation::MeanAbsoluteDeviation; 12 | 13 | mod relative_strength_index; 14 | pub use self::relative_strength_index::RelativeStrengthIndex; 15 | 16 | mod minimum; 17 | pub use self::minimum::Minimum; 18 | 19 | mod maximum; 20 | pub use self::maximum::Maximum; 21 | 22 | mod max_drawdown; 23 | pub use self::max_drawdown::MaxDrawdown; 24 | 25 | mod max_drawup; 26 | pub use self::max_drawup::MaxDrawup; 27 | 28 | mod bollinger_bands; 29 | pub use self::bollinger_bands::{BollingerBands, BollingerBandsOutput}; 30 | 31 | mod rate_of_change; 32 | pub use self::rate_of_change::RateOfChange; 33 | 34 | mod adaptive; 35 | pub use self::adaptive::{AdaptiveTimeDetector, DetectedFrequency}; 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2023 Austin Starks 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ta" 3 | version = "0.5.0" 4 | authors = ["Austin Starks "] 5 | edition = "2021" 6 | description = "Technical analysis library. Implements number of indicators: EMA, SMA, RSI, MACD, Stochastic, etc." 7 | keywords = ["technical-analysis", "financial", "ema", "indicators", "trading"] 8 | license = "MIT" 9 | repository = "https://github.com/austin-starks/ta-rs-improved" 10 | homepage = "https://github.com/austin-starks/ta-rs-improved" 11 | documentation = "https://docs.rs/ta" 12 | readme = "README.md" 13 | categories = ["science", "algorithms"] 14 | include = [ 15 | "src/**/*", 16 | "Cargo.toml", 17 | "README.md" 18 | ] 19 | 20 | [badges] 21 | travis-ci = { repository = "greyblake/ta-rs", branch = "master" } 22 | 23 | [dependencies] 24 | serde = { version = "1.0", features = ["derive"], optional = true } 25 | chrono = { version = "0.4", features = ["serde"] } 26 | 27 | [dev-dependencies] 28 | assert_approx_eq = "1.0.0" 29 | csv = "1.1.0" 30 | bencher = "0.1.5" 31 | rand = "0.6.5" 32 | bincode = "1.3.1" 33 | 34 | [profile.release] 35 | lto = true 36 | 37 | [[bench]] 38 | name = "indicators" 39 | path = "benches/indicators.rs" 40 | harness = false 41 | 42 | [[example]] 43 | name = "ema_serde" 44 | path = "examples/ema_serde.rs" 45 | required-features = ["serde"] 46 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | #### Unreleased 2 | 3 | * Add Weighted Moving Average (WMA) 4 | 5 | 6 | #### v0.5.0 - 2021-06-27 7 | 8 | * [breaking] - get rid of error-chain. ta::Error -> ta::TaError 9 | * Implement Mean Absolute Deviation (MAD) 10 | * Implement Commodity Channel Index (CCI) 11 | * More efficient MoneyFlowIndex 12 | * Fix identical TypicalPrice bug in MoneyFlowIndex 13 | 14 | #### v0.4.0 - 2020-11-03 15 | 16 | * [breaking] Unify parameters for the indicators 17 | * Implement Chandelier Exit (CE) 18 | 19 | #### v0.3.1 - 2020-10-20 20 | 21 | * Fix NaN bug in StandardDeviation 22 | 23 | #### v0.3.0 - 2020-10-06 24 | 25 | * Implement Percentage Price Oscillator (PPO) 26 | * More efficient BollingerBands 27 | * More efficient FastStochastic 28 | * More efficient SlowStochastic 29 | * More efficient StandardDeviation 30 | * More efficient Minimum 31 | * More efficient Maximum 32 | * More efficient SimpleMovingAverage 33 | * Serde support 34 | 35 | #### v0.2.0 - 2020-08-31 36 | 37 | * Breaking: MovingAverageConvergenceDivergence now returns MovingAverageConvergenceDivergenceOutput instead of tuple 38 | * Implement Keltner Channel (KC) 39 | * Update error-chain dependency: 0.11 -> 0.12 40 | 41 | #### v0.1.5 - 2019-12-16 42 | 43 | * StandardDeviation Implementation 44 | * More Efficient BollingerBands 45 | 46 | #### v0.1.4 - 2019-04-09 47 | 48 | * Implement On Balance Volume (OBV) 49 | 50 | #### v0.1.3 - 2019-03-28 51 | 52 | * Implement Money Flow Index (MFI) 53 | * Add benchmarks 54 | 55 | #### v0.1.2 - 2019-03-17 56 | 57 | * Implement Bollinger Bands (BB) 58 | 59 | #### v0.1.1 - 2019-02-26 60 | 61 | * Implement Kaufman's Efficiency Ratio (ER) 62 | * Implement Rate of Change (ROC) 63 | * Migrate to Rust 2018 edition 64 | 65 | #### v0.1.0 - 2017-12-05 66 | 67 | * Initial release 68 | * Implemented indicators 69 | * Trend 70 | * Exponential Moving Average (EMA) 71 | * Simple Moving Average (SMA) 72 | * Oscillators 73 | * Relative Strength Index (RSI) 74 | * Fast Stochastic 75 | * Slow Stochastic 76 | * Moving Average Convergence Divergence (MACD) 77 | * Other 78 | * Minimum 79 | * Maximum 80 | * True Range 81 | * Average True Range (AR) 82 | * Rate of Change (ROC) 83 | -------------------------------------------------------------------------------- /src/test_helper.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, PartialEq)] 2 | pub struct Bar { 3 | open: f64, 4 | high: f64, 5 | low: f64, 6 | close: f64, 7 | volume: f64, 8 | } 9 | 10 | impl Bar { 11 | pub fn new() -> Self { 12 | Self { 13 | open: 0.0, 14 | close: 0.0, 15 | low: 0.0, 16 | high: 0.0, 17 | volume: 0.0, 18 | } 19 | } 20 | 21 | pub fn high>(mut self, val: T) -> Self { 22 | self.high = val.into(); 23 | self 24 | } 25 | 26 | pub fn low>(mut self, val: T) -> Self { 27 | self.low = val.into(); 28 | self 29 | } 30 | 31 | pub fn close>(mut self, val: T) -> Self { 32 | self.close = val.into(); 33 | self 34 | } 35 | 36 | pub fn volume(mut self, val: f64) -> Self { 37 | self.volume = val; 38 | self 39 | } 40 | } 41 | 42 | pub fn round(num: f64) -> f64 { 43 | (num * 1000.0).round() / 1000.00 44 | } 45 | 46 | macro_rules! test_indicator { 47 | ($i:tt) => { 48 | #[test] 49 | fn test_indicator() { 50 | use chrono::TimeZone; // Import TimeZone trait to use the Utc.ymd method 51 | 52 | let bar = Bar::new(); 53 | 54 | // Create a fixed timestamp for testing 55 | let timestamp = Utc.ymd(2023, 1, 1).and_hms(0, 0, 0); 56 | 57 | // ensure Default trait is implemented 58 | let mut indicator = $i::default(); 59 | 60 | // ensure Next is implemented 61 | // Provide a tuple with the timestamp and the value 62 | let first_output = indicator.next((timestamp, 12.3)); 63 | 64 | // ensure next accepts &DataItem as well 65 | // You will need to modify the implementation of Next for &DataItem 66 | // to accept a tuple with a timestamp as well 67 | // For example: 68 | // indicator.next((timestamp, &bar)); 69 | 70 | // ensure Reset is implemented and works correctly 71 | indicator.reset(); 72 | assert_eq!(indicator.next((timestamp, 12.3)), first_output); 73 | 74 | // ensure Display is implemented 75 | format!("{}", indicator); 76 | } 77 | }; 78 | } 79 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Technical Analysis for Rust (ta) 2 | 3 | Technical analysis library for Rust. 4 | 5 | - [Introduction](#introduction) 6 | - [Getting started](#getting-started) 7 | - [Basic ideas](#basic-ideas) 8 | - [List of indicators](#list-of-indicators) 9 | - [Contributors](#contributors) 10 | 11 | ## Introduction 12 | 13 | This is ta-rs-improved, an improved version of the technical indicator library in Rust. There are two notable changes that makes this 14 | application improved. 15 | 16 | - Dynamic Window Sizes. This means you can do a 30 day SMA and a 15 hour SMA. 17 | - **Correct calculation of the Relative Strength Index (RSI)** 18 | 19 | This library is used to power [NexusTrade](https://nexustrade.io/), an AI-Powered automated investing research platform. NexusTrade allows even non-technical users to perform financial research, create automated strategies, and optimize those strategies with an easy-to-use UI. Users can then deploy their strategies live to the market with the click of a button. 20 | 21 | For more information about this repository, [read the following article.](https://nexustrade.io/blog/i-used-an-ai-to-fix-a-major-bug-in-a-very-popular-open-source-technical-indicator-library-20231223) 22 | 23 | ## NexusTrade – AI-Powered Trading 24 | 25 | These indicators are implemented in [NexusTrade](https://nexustrade.io/). NexusTrade is an AI-Powered algorithmic trading platform that lets users create, test, optimize, and deploy automated trading strategies. Try it now for free! 26 | 27 | ## Getting started 28 | 29 | Add to you `Cargo.toml`: 30 | 31 | ``` 32 | [dependencies] 33 | ta = { git = "https://github.com/austin-starks/ta-rs-improved" } 34 | ``` 35 | 36 | Example: 37 | 38 | ```rust 39 | use ta::indicators::ExponentialMovingAverage; 40 | use ta::Next; 41 | 42 | let mut ema = ExponentialMovingAverage::new(Duration::seconds(3)).unwrap(); // window size of 3 seconds 43 | let now = Utc::now(); 44 | 45 | assert_eq!(ema.next((now, 2.0)), 2.0); 46 | assert_eq!(ema.next((now + Duration::seconds(1), 5.0)), 3.5); 47 | assert_eq!(ema.next((now + Duration::seconds(2), 1.0)), 2.25); 48 | assert_eq!(ema.next((now + Duration::seconds(3), 6.25)), 4.25); 49 | ``` 50 | 51 | See more in the examples [here](https://github.com/greyblake/ta-rs/tree/master/examples). 52 | Check also the [documentation](https://docs.rs/ta). 53 | 54 | ## Basic ideas 55 | 56 | Indicators typically implement the following traits: 57 | 58 | - `Next` (often `Next`) - to feed and get the next value 59 | - `Reset` - to reset an indicator 60 | - `Debug` 61 | - `Display` 62 | - `Default` 63 | - `Clone` 64 | 65 | ## List of indicators 66 | 67 | So far there are the following indicators available. 68 | 69 | - Trend 70 | - Exponential Moving Average (EMA) 71 | - Simple Moving Average (SMA) 72 | - Oscillators 73 | - Relative Strength Index (RSI) 74 | - Other 75 | - Minimum 76 | - Maximum 77 | - Standard Deviation (SD) 78 | - Mean Absolute Deviation (MAD) 79 | - Bollinger Bands (BB) 80 | - Rate of Change (ROC) 81 | 82 | ## Contributors 83 | 84 | - [greyblake](https://github.com/greyblake) Potapov Sergey - original creator of ta-rs. 85 | - [austin-starks](https://github.com/austin-starks) Austin Starks – the creator of this repo 86 | -------------------------------------------------------------------------------- /src/data_item.rs: -------------------------------------------------------------------------------- 1 | use crate::errors::*; 2 | 3 | #[cfg(feature = "serde")] 4 | use serde::{Deserialize, Serialize}; 5 | 6 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 7 | #[derive(Debug, Clone, PartialEq)] 8 | pub struct DataItem { 9 | open: f64, 10 | high: f64, 11 | low: f64, 12 | close: f64, 13 | volume: f64, 14 | } 15 | 16 | impl DataItem { 17 | pub fn builder() -> DataItemBuilder { 18 | DataItemBuilder::new() 19 | } 20 | } 21 | 22 | pub struct DataItemBuilder { 23 | open: Option, 24 | high: Option, 25 | low: Option, 26 | close: Option, 27 | volume: Option, 28 | } 29 | 30 | impl DataItemBuilder { 31 | pub fn new() -> Self { 32 | Self { 33 | open: None, 34 | high: None, 35 | low: None, 36 | close: None, 37 | volume: None, 38 | } 39 | } 40 | 41 | pub fn open(mut self, val: f64) -> Self { 42 | self.open = Some(val); 43 | self 44 | } 45 | 46 | pub fn high(mut self, val: f64) -> Self { 47 | self.high = Some(val); 48 | self 49 | } 50 | 51 | pub fn low(mut self, val: f64) -> Self { 52 | self.low = Some(val); 53 | self 54 | } 55 | 56 | pub fn close(mut self, val: f64) -> Self { 57 | self.close = Some(val); 58 | self 59 | } 60 | 61 | pub fn volume(mut self, val: f64) -> Self { 62 | self.volume = Some(val); 63 | self 64 | } 65 | 66 | pub fn build(self) -> Result { 67 | if let (Some(open), Some(high), Some(low), Some(close), Some(volume)) = 68 | (self.open, self.high, self.low, self.close, self.volume) 69 | { 70 | // validate 71 | if low <= open 72 | && low <= close 73 | && low <= high 74 | && high >= open 75 | && high >= close 76 | && volume >= 0.0 77 | { 78 | let item = DataItem { 79 | open, 80 | high, 81 | low, 82 | close, 83 | volume, 84 | }; 85 | Ok(item) 86 | } else { 87 | Err(TaError::DataItemInvalid) 88 | } 89 | } else { 90 | Err(TaError::DataItemIncomplete) 91 | } 92 | } 93 | } 94 | 95 | #[cfg(test)] 96 | mod tests { 97 | use super::*; 98 | 99 | #[test] 100 | fn test_builder() { 101 | fn assert_valid((open, high, low, close, volume): (f64, f64, f64, f64, f64)) { 102 | let result = DataItem::builder() 103 | .open(open) 104 | .high(high) 105 | .low(low) 106 | .close(close) 107 | .volume(volume) 108 | .build(); 109 | assert!(result.is_ok()); 110 | } 111 | 112 | fn assert_invalid(record: (f64, f64, f64, f64, f64)) { 113 | let (open, high, low, close, volume) = record; 114 | let result = DataItem::builder() 115 | .open(open) 116 | .high(high) 117 | .low(low) 118 | .close(close) 119 | .volume(volume) 120 | .build(); 121 | assert_eq!(result, Err(TaError::DataItemInvalid)); 122 | } 123 | 124 | let valid_records = vec![ 125 | // open, high, low , close, volume 126 | (20.0, 25.0, 15.0, 21.0, 7500.0), 127 | (10.0, 10.0, 10.0, 10.0, 10.0), 128 | (0.0, 0.0, 0.0, 0.0, 0.0), 129 | ]; 130 | for record in valid_records { 131 | assert_valid(record) 132 | } 133 | 134 | let invalid_records = vec![ 135 | // open, high, low , close, volume 136 | (-1.0, 25.0, 15.0, 21.0, 7500.0), 137 | (20.0, -1.0, 15.0, 21.0, 7500.0), 138 | (20.0, 25.0, 15.0, -1.0, 7500.0), 139 | (20.0, 25.0, 15.0, 21.0, -1.0), 140 | (14.9, 25.0, 15.0, 21.0, 7500.0), 141 | (25.1, 25.0, 15.0, 21.0, 7500.0), 142 | (20.0, 25.0, 15.0, 14.9, 7500.0), 143 | (20.0, 25.0, 15.0, 25.1, 7500.0), 144 | (20.0, 15.0, 25.0, 21.0, 7500.0), 145 | ]; 146 | for record in invalid_records { 147 | assert_invalid(record) 148 | } 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/indicators/minimum.rs: -------------------------------------------------------------------------------- 1 | use std::collections::VecDeque; 2 | use std::fmt; 3 | use std::time::Duration; // Change: Use std::time::Duration 4 | 5 | use crate::errors::Result; 6 | use crate::indicators::AdaptiveTimeDetector; 7 | use crate::{Next, Reset}; 8 | use chrono::{DateTime, Utc}; 9 | #[cfg(feature = "serde")] 10 | use serde::{Deserialize, Serialize}; 11 | 12 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 13 | #[derive(Debug, Clone)] 14 | pub struct Minimum { 15 | duration: Duration, // Now std::time::Duration 16 | window: VecDeque<(DateTime, f64)>, 17 | min_value: f64, 18 | detector: AdaptiveTimeDetector, 19 | } 20 | 21 | impl Minimum { 22 | pub fn get_window(&self) -> VecDeque<(DateTime, f64)> { 23 | self.window.clone() 24 | } 25 | 26 | pub fn new(duration: Duration) -> Result { 27 | // Change: Check for zero duration (std::time::Duration can't be negative) 28 | if duration.as_secs() == 0 && duration.subsec_nanos() == 0 { 29 | return Err(crate::errors::TaError::InvalidParameter); 30 | } 31 | Ok(Self { 32 | duration, 33 | window: VecDeque::new(), 34 | min_value: f64::INFINITY, 35 | detector: AdaptiveTimeDetector::new(duration), 36 | }) 37 | } 38 | 39 | fn update_min(&mut self) { 40 | self.min_value = self 41 | .window 42 | .iter() 43 | .map(|&(_, val)| val) 44 | .fold(f64::INFINITY, f64::min); 45 | } 46 | 47 | fn remove_old(&mut self, current_time: DateTime) { 48 | // Change: Convert std::time::Duration to chrono::Duration for date arithmetic 49 | let chrono_duration = chrono::Duration::from_std(self.duration).unwrap(); 50 | while self 51 | .window 52 | .front() 53 | .map_or(false, |&(time, _)| time < current_time - chrono_duration) 54 | { 55 | self.window.pop_front(); 56 | } 57 | } 58 | } 59 | 60 | impl Next for Minimum { 61 | type Output = f64; 62 | 63 | fn next(&mut self, (timestamp, value): (DateTime, f64)) -> Self::Output { 64 | // Check if we should replace the last value (same time bucket) 65 | let should_replace = self.detector.should_replace(timestamp); 66 | 67 | if should_replace && !self.window.is_empty() { 68 | // Replace the last value in the same time bucket 69 | self.window.pop_back(); 70 | } else { 71 | // New time period - remove old data first 72 | self.remove_old(timestamp); 73 | } 74 | 75 | self.window.push_back((timestamp, value)); 76 | 77 | if value < self.min_value { 78 | self.min_value = value; 79 | } else { 80 | self.update_min(); 81 | } 82 | 83 | self.min_value 84 | } 85 | } 86 | 87 | impl Reset for Minimum { 88 | fn reset(&mut self) { 89 | self.window.clear(); 90 | self.min_value = f64::INFINITY; 91 | self.detector.reset(); 92 | } 93 | } 94 | 95 | impl Default for Minimum { 96 | fn default() -> Self { 97 | // Change: Use Duration::from_secs for 14 days 98 | Self::new(Duration::from_secs(14 * 24 * 60 * 60)).unwrap() 99 | } 100 | } 101 | 102 | impl fmt::Display for Minimum { 103 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 104 | // Change: Calculate days from seconds 105 | let days = self.duration.as_secs() / 86400; 106 | write!(f, "MIN({} days)", days) 107 | } 108 | } 109 | 110 | #[cfg(test)] 111 | mod tests { 112 | use super::*; 113 | use chrono::{TimeZone, Utc}; 114 | 115 | // Helper function to create a DateTime from a date string for testing 116 | fn datetime(s: &str) -> DateTime { 117 | Utc.datetime_from_str(s, "%Y-%m-%d %H:%M:%S").unwrap() 118 | } 119 | 120 | #[test] 121 | fn test_new() { 122 | // Change: Use std::time::Duration constructors 123 | assert!(Minimum::new(Duration::from_secs(0)).is_err()); 124 | assert!(Minimum::new(Duration::from_secs(86400)).is_ok()); // 1 day 125 | } 126 | 127 | #[test] 128 | fn test_next() { 129 | let duration = Duration::from_secs(2 * 86400); // 2 days 130 | let mut min = Minimum::new(duration).unwrap(); 131 | 132 | assert_eq!(min.next((datetime("2023-01-01 00:00:00"), 4.0)), 4.0); 133 | assert_eq!(min.next((datetime("2023-01-02 00:00:00"), 1.2)), 1.2); 134 | assert_eq!(min.next((datetime("2023-01-03 00:00:00"), 5.0)), 1.2); 135 | assert_eq!(min.next((datetime("2023-01-04 00:00:00"), 3.0)), 1.2); 136 | assert_eq!(min.next((datetime("2023-01-05 00:00:00"), 4.0)), 3.0); 137 | assert_eq!(min.next((datetime("2023-01-06 00:00:00"), 6.0)), 3.0); 138 | assert_eq!(min.next((datetime("2023-01-07 00:00:00"), 7.0)), 4.0); 139 | assert_eq!(min.next((datetime("2023-01-08 00:00:00"), 8.0)), 6.0); 140 | assert_eq!(min.next((datetime("2023-01-09 00:00:00"), -9.0)), -9.0); 141 | assert_eq!(min.next((datetime("2023-01-10 00:00:00"), 0.0)), -9.0); 142 | } 143 | 144 | #[test] 145 | fn test_reset() { 146 | let duration = Duration::from_secs(10 * 86400); // 10 days 147 | let mut min = Minimum::new(duration).unwrap(); 148 | 149 | assert_eq!(min.next((datetime("2023-01-01 00:00:00"), 5.0)), 5.0); 150 | assert_eq!(min.next((datetime("2023-01-02 00:00:00"), 7.0)), 5.0); 151 | 152 | min.reset(); 153 | assert_eq!(min.next((datetime("2023-01-03 00:00:00"), 8.0)), 8.0); 154 | } 155 | 156 | #[test] 157 | fn test_default() { 158 | let _ = Minimum::default(); 159 | } 160 | 161 | #[test] 162 | fn test_display() { 163 | let indicator = Minimum::new(Duration::from_secs(10 * 86400)).unwrap(); // 10 days 164 | assert_eq!(format!("{}", indicator), "MIN(10 days)"); 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /src/indicators/simple_moving_average.rs: -------------------------------------------------------------------------------- 1 | use std::collections::VecDeque; 2 | use std::fmt; 3 | use std::time::Duration; 4 | 5 | use crate::indicators::AdaptiveTimeDetector; 6 | use crate::Next; 7 | use crate::{errors::Result, Reset}; 8 | use chrono::{DateTime, Utc}; 9 | #[cfg(feature = "serde")] 10 | use serde::{Deserialize, Serialize}; 11 | 12 | #[doc(alias = "SMA")] 13 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 14 | #[derive(Debug, Clone)] 15 | pub struct SimpleMovingAverage { 16 | duration: Duration, // Now std::time::Duration 17 | window: VecDeque<(DateTime, f64)>, 18 | sum: f64, 19 | detector: AdaptiveTimeDetector, 20 | } 21 | 22 | impl SimpleMovingAverage { 23 | pub fn get_window(&self) -> VecDeque<(DateTime, f64)> { 24 | self.window.clone() 25 | } 26 | pub fn new(duration: Duration) -> Result { 27 | // std::time::Duration can't be negative, so just check if it's zero 28 | if duration.as_secs() == 0 && duration.subsec_nanos() == 0 { 29 | return Err(crate::errors::TaError::InvalidParameter); 30 | } 31 | Ok(Self { 32 | duration, 33 | window: VecDeque::new(), 34 | sum: 0.0, 35 | detector: AdaptiveTimeDetector::new(duration), 36 | }) 37 | } 38 | 39 | pub fn get_internal_state(&self) -> (Duration, VecDeque<(DateTime, f64)>, f64) { 40 | (self.duration, self.window.clone(), self.sum) 41 | } 42 | 43 | fn remove_old_data(&mut self, current_time: DateTime) { 44 | // Convert std::time::Duration to chrono::Duration for the subtraction 45 | let chrono_duration = chrono::Duration::from_std(self.duration).unwrap(); 46 | while self 47 | .window 48 | .front() 49 | .map_or(false, |(time, _)| *time <= current_time - chrono_duration) 50 | { 51 | if let Some((_, value)) = self.window.pop_front() { 52 | self.sum -= value; 53 | } 54 | } 55 | } 56 | } 57 | 58 | impl Next for SimpleMovingAverage { 59 | type Output = f64; 60 | 61 | fn next(&mut self, (timestamp, value): (DateTime, f64)) -> Self::Output { 62 | // Check if we should replace the last value (same time bucket) 63 | let should_replace = self.detector.should_replace(timestamp); 64 | 65 | // ALWAYS remove old data first, regardless of replace/add 66 | self.remove_old_data(timestamp); 67 | 68 | if should_replace && !self.window.is_empty() { 69 | // Replace the last value in the same time bucket 70 | if let Some((_, old_value)) = self.window.pop_back() { 71 | self.sum -= old_value; 72 | } 73 | } 74 | 75 | // Add new data point 76 | self.window.push_back((timestamp, value)); 77 | self.sum += value; 78 | 79 | // Calculate moving average 80 | if self.window.is_empty() { 81 | 0.0 82 | } else { 83 | self.sum / self.window.len() as f64 84 | } 85 | } 86 | } 87 | 88 | impl Reset for SimpleMovingAverage { 89 | fn reset(&mut self) { 90 | self.window.clear(); 91 | self.sum = 0.0; 92 | self.detector.reset(); 93 | } 94 | } 95 | 96 | impl Default for SimpleMovingAverage { 97 | fn default() -> Self { 98 | // Use std::time::Duration constructor 99 | Self::new(Duration::from_secs(14 * 24 * 60 * 60)).unwrap() // 14 days in seconds 100 | } 101 | } 102 | 103 | impl fmt::Display for SimpleMovingAverage { 104 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 105 | // Use as_secs() instead of Debug format 106 | write!(f, "SMA({}s)", self.duration.as_secs()) 107 | } 108 | } 109 | 110 | #[cfg(test)] 111 | mod tests { 112 | use super::*; 113 | use chrono::{TimeZone, Utc}; 114 | 115 | #[test] 116 | fn test_new() { 117 | assert!(SimpleMovingAverage::new(Duration::from_secs(0)).is_err()); 118 | assert!(SimpleMovingAverage::new(Duration::from_secs(1)).is_ok()); 119 | } 120 | 121 | #[test] 122 | fn test_next() { 123 | let duration = Duration::from_secs(4); 124 | let mut sma = SimpleMovingAverage::new(duration).unwrap(); 125 | let start_time = Utc::now(); 126 | let elapsed_time = chrono::Duration::seconds(1); 127 | assert_eq!(sma.next((start_time, 4.0)), 4.0); 128 | assert_eq!(sma.next((start_time + elapsed_time, 5.0)), 4.5); 129 | assert_eq!(sma.next((start_time + elapsed_time * 2, 6.0)), 5.0); 130 | assert_eq!(sma.next((start_time + elapsed_time * 3, 6.0)), 5.25); 131 | assert_eq!(sma.next((start_time + elapsed_time * 4, 6.0)), 5.75); 132 | assert_eq!(sma.next((start_time + elapsed_time * 5, 6.0)), 6.0); 133 | assert_eq!(sma.next((start_time + elapsed_time * 6, 2.0)), 5.0); 134 | // test explicit out of bounds 135 | let chrono_duration = chrono::Duration::from_std(duration).unwrap(); 136 | assert_eq!( 137 | sma.next((start_time + elapsed_time * 6 + chrono_duration, 2.0)), 138 | 2.0 139 | ); 140 | } 141 | 142 | #[test] 143 | fn test_reset() { 144 | let duration = Duration::from_secs(4); 145 | let mut sma = SimpleMovingAverage::new(duration).unwrap(); 146 | let start_time = Utc::now(); 147 | let elapsed_time = chrono::Duration::seconds(1); 148 | assert_eq!(sma.next((start_time, 4.0)), 4.0); 149 | assert_eq!(sma.next((start_time + elapsed_time, 5.0)), 4.5); 150 | assert_eq!(sma.next((start_time + elapsed_time * 2, 6.0)), 5.0); 151 | 152 | sma.reset(); 153 | assert_eq!(sma.next((start_time + elapsed_time * 3, 99.0)), 99.0); 154 | } 155 | 156 | #[test] 157 | fn test_default() { 158 | let _sma = SimpleMovingAverage::default(); 159 | } 160 | 161 | #[test] 162 | fn test_display() { 163 | let indicator = SimpleMovingAverage::new(Duration::from_secs(7)).unwrap(); 164 | assert_eq!(format!("{}", indicator), "SMA(7s)"); 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /src/indicators/rate_of_change.rs: -------------------------------------------------------------------------------- 1 | use std::collections::VecDeque; 2 | use std::fmt; 3 | use std::time::Duration; 4 | 5 | use crate::errors::{Result, TaError}; 6 | use crate::indicators::AdaptiveTimeDetector; 7 | use crate::traits::{Next, Reset}; 8 | use chrono::{DateTime, Utc}; 9 | #[cfg(feature = "serde")] 10 | use serde::{Deserialize, Serialize}; 11 | 12 | #[doc(alias = "ROC")] 13 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 14 | #[derive(Debug, Clone)] 15 | pub struct RateOfChange { 16 | duration: Duration, // Now std::time::Duration 17 | window: VecDeque<(DateTime, f64)>, 18 | detector: AdaptiveTimeDetector, 19 | } 20 | 21 | impl RateOfChange { 22 | pub fn get_window(&self) -> VecDeque<(DateTime, f64)> { 23 | self.window.clone() 24 | } 25 | pub fn new(duration: Duration) -> Result { 26 | // std::time::Duration can't be negative, so just check if it's zero 27 | if duration.as_secs() == 0 && duration.subsec_nanos() == 0 { 28 | Err(TaError::InvalidParameter) 29 | } else { 30 | Ok(Self { 31 | duration, 32 | window: VecDeque::new(), 33 | detector: AdaptiveTimeDetector::new(duration), 34 | }) 35 | } 36 | } 37 | 38 | // Add a method to remove old data points outside the duration 39 | fn remove_old_data(&mut self, current_time: DateTime) { 40 | // Convert std::time::Duration to chrono::Duration for the subtraction 41 | let chrono_duration = chrono::Duration::from_std(self.duration).unwrap(); 42 | while self 43 | .window 44 | .front() 45 | .map_or(false, |(time, _)| *time < current_time - chrono_duration) 46 | { 47 | self.window.pop_front(); 48 | } 49 | } 50 | } 51 | 52 | impl Next for RateOfChange { 53 | type Output = f64; 54 | 55 | fn next(&mut self, (timestamp, value): (DateTime, f64)) -> Self::Output { 56 | // Check if we should replace the last value (same time bucket) 57 | let should_replace = self.detector.should_replace(timestamp); 58 | 59 | // ALWAYS remove old data first, regardless of replace/add 60 | self.remove_old_data(timestamp); 61 | 62 | if should_replace && !self.window.is_empty() { 63 | // Replace the last value in the same time bucket 64 | self.window.pop_back(); 65 | } 66 | 67 | // Add the new data point 68 | self.window.push_back((timestamp, value)); 69 | 70 | // Calculate the rate of change if we have at least two data points 71 | if self.window.len() > 1 { 72 | let (oldest_time, oldest_value) = 73 | self.window.front().expect("Window has at least one item"); 74 | let (newest_time, newest_value) = 75 | self.window.back().expect("Window has at least one item"); 76 | 77 | // Ensure we do not divide by zero 78 | if oldest_value.clone() != 0.0 { 79 | (newest_value - oldest_value) / oldest_value * 100.0 80 | } else { 81 | 0.0 82 | } 83 | } else { 84 | 0.0 85 | } 86 | } 87 | } 88 | 89 | impl Default for RateOfChange { 90 | fn default() -> Self { 91 | // Use std::time::Duration constructor 92 | Self::new(Duration::from_secs(14 * 24 * 60 * 60)).unwrap() // 14 days in seconds 93 | } 94 | } 95 | 96 | impl fmt::Display for RateOfChange { 97 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 98 | // Use as_secs() instead of Debug format 99 | write!(f, "ROC({}s)", self.duration.as_secs()) 100 | } 101 | } 102 | 103 | impl Reset for RateOfChange { 104 | fn reset(&mut self) { 105 | self.window.clear(); 106 | self.detector.reset(); 107 | } 108 | } 109 | #[cfg(test)] 110 | mod tests { 111 | use super::*; 112 | use crate::test_helper::*; 113 | use chrono::{TimeZone, Utc}; 114 | 115 | test_indicator!(RateOfChange); 116 | const EPSILON: f64 = 1e-10; 117 | 118 | #[test] 119 | fn test_new() { 120 | assert!(RateOfChange::new(Duration::from_secs(0)).is_err()); 121 | assert!(RateOfChange::new(Duration::from_secs(1)).is_ok()); 122 | assert!(RateOfChange::new(Duration::from_secs(100_000)).is_ok()); 123 | } 124 | 125 | #[test] 126 | fn test_next_f64() { 127 | let mut roc = RateOfChange::new(Duration::from_secs(3)).unwrap(); 128 | let start_time = Utc.ymd(2020, 1, 1).and_hms(0, 0, 0); 129 | 130 | assert_eq!(round(roc.next((start_time, 10.0))), 0.0); 131 | assert_eq!( 132 | round(roc.next((start_time + chrono::Duration::seconds(1), 10.4))), 133 | 4.0 134 | ); 135 | assert_eq!( 136 | round(roc.next((start_time + chrono::Duration::seconds(2), 10.57))), 137 | 5.7 138 | ); 139 | assert_eq!( 140 | round(roc.next((start_time + chrono::Duration::seconds(3), 10.8))), 141 | 8.0 142 | ); 143 | assert_eq!( 144 | round(roc.next((start_time + chrono::Duration::seconds(4), 10.9))), 145 | 4.808 146 | ); 147 | assert_eq!( 148 | round(roc.next((start_time + chrono::Duration::seconds(5), 10.0))), 149 | -5.393 150 | ); 151 | } 152 | 153 | #[test] 154 | fn test_reset() { 155 | let mut roc = RateOfChange::new(Duration::from_secs(3)).unwrap(); 156 | let start_time = Utc.ymd(2020, 1, 1).and_hms(0, 0, 0); 157 | 158 | roc.next((start_time, 12.3)); 159 | roc.next((start_time + chrono::Duration::seconds(1), 15.0)); 160 | 161 | roc.reset(); 162 | 163 | assert_eq!(round(roc.next((start_time, 10.0))), 0.0); 164 | assert_eq!( 165 | round(roc.next((start_time + chrono::Duration::seconds(1), 10.4))), 166 | 4.0 167 | ); 168 | assert_eq!( 169 | round(roc.next((start_time + chrono::Duration::seconds(2), 10.57))), 170 | 5.7 171 | ); 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /src/indicators/mean_absolute_deviation.rs: -------------------------------------------------------------------------------- 1 | use chrono::{DateTime, Utc}; 2 | use std::collections::VecDeque; 3 | use std::fmt; 4 | use std::time::Duration; 5 | 6 | #[cfg(feature = "serde")] 7 | use serde::{Deserialize, Serialize}; 8 | 9 | use crate::errors::{Result, TaError}; 10 | use crate::indicators::AdaptiveTimeDetector; 11 | use crate::{Next, Reset}; 12 | 13 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 14 | #[derive(Debug, Clone)] 15 | pub struct MeanAbsoluteDeviation { 16 | duration: Duration, // Now std::time::Duration 17 | sum: f64, 18 | window: VecDeque<(DateTime, f64)>, 19 | detector: AdaptiveTimeDetector, 20 | } 21 | 22 | impl MeanAbsoluteDeviation { 23 | pub fn get_window(&self) -> VecDeque<(DateTime, f64)> { 24 | self.window.clone() 25 | } 26 | pub fn new(duration: Duration) -> Result { 27 | // std::time::Duration can't be negative, so just check if it's zero 28 | if duration.as_secs() == 0 && duration.subsec_nanos() == 0 { 29 | Err(TaError::InvalidParameter) 30 | } else { 31 | Ok(Self { 32 | duration, 33 | sum: 0.0, 34 | window: VecDeque::new(), 35 | detector: AdaptiveTimeDetector::new(duration), 36 | }) 37 | } 38 | } 39 | 40 | fn remove_old_data(&mut self, current_time: DateTime) { 41 | // Convert std::time::Duration to chrono::Duration for the subtraction 42 | let chrono_duration = chrono::Duration::from_std(self.duration).unwrap(); 43 | while self 44 | .window 45 | .front() 46 | .map_or(false, |(time, _)| *time <= current_time - chrono_duration) 47 | { 48 | if let Some((_, value)) = self.window.pop_front() { 49 | self.sum -= value; 50 | } 51 | } 52 | } 53 | } 54 | 55 | impl Next for MeanAbsoluteDeviation { 56 | type Output = f64; 57 | 58 | fn next(&mut self, (timestamp, value): (DateTime, f64)) -> Self::Output { 59 | // Check if we should replace the last value (same time bucket) 60 | let should_replace = self.detector.should_replace(timestamp); 61 | 62 | // ALWAYS remove old data first, regardless of replace/add 63 | self.remove_old_data(timestamp); 64 | 65 | if should_replace && !self.window.is_empty() { 66 | // Replace the last value in the same time bucket 67 | if let Some((_, old_value)) = self.window.pop_back() { 68 | self.sum -= old_value; 69 | } 70 | } 71 | 72 | self.window.push_back((timestamp, value)); 73 | self.sum += value; 74 | 75 | let mean = self.sum / self.window.len() as f64; 76 | 77 | let mut mad = 0.0; 78 | for &(_, val) in &self.window { 79 | mad += (val - mean).abs(); 80 | } 81 | 82 | if self.window.is_empty() { 83 | 0.0 84 | } else { 85 | mad / self.window.len() as f64 86 | } 87 | } 88 | } 89 | 90 | impl Reset for MeanAbsoluteDeviation { 91 | fn reset(&mut self) { 92 | self.sum = 0.0; 93 | self.window.clear(); 94 | self.detector.reset(); 95 | } 96 | } 97 | 98 | impl Default for MeanAbsoluteDeviation { 99 | fn default() -> Self { 100 | // Use std::time::Duration constructor 101 | Self::new(Duration::from_secs(14 * 24 * 60 * 60)).unwrap() // 14 days in seconds 102 | } 103 | } 104 | 105 | impl fmt::Display for MeanAbsoluteDeviation { 106 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 107 | // Use as_secs() instead of Debug format 108 | write!(f, "MAD({}s)", self.duration.as_secs()) 109 | } 110 | } 111 | 112 | #[cfg(test)] 113 | mod tests { 114 | use super::*; 115 | use crate::test_helper::*; 116 | use chrono::{TimeZone, Utc}; 117 | 118 | // Helper function to create a Utc DateTime from a timestamp 119 | fn to_utc_datetime(timestamp: i64) -> DateTime { 120 | Utc.timestamp(timestamp, 0) 121 | } 122 | 123 | #[test] 124 | fn test_new() { 125 | assert!(MeanAbsoluteDeviation::new(Duration::from_secs(0)).is_err()); 126 | assert!(MeanAbsoluteDeviation::new(Duration::from_secs(1)).is_ok()); 127 | } 128 | 129 | #[test] 130 | fn test_next() { 131 | let duration = Duration::from_secs(5); 132 | let mut mad = MeanAbsoluteDeviation::new(duration).unwrap(); 133 | 134 | let timestamp1 = to_utc_datetime(0); 135 | let timestamp2 = to_utc_datetime(1); 136 | let timestamp3 = to_utc_datetime(2); 137 | let timestamp4 = to_utc_datetime(3); 138 | let timestamp5 = to_utc_datetime(4); 139 | let timestamp6 = to_utc_datetime(5); 140 | 141 | assert_eq!(round(mad.next((timestamp1, 1.5))), 0.0); 142 | assert_eq!(round(mad.next((timestamp2, 4.0))), 1.25); 143 | assert_eq!(round(mad.next((timestamp3, 8.0))), 2.333); 144 | assert_eq!(round(mad.next((timestamp4, 4.0))), 1.813); 145 | assert_eq!(round(mad.next((timestamp5, 4.0))), 1.48); 146 | assert_eq!(round(mad.next((timestamp6, 1.5))), 1.48); 147 | } 148 | 149 | #[test] 150 | fn test_reset() { 151 | let duration = Duration::from_secs(5); 152 | let mut mad = MeanAbsoluteDeviation::new(duration).unwrap(); 153 | 154 | let timestamp1 = to_utc_datetime(0); 155 | let timestamp2 = to_utc_datetime(1); 156 | 157 | assert_eq!(round(mad.next((timestamp1, 1.5))), 0.0); 158 | assert_eq!(round(mad.next((timestamp2, 4.0))), 1.25); 159 | 160 | mad.reset(); 161 | 162 | assert_eq!(round(mad.next((timestamp1, 1.5))), 0.0); 163 | assert_eq!(round(mad.next((timestamp2, 4.0))), 1.25); 164 | } 165 | 166 | #[test] 167 | fn test_default() { 168 | MeanAbsoluteDeviation::default(); 169 | } 170 | 171 | #[test] 172 | fn test_display() { 173 | let indicator = MeanAbsoluteDeviation::new(Duration::from_secs(10)).unwrap(); 174 | assert_eq!(format!("{}", indicator), "MAD(10s)"); 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /src/indicators/maximum.rs: -------------------------------------------------------------------------------- 1 | use std::collections::VecDeque; 2 | use std::fmt; 3 | use std::time::Duration; // Change: Use std::time::Duration 4 | 5 | use crate::errors::{Result, TaError}; 6 | use crate::indicators::AdaptiveTimeDetector; 7 | use crate::{Next, Reset}; 8 | use chrono::{DateTime, Utc}; 9 | #[cfg(feature = "serde")] 10 | use serde::{Deserialize, Serialize}; 11 | 12 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 13 | #[derive(Debug, Clone)] 14 | pub struct Maximum { 15 | duration: Duration, // Now std::time::Duration 16 | window: VecDeque<(DateTime, f64)>, 17 | detector: AdaptiveTimeDetector, 18 | } 19 | 20 | impl Maximum { 21 | pub fn get_window(&self) -> VecDeque<(DateTime, f64)> { 22 | self.window.clone() 23 | } 24 | 25 | pub fn new(duration: Duration) -> Result { 26 | // Change: Check for zero duration (std::time::Duration can't be negative) 27 | if duration.as_secs() == 0 && duration.subsec_nanos() == 0 { 28 | Err(TaError::InvalidParameter) 29 | } else { 30 | Ok(Self { 31 | duration, 32 | window: VecDeque::new(), 33 | detector: AdaptiveTimeDetector::new(duration), 34 | }) 35 | } 36 | } 37 | 38 | fn find_max_value(&self) -> f64 { 39 | self.window 40 | .iter() 41 | .map(|&(_, val)| val) 42 | .fold(f64::NEG_INFINITY, f64::max) 43 | } 44 | 45 | fn remove_old_data(&mut self, current_time: DateTime) { 46 | // Change: Convert std::time::Duration to chrono::Duration for date arithmetic 47 | let chrono_duration = chrono::Duration::from_std(self.duration).unwrap(); 48 | while self 49 | .window 50 | .front() 51 | .map_or(false, |(time, _)| *time <= current_time - chrono_duration) 52 | { 53 | self.window.pop_front(); 54 | } 55 | } 56 | } 57 | 58 | impl Default for Maximum { 59 | fn default() -> Self { 60 | // Change: Use Duration::from_secs for 14 days 61 | Self::new(Duration::from_secs(14 * 24 * 60 * 60)).unwrap() 62 | } 63 | } 64 | 65 | impl Next for Maximum { 66 | type Output = f64; 67 | 68 | fn next(&mut self, (timestamp, value): (DateTime, f64)) -> Self::Output { 69 | // Check if we should replace the last value (same time bucket) 70 | let should_replace = self.detector.should_replace(timestamp); 71 | 72 | // ALWAYS remove old data first, regardless of replace/add 73 | self.remove_old_data(timestamp); 74 | 75 | if should_replace && !self.window.is_empty() { 76 | // Replace the last value in the same time bucket 77 | self.window.pop_back(); 78 | } 79 | 80 | // Add the new data point 81 | self.window.push_back((timestamp, value)); 82 | 83 | // Find the maximum value in the current window 84 | self.find_max_value() 85 | } 86 | } 87 | 88 | impl Reset for Maximum { 89 | fn reset(&mut self) { 90 | self.window.clear(); 91 | self.detector.reset(); 92 | } 93 | } 94 | 95 | impl fmt::Display for Maximum { 96 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 97 | // Change: Use as_secs() instead of num_seconds() 98 | write!(f, "MAX({}s)", self.duration.as_secs()) 99 | } 100 | } 101 | 102 | #[cfg(test)] 103 | mod tests { 104 | use super::*; 105 | use chrono::TimeZone; 106 | 107 | #[test] 108 | fn test_new() { 109 | // Change: Use std::time::Duration constructors 110 | assert!(Maximum::new(Duration::from_secs(0)).is_err()); 111 | assert!(Maximum::new(Duration::from_secs(1)).is_ok()); 112 | } 113 | 114 | #[test] 115 | fn test_next() { 116 | let duration = Duration::from_secs(2); 117 | let mut max = Maximum::new(duration).unwrap(); 118 | let start_time = Utc.ymd(2020, 1, 1).and_hms(0, 0, 0); 119 | 120 | // Use chrono::Duration for date arithmetic 121 | assert_eq!(max.next((start_time, 4.0)), 4.0); 122 | assert_eq!( 123 | max.next((start_time + chrono::Duration::seconds(1), 1.2)), 124 | 4.0 125 | ); 126 | assert_eq!( 127 | max.next((start_time + chrono::Duration::seconds(2), 5.0)), 128 | 5.0 129 | ); 130 | assert_eq!( 131 | max.next((start_time + chrono::Duration::seconds(3), 3.0)), 132 | 5.0 133 | ); 134 | assert_eq!( 135 | max.next((start_time + chrono::Duration::seconds(4), 4.0)), 136 | 4.0 137 | ); 138 | assert_eq!( 139 | max.next((start_time + chrono::Duration::seconds(5), 0.0)), 140 | 4.0 141 | ); 142 | assert_eq!( 143 | max.next((start_time + chrono::Duration::seconds(6), -1.0)), 144 | 0.0 145 | ); 146 | assert_eq!( 147 | max.next((start_time + chrono::Duration::seconds(7), -2.0)), 148 | -1.0 149 | ); 150 | assert_eq!( 151 | max.next((start_time + chrono::Duration::seconds(8), -1.5)), 152 | -1.5 153 | ); 154 | } 155 | 156 | #[test] 157 | fn test_reset() { 158 | let duration = Duration::from_secs(100); 159 | let mut max = Maximum::new(duration).unwrap(); 160 | let start_time = Utc.ymd(2020, 1, 1).and_hms(0, 0, 0); 161 | 162 | assert_eq!(max.next((start_time, 4.0)), 4.0); 163 | assert_eq!( 164 | max.next((start_time + chrono::Duration::seconds(50), 10.0)), 165 | 10.0 166 | ); 167 | assert_eq!( 168 | max.next((start_time + chrono::Duration::seconds(100), 4.0)), 169 | 10.0 170 | ); 171 | 172 | max.reset(); 173 | assert_eq!( 174 | max.next((start_time + chrono::Duration::seconds(150), 4.0)), 175 | 4.0 176 | ); 177 | } 178 | 179 | #[test] 180 | fn test_default() { 181 | let _ = Maximum::default(); 182 | } 183 | 184 | #[test] 185 | fn test_display() { 186 | let indicator = Maximum::new(Duration::from_secs(7)).unwrap(); 187 | assert_eq!(format!("{}", indicator), "MAX(7s)"); 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /src/indicators/bollinger_bands.rs: -------------------------------------------------------------------------------- 1 | use chrono::{DateTime, Utc}; 2 | use std::collections::VecDeque; 3 | use std::fmt; 4 | use std::time::Duration; // Change: Use std::time::Duration 5 | 6 | use crate::errors::Result; 7 | use crate::indicators::{AdaptiveTimeDetector, StandardDeviation as Sd}; 8 | use crate::{Next, Reset}; 9 | #[cfg(feature = "serde")] 10 | use serde::{Deserialize, Serialize}; 11 | 12 | #[doc(alias = "BB")] 13 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 14 | #[derive(Debug, Clone)] 15 | pub struct BollingerBands { 16 | duration: Duration, // Now std::time::Duration 17 | multiplier: f64, 18 | sd: Sd, 19 | window: VecDeque<(DateTime, f64)>, 20 | detector: AdaptiveTimeDetector, 21 | } 22 | 23 | #[derive(Debug, Clone, PartialEq)] 24 | pub struct BollingerBandsOutput { 25 | pub average: f64, 26 | pub upper: f64, 27 | pub lower: f64, 28 | } 29 | 30 | impl BollingerBands { 31 | pub fn get_window(&self) -> VecDeque<(DateTime, f64)> { 32 | self.window.clone() 33 | } 34 | 35 | pub fn new(duration: Duration, multiplier: f64) -> Result { 36 | // Change: Check for zero duration (std::time::Duration can't be negative) 37 | if duration.as_secs() == 0 && duration.subsec_nanos() == 0 { 38 | return Err(crate::errors::TaError::InvalidParameter); 39 | } 40 | Ok(Self { 41 | duration, 42 | multiplier, 43 | sd: Sd::new(duration)?, // Pass std::time::Duration 44 | window: VecDeque::new(), 45 | detector: AdaptiveTimeDetector::new(duration), 46 | }) 47 | } 48 | 49 | pub fn multiplier(&self) -> f64 { 50 | self.multiplier 51 | } 52 | 53 | fn remove_old_data(&mut self, current_time: DateTime) { 54 | // Change: Convert std::time::Duration to chrono::Duration for date arithmetic 55 | let chrono_duration = chrono::Duration::from_std(self.duration).unwrap(); 56 | while self 57 | .window 58 | .front() 59 | .map_or(false, |(time, _)| *time <= current_time - chrono_duration) 60 | { 61 | self.window.pop_front(); 62 | } 63 | } 64 | } 65 | 66 | impl Next for BollingerBands { 67 | type Output = f64; 68 | 69 | fn next(&mut self, (timestamp, value): (DateTime, f64)) -> Self::Output { 70 | // Check if we should replace the last value (same time bucket) 71 | let should_replace = self.detector.should_replace(timestamp); 72 | 73 | // ALWAYS remove old data first, regardless of replace/add 74 | self.remove_old_data(timestamp); 75 | 76 | if should_replace && !self.window.is_empty() { 77 | // Replace the last value in the same time bucket 78 | self.window.pop_back(); 79 | } 80 | 81 | // Add the new data point 82 | self.window.push_back((timestamp, value)); 83 | 84 | // Calculate the mean and standard deviation based on the current window 85 | let values: Vec = self.window.iter().map(|&(_, val)| val).collect(); 86 | let mean = values.iter().sum::() / values.len() as f64; 87 | let sd = self.sd.next((timestamp, value)); 88 | mean + sd * self.multiplier 89 | } 90 | } 91 | 92 | impl Reset for BollingerBands { 93 | fn reset(&mut self) { 94 | self.sd.reset(); 95 | self.window.clear(); 96 | self.detector.reset(); 97 | } 98 | } 99 | 100 | impl Default for BollingerBands { 101 | fn default() -> Self { 102 | // Change: Use Duration::from_secs for 14 days 103 | Self::new(Duration::from_secs(14 * 24 * 60 * 60), 2_f64).unwrap() 104 | } 105 | } 106 | 107 | impl fmt::Display for BollingerBands { 108 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 109 | // Change: Display duration in seconds 110 | write!(f, "BB({}s, {})", self.duration.as_secs(), self.multiplier) 111 | } 112 | } 113 | 114 | #[cfg(test)] 115 | mod tests { 116 | use super::*; 117 | use crate::test_helper::*; 118 | use chrono::Utc; 119 | 120 | test_indicator!(BollingerBands); 121 | 122 | #[test] 123 | fn test_new() { 124 | // Change: Use std::time::Duration constructors 125 | assert!(BollingerBands::new(Duration::from_secs(0), 2_f64).is_err()); 126 | assert!(BollingerBands::new(Duration::from_secs(86400), 2_f64).is_ok()); // 1 day 127 | assert!(BollingerBands::new(Duration::from_secs(172800), 2_f64).is_ok()); 128 | // 2 days 129 | } 130 | 131 | #[test] 132 | fn test_next() { 133 | let mut bb = BollingerBands::new(Duration::from_secs(3 * 86400), 2.0).unwrap(); // 3 days 134 | let now = Utc::now(); 135 | 136 | // Use chrono::Duration for date arithmetic 137 | let a = bb.next((now, 2.0)); 138 | let b = bb.next((now + chrono::Duration::days(1), 5.0)); 139 | let c = bb.next((now + chrono::Duration::days(2), 1.0)); 140 | let d = bb.next((now + chrono::Duration::days(3), 6.25)); 141 | 142 | assert_eq!(round(a), 2.0); 143 | assert_eq!(round(b), 6.5); 144 | assert_eq!(round(c), 6.066); 145 | assert_eq!(round(d), 8.562); 146 | } 147 | 148 | #[test] 149 | fn test_reset() { 150 | let mut bb = BollingerBands::new(Duration::from_secs(5 * 86400), 2.0_f64).unwrap(); // 5 days 151 | let now = Utc::now(); 152 | 153 | let out = bb.next((now, 3.0)); 154 | 155 | assert_eq!(out, 3.0); 156 | 157 | bb.next((now + chrono::Duration::days(1), 2.5)); 158 | bb.next((now + chrono::Duration::days(2), 3.5)); 159 | bb.next((now + chrono::Duration::days(3), 4.0)); 160 | 161 | let out = bb.next((now + chrono::Duration::days(4), 2.0)); 162 | 163 | assert_eq!(round(out), 4.414); 164 | 165 | bb.reset(); 166 | let out = bb.next((now, 3.0)); 167 | assert_eq!(out, 3.0); 168 | } 169 | 170 | #[test] 171 | fn test_default() { 172 | BollingerBands::default(); 173 | } 174 | 175 | #[test] 176 | fn test_display() { 177 | let duration = Duration::from_secs(10 * 86400); // 10 days 178 | let bb = BollingerBands::new(duration, 3.0_f64).unwrap(); 179 | assert_eq!(format!("{}", bb), format!("BB({}s, 3)", 10 * 86400)); 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /src/indicators/max_drawdown.rs: -------------------------------------------------------------------------------- 1 | use crate::errors::{Result, TaError}; 2 | use crate::indicators::AdaptiveTimeDetector; 3 | use crate::{Next, Reset}; 4 | use chrono::{DateTime, Utc}; // Remove Duration from here 5 | #[cfg(feature = "serde")] 6 | use serde::{Deserialize, Serialize}; 7 | use std::collections::VecDeque; 8 | use std::fmt; 9 | use std::time::Duration; // Change: use std::time::Duration 10 | 11 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 12 | #[derive(Debug, Clone)] 13 | pub struct MaxDrawdown { 14 | duration: Duration, // Now std::time::Duration 15 | window: VecDeque<(DateTime, f64)>, 16 | detector: AdaptiveTimeDetector, 17 | } 18 | 19 | impl MaxDrawdown { 20 | pub fn get_window(&self) -> VecDeque<(DateTime, f64)> { 21 | self.window.clone() 22 | } 23 | 24 | pub fn new(duration: Duration) -> Result { 25 | // Change: std::time::Duration can't be negative, so just check if it's zero 26 | if duration.as_secs() == 0 && duration.subsec_nanos() == 0 { 27 | Err(TaError::InvalidParameter) 28 | } else { 29 | Ok(Self { 30 | duration, 31 | window: VecDeque::new(), 32 | detector: AdaptiveTimeDetector::new(duration), 33 | }) 34 | } 35 | } 36 | 37 | fn calculate_max_drawdown(&self) -> f64 { 38 | // No changes needed here 39 | let mut peak = f64::MIN; 40 | let mut max_drawdown = 0.0; 41 | for &(_, value) in &self.window { 42 | if value > peak { 43 | peak = value; 44 | } 45 | let drawdown = (peak - value) / peak; 46 | if drawdown > max_drawdown { 47 | max_drawdown = drawdown; 48 | } 49 | } 50 | 100.0 * max_drawdown 51 | } 52 | 53 | fn remove_old_data(&mut self, current_time: DateTime) { 54 | // Change: Convert std::time::Duration to chrono::Duration for the subtraction 55 | let chrono_duration = chrono::Duration::from_std(self.duration).unwrap(); 56 | while self 57 | .window 58 | .front() 59 | .map_or(false, |(time, _)| *time < current_time - chrono_duration) 60 | { 61 | self.window.pop_front(); 62 | } 63 | } 64 | } 65 | 66 | impl Next for MaxDrawdown { 67 | type Output = f64; 68 | 69 | fn next(&mut self, (timestamp, value): (DateTime, f64)) -> Self::Output { 70 | // Check if we should replace the last value (same time bucket) 71 | let should_replace = self.detector.should_replace(timestamp); 72 | 73 | // ALWAYS remove old data first, regardless of replace/add 74 | self.remove_old_data(timestamp); 75 | 76 | if should_replace && !self.window.is_empty() { 77 | self.window.pop_back(); 78 | } 79 | self.window.push_back((timestamp, value)); 80 | self.calculate_max_drawdown() 81 | } 82 | } 83 | 84 | impl Reset for MaxDrawdown { 85 | fn reset(&mut self) { 86 | // No changes needed 87 | self.window.clear(); 88 | self.detector.reset(); 89 | } 90 | } 91 | 92 | impl fmt::Display for MaxDrawdown { 93 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 94 | // Change: Use as_secs() instead of num_seconds() 95 | write!(f, "MaxDrawdown({}s)", self.duration.as_secs()) 96 | } 97 | } 98 | 99 | impl Default for MaxDrawdown { 100 | fn default() -> Self { 101 | // Change: Use std::time::Duration constructor 102 | Self::new(Duration::from_secs(14 * 24 * 60 * 60)).unwrap() // 14 days in seconds 103 | } 104 | } 105 | 106 | #[cfg(test)] 107 | mod tests { 108 | use super::*; 109 | use chrono::TimeZone; 110 | 111 | #[test] 112 | fn test_new() { 113 | assert!(MaxDrawdown::new(Duration::from_secs(0)).is_err()); 114 | assert!(MaxDrawdown::new(Duration::from_secs(1)).is_ok()); 115 | } 116 | 117 | #[test] 118 | fn test_next() { 119 | let duration = Duration::from_secs(2); 120 | let mut max = MaxDrawdown::new(duration).unwrap(); 121 | let start_time = Utc.ymd(2020, 1, 1).and_hms(0, 0, 0); 122 | 123 | // Change: Use chrono::Duration for adding to DateTime 124 | assert_eq!(max.next((start_time, 4.0)), 0.0); 125 | assert_eq!( 126 | max.next((start_time + chrono::Duration::seconds(1), 2.0)), 127 | 50.0 128 | ); 129 | assert_eq!( 130 | max.next((start_time + chrono::Duration::seconds(2), 1.0)), 131 | 75.0 132 | ); 133 | assert_eq!( 134 | max.next((start_time + chrono::Duration::seconds(3), 3.0)), 135 | 50.0 136 | ); 137 | assert_eq!( 138 | max.next((start_time + chrono::Duration::seconds(4), 4.0)), 139 | 0.0 140 | ); 141 | assert_eq!( 142 | max.next((start_time + chrono::Duration::seconds(5), 0.0)), 143 | 100.0 144 | ); 145 | assert_eq!( 146 | max.next((start_time + chrono::Duration::seconds(6), 2.0)), 147 | 100.0 148 | ); 149 | assert_eq!( 150 | max.next((start_time + chrono::Duration::seconds(7), 3.0)), 151 | 0.0 152 | ); 153 | assert_eq!( 154 | max.next((start_time + chrono::Duration::seconds(8), 1.5)), 155 | 50.0 156 | ); 157 | } 158 | 159 | #[test] 160 | fn test_reset() { 161 | let duration = Duration::from_secs(100); 162 | let mut max = MaxDrawdown::new(duration).unwrap(); 163 | let start_time = Utc.ymd(2020, 1, 1).and_hms(0, 0, 0); 164 | 165 | // Change: Use chrono::Duration for adding to DateTime 166 | assert_eq!(max.next((start_time, 4.0)), 0.0); 167 | assert_eq!( 168 | max.next((start_time + chrono::Duration::seconds(50), 10.0)), 169 | 0.0 170 | ); 171 | assert_eq!( 172 | max.next((start_time + chrono::Duration::seconds(100), 2.0)), 173 | 80.0 174 | ); 175 | max.reset(); 176 | assert_eq!( 177 | max.next((start_time + chrono::Duration::seconds(150), 4.0)), 178 | 0.0 179 | ); 180 | } 181 | 182 | #[test] 183 | fn test_display() { 184 | let indicator = MaxDrawdown::new(Duration::from_secs(7)).unwrap(); 185 | assert_eq!(format!("{}", indicator), "MaxDrawdown(7s)"); 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /src/indicators/standard_deviation.rs: -------------------------------------------------------------------------------- 1 | use std::collections::VecDeque; 2 | use std::fmt; 3 | use std::time::Duration; 4 | 5 | use crate::errors::Result; 6 | use crate::indicators::AdaptiveTimeDetector; 7 | use crate::{Next, Reset}; 8 | use chrono::{DateTime, Utc}; 9 | #[cfg(feature = "serde")] 10 | use serde::{Deserialize, Serialize}; 11 | 12 | #[doc(alias = "SD")] 13 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 14 | #[derive(Debug, Clone)] 15 | pub struct StandardDeviation { 16 | duration: Duration, // Now std::time::Duration 17 | window: VecDeque<(DateTime, f64)>, 18 | sum: f64, 19 | sum_sq: f64, 20 | detector: AdaptiveTimeDetector, 21 | } 22 | 23 | impl StandardDeviation { 24 | pub fn get_window(&self) -> VecDeque<(DateTime, f64)> { 25 | self.window.clone() 26 | } 27 | pub fn new(duration: Duration) -> Result { 28 | // std::time::Duration can't be negative, so just check if it's zero 29 | if duration.as_secs() == 0 && duration.subsec_nanos() == 0 { 30 | return Err(crate::errors::TaError::InvalidParameter); 31 | } 32 | Ok(Self { 33 | duration, 34 | window: VecDeque::new(), 35 | sum: 0.0, 36 | sum_sq: 0.0, 37 | detector: AdaptiveTimeDetector::new(duration), 38 | }) 39 | } 40 | 41 | // Helper method to remove old data points 42 | fn remove_old_data(&mut self, current_time: DateTime) { 43 | // Convert std::time::Duration to chrono::Duration for the subtraction 44 | let chrono_duration = chrono::Duration::from_std(self.duration).unwrap(); 45 | while self 46 | .window 47 | .front() 48 | .map_or(false, |(time, _)| *time <= current_time - chrono_duration) 49 | { 50 | if let Some((_, old_value)) = self.window.pop_front() { 51 | self.sum -= old_value; 52 | self.sum_sq -= old_value * old_value; 53 | } 54 | } 55 | } 56 | 57 | // Calculate the mean based on the current window 58 | pub(super) fn mean(&self) -> f64 { 59 | if !self.window.is_empty() { 60 | self.sum / self.window.len() as f64 61 | } else { 62 | 0.0 63 | } 64 | } 65 | } 66 | 67 | impl Next for StandardDeviation { 68 | type Output = f64; 69 | fn next(&mut self, input: (DateTime, f64)) -> Self::Output { 70 | let (timestamp, value) = input; 71 | 72 | // Check if we should replace the last value (same time bucket) 73 | let should_replace = self.detector.should_replace(timestamp); 74 | 75 | // ALWAYS remove old data first, regardless of replace/add 76 | self.remove_old_data(timestamp); 77 | 78 | if should_replace && !self.window.is_empty() { 79 | // Replace the last value in the same time bucket 80 | if let Some((_, old_value)) = self.window.pop_back() { 81 | self.sum -= old_value; 82 | self.sum_sq -= old_value * old_value; 83 | } 84 | } 85 | 86 | // Add new value to the window 87 | self.window.push_back((timestamp, value)); 88 | self.sum += value; 89 | self.sum_sq += value * value; 90 | 91 | // Calculate the population standard deviation 92 | let n = self.window.len() as f64; 93 | if n == 0.0 { 94 | 0.0 95 | } else { 96 | let mean = self.sum / n; 97 | let variance = (self.sum_sq - (self.sum * mean)) / n; 98 | variance.sqrt() 99 | } 100 | } 101 | } 102 | 103 | impl Reset for StandardDeviation { 104 | fn reset(&mut self) { 105 | self.window.clear(); 106 | self.sum = 0.0; 107 | self.sum_sq = 0.0; 108 | self.detector.reset(); 109 | } 110 | } 111 | 112 | impl Default for StandardDeviation { 113 | fn default() -> Self { 114 | // Use std::time::Duration constructor 115 | Self::new(Duration::from_secs(14 * 24 * 60 * 60)).unwrap() // 14 days in seconds 116 | } 117 | } 118 | 119 | impl fmt::Display for StandardDeviation { 120 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 121 | // Use as_secs() instead of Debug format 122 | write!(f, "SD({}s)", self.duration.as_secs()) 123 | } 124 | } 125 | 126 | #[cfg(test)] 127 | mod tests { 128 | use crate::test_helper::round; 129 | 130 | use super::*; 131 | use chrono::{TimeZone, Utc}; 132 | 133 | #[test] 134 | fn test_new() { 135 | assert!(StandardDeviation::new(Duration::from_secs(0)).is_err()); 136 | assert!(StandardDeviation::new(Duration::from_secs(1)).is_ok()); 137 | } 138 | 139 | #[test] 140 | fn test_next() { 141 | let duration = Duration::from_secs(4); 142 | let mut sd = StandardDeviation::new(duration).unwrap(); 143 | let now = Utc::now(); 144 | // Use chrono::Duration for adding to DateTime 145 | assert_eq!(sd.next((now + chrono::Duration::seconds(1), 10.0)), 0.0); 146 | assert_eq!(sd.next((now + chrono::Duration::seconds(2), 20.0)), 5.0); 147 | assert_eq!( 148 | round(sd.next((now + chrono::Duration::seconds(3), 30.0))), 149 | 8.165 150 | ); 151 | assert_eq!( 152 | round(sd.next((now + chrono::Duration::seconds(4), 20.0))), 153 | 7.071 154 | ); 155 | assert_eq!( 156 | round(sd.next((now + chrono::Duration::seconds(5), 10.0))), 157 | 7.071 158 | ); 159 | assert_eq!( 160 | round(sd.next((now + chrono::Duration::seconds(6), 100.0))), 161 | 35.355 162 | ); 163 | } 164 | 165 | #[test] 166 | fn test_reset() { 167 | let duration = Duration::from_secs(4); 168 | let mut sd = StandardDeviation::new(duration).unwrap(); 169 | let now = Utc::now(); 170 | assert_eq!(sd.next((now, 10.0)), 0.0); 171 | assert_eq!(sd.next((now + chrono::Duration::seconds(1), 20.0)), 5.0); 172 | assert_eq!( 173 | round(sd.next((now + chrono::Duration::seconds(2), 30.0))), 174 | 8.165 175 | ); 176 | 177 | sd.reset(); 178 | assert_eq!(sd.next((now + chrono::Duration::seconds(3), 20.0)), 0.0); 179 | } 180 | 181 | #[test] 182 | fn test_default() { 183 | let _sd = StandardDeviation::default(); 184 | } 185 | 186 | #[test] 187 | fn test_display() { 188 | let indicator = StandardDeviation::new(Duration::from_secs(7)).unwrap(); 189 | assert_eq!(format!("{}", indicator), "SD(7s)"); 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /src/indicators/max_drawup.rs: -------------------------------------------------------------------------------- 1 | use std::collections::VecDeque; 2 | use std::fmt; 3 | use std::time::Duration; // Change: Use std::time::Duration 4 | 5 | use crate::errors::{Result, TaError}; 6 | use crate::indicators::AdaptiveTimeDetector; 7 | use crate::{Next, Reset}; 8 | use chrono::{DateTime, Utc}; 9 | #[cfg(feature = "serde")] 10 | use serde::{Deserialize, Serialize}; 11 | 12 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 13 | #[derive(Debug, Clone)] 14 | pub struct MaxDrawup { 15 | duration: Duration, // Now std::time::Duration 16 | window: VecDeque<(DateTime, f64)>, 17 | detector: AdaptiveTimeDetector, 18 | } 19 | 20 | impl MaxDrawup { 21 | pub fn get_window(&self) -> VecDeque<(DateTime, f64)> { 22 | self.window.clone() 23 | } 24 | 25 | pub fn new(duration: Duration) -> Result { 26 | // Change: Check for zero duration (std::time::Duration can't be negative) 27 | if duration.as_secs() == 0 && duration.subsec_nanos() == 0 { 28 | Err(TaError::InvalidParameter) 29 | } else { 30 | Ok(Self { 31 | duration, 32 | window: VecDeque::new(), 33 | detector: AdaptiveTimeDetector::new(duration), 34 | }) 35 | } 36 | } 37 | 38 | fn calculate_max_drawup(&self) -> f64 { 39 | let mut trough = f64::MAX; 40 | let mut max_drawup = 0.0; 41 | 42 | for &(_, value) in &self.window { 43 | if value < trough { 44 | trough = value; 45 | } 46 | let drawup = (value - trough) / trough; 47 | if drawup > max_drawup { 48 | max_drawup = drawup; 49 | } 50 | } 51 | 52 | 100.0 * max_drawup 53 | } 54 | 55 | fn remove_old_data(&mut self, current_time: DateTime) { 56 | // Change: Convert std::time::Duration to chrono::Duration for date arithmetic 57 | let chrono_duration = chrono::Duration::from_std(self.duration).unwrap(); 58 | while self 59 | .window 60 | .front() 61 | .map_or(false, |(time, _)| *time < current_time - chrono_duration) 62 | { 63 | self.window.pop_front(); 64 | } 65 | } 66 | } 67 | 68 | impl Next for MaxDrawup { 69 | type Output = f64; 70 | 71 | fn next(&mut self, (timestamp, value): (DateTime, f64)) -> Self::Output { 72 | // Check if we should replace the last value (same time bucket) 73 | let should_replace = self.detector.should_replace(timestamp); 74 | 75 | // ALWAYS remove old data first, regardless of replace/add 76 | self.remove_old_data(timestamp); 77 | 78 | if should_replace && !self.window.is_empty() { 79 | // Replace the last value in the same time bucket 80 | self.window.pop_back(); 81 | } 82 | 83 | // Add the new data point 84 | self.window.push_back((timestamp, value)); 85 | 86 | // Calculate the maximum drawup within the current window 87 | self.calculate_max_drawup() 88 | } 89 | } 90 | 91 | impl Reset for MaxDrawup { 92 | fn reset(&mut self) { 93 | self.window.clear(); 94 | self.detector.reset(); 95 | } 96 | } 97 | 98 | impl fmt::Display for MaxDrawup { 99 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 100 | // Change: Use as_secs() instead of num_seconds() 101 | write!(f, "MaxDrawup({}s)", self.duration.as_secs()) 102 | } 103 | } 104 | 105 | impl Default for MaxDrawup { 106 | fn default() -> Self { 107 | // Change: Use Duration::from_secs for 14 days 108 | Self::new(Duration::from_secs(14 * 24 * 60 * 60)).unwrap() 109 | } 110 | } 111 | 112 | #[cfg(test)] 113 | mod tests { 114 | use super::*; 115 | use chrono::TimeZone; 116 | 117 | #[test] 118 | fn test_new() { 119 | // Change: Use std::time::Duration constructors 120 | assert!(MaxDrawup::new(Duration::from_secs(0)).is_err()); 121 | assert!(MaxDrawup::new(Duration::from_secs(1)).is_ok()); 122 | } 123 | 124 | #[test] 125 | fn test_next() { 126 | let duration = Duration::from_secs(2); 127 | let mut max = MaxDrawup::new(duration).unwrap(); 128 | let start_time = Utc.ymd(2020, 1, 1).and_hms(0, 0, 0); 129 | 130 | // Use chrono::Duration for date arithmetic 131 | assert_eq!(max.next((start_time, 4.0)), 0.0); 132 | assert_eq!( 133 | max.next((start_time + chrono::Duration::seconds(1), 2.0)), 134 | 0.0 135 | ); 136 | assert_eq!( 137 | max.next((start_time + chrono::Duration::seconds(2), 1.0)), 138 | 0.0 139 | ); 140 | assert_eq!( 141 | max.next((start_time + chrono::Duration::seconds(3), 3.0)), 142 | 200.0 143 | ); 144 | assert_eq!( 145 | max.next((start_time + chrono::Duration::seconds(4), 4.0)), 146 | 300.0 147 | ); 148 | assert_eq!( 149 | crate::test_helper::round(max.next((start_time + chrono::Duration::seconds(5), 3.0))), 150 | 33.333 151 | ); 152 | assert_eq!( 153 | max.next((start_time + chrono::Duration::seconds(6), 6.0)), 154 | 100.0 155 | ); 156 | assert_eq!( 157 | max.next((start_time + chrono::Duration::seconds(7), 9.0)), 158 | 200.0 159 | ); 160 | } 161 | 162 | #[test] 163 | fn test_reset() { 164 | let duration = Duration::from_secs(100); 165 | let mut max_drawup = MaxDrawup::new(duration).unwrap(); 166 | let start_time = Utc.ymd(2020, 1, 1).and_hms(0, 0, 0); 167 | 168 | assert_eq!(max_drawup.next((start_time, 4.0)), 0.0); 169 | 170 | assert_eq!( 171 | max_drawup.next((start_time + chrono::Duration::seconds(50), 10.0)), 172 | 150.0 173 | ); 174 | 175 | assert_eq!( 176 | max_drawup.next((start_time + chrono::Duration::seconds(100), 2.0)), 177 | 150.0 178 | ); 179 | 180 | max_drawup.reset(); 181 | 182 | assert_eq!( 183 | max_drawup.next((start_time + chrono::Duration::seconds(150), 4.0)), 184 | 0.0 185 | ); 186 | 187 | assert_eq!( 188 | max_drawup.next((start_time + chrono::Duration::seconds(200), 8.0)), 189 | 100.0 190 | ); 191 | } 192 | 193 | #[test] 194 | fn test_display() { 195 | let indicator = MaxDrawup::new(Duration::from_secs(7)).unwrap(); 196 | assert_eq!(format!("{}", indicator), "MaxDrawup(7s)"); 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /src/indicators/relative_strength_index.rs: -------------------------------------------------------------------------------- 1 | use std::collections::VecDeque; 2 | use std::fmt; 3 | use std::time::Duration; // Change: Use std::time::Duration 4 | 5 | use crate::errors::Result; 6 | use crate::indicators::{AdaptiveTimeDetector, ExponentialMovingAverage as Ema}; 7 | use crate::{Next, Reset}; 8 | use chrono::{DateTime, Utc}; 9 | #[cfg(feature = "serde")] 10 | use serde::{Deserialize, Serialize}; 11 | 12 | #[doc(alias = "RSI")] 13 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 14 | #[derive(Debug, Clone)] 15 | pub struct RelativeStrengthIndex { 16 | duration: Duration, // Now std::time::Duration 17 | up_ema_indicator: Ema, 18 | down_ema_indicator: Ema, 19 | window: VecDeque<(DateTime, f64)>, 20 | prev_val: Option, 21 | detector: AdaptiveTimeDetector, 22 | } 23 | 24 | impl RelativeStrengthIndex { 25 | pub fn new(duration: Duration) -> Result { 26 | // Note: Ema::new() now also expects std::time::Duration 27 | Ok(Self { 28 | duration, 29 | up_ema_indicator: Ema::new(duration)?, // Assuming 14-period EMA 30 | down_ema_indicator: Ema::new(duration)?, 31 | window: VecDeque::new(), 32 | prev_val: None, 33 | detector: AdaptiveTimeDetector::new(duration), 34 | }) 35 | } 36 | 37 | fn remove_old_data(&mut self, current_time: DateTime) { 38 | // Change: Convert std::time::Duration to chrono::Duration for date arithmetic 39 | let chrono_duration = chrono::Duration::from_std(self.duration).unwrap(); 40 | while self 41 | .window 42 | .front() 43 | .map_or(false, |(time, _)| *time < current_time - chrono_duration) 44 | { 45 | self.window.pop_front(); 46 | } 47 | } 48 | } 49 | 50 | impl Next for RelativeStrengthIndex { 51 | type Output = f64; 52 | 53 | fn next(&mut self, (timestamp, value): (DateTime, f64)) -> Self::Output { 54 | // Check if we should replace the last value (same time bucket) 55 | let should_replace = self.detector.should_replace(timestamp); 56 | 57 | // ALWAYS remove old data first, regardless of replace/add 58 | self.remove_old_data(timestamp); 59 | 60 | if should_replace && !self.window.is_empty() { 61 | // For RSI, when replacing a value in the same time bucket, 62 | // we don't change prev_val since it represents the previous period's close 63 | // Just remove the last window entry to be replaced 64 | self.window.pop_back(); 65 | } else { 66 | // Update prev_val to the last complete period's value 67 | // This is crucial: prev_val should be the closing value of the previous period 68 | if !self.window.is_empty() { 69 | self.prev_val = Some(self.window.back().unwrap().1); 70 | } 71 | } 72 | 73 | // Calculate gain and loss using the stable prev_val 74 | let (gain, loss) = if let Some(prev_val) = self.prev_val { 75 | if value > prev_val { 76 | (value - prev_val, 0.0) 77 | } else { 78 | (0.0, prev_val - value) 79 | } 80 | } else { 81 | (0.0, 0.0) 82 | }; 83 | 84 | // Add to window AFTER calculating gain/loss 85 | self.window.push_back((timestamp, value)); 86 | 87 | // Only update prev_val for the NEXT period if this is not a replacement 88 | // When replacing, prev_val stays as the previous period's close 89 | if !should_replace { 90 | self.prev_val = Some(value); 91 | } 92 | 93 | // Update EMAs 94 | let avg_up = self.up_ema_indicator.next((timestamp, gain)); 95 | let avg_down = self.down_ema_indicator.next((timestamp, loss)); 96 | 97 | // Calculate and return RSI 98 | if avg_down == 0.0 { 99 | if avg_up == 0.0 { 100 | 50.0 // Neutral value when no movement 101 | } else { 102 | 100.0 // Max value when only gains 103 | } 104 | } else { 105 | let rs = avg_up / avg_down; 106 | 100.0 - (100.0 / (1.0 + rs)) 107 | } 108 | } 109 | } 110 | 111 | impl Reset for RelativeStrengthIndex { 112 | fn reset(&mut self) { 113 | self.window.clear(); 114 | self.prev_val = None; 115 | self.up_ema_indicator.reset(); 116 | self.down_ema_indicator.reset(); 117 | self.detector.reset(); 118 | } 119 | } 120 | 121 | impl Default for RelativeStrengthIndex { 122 | fn default() -> Self { 123 | // Change: Use Duration::from_secs for 14 days 124 | Self::new(Duration::from_secs(14 * 24 * 60 * 60)).unwrap() 125 | } 126 | } 127 | 128 | impl fmt::Display for RelativeStrengthIndex { 129 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 130 | // Change: Calculate days from seconds 131 | let days = self.duration.as_secs() / 86400; 132 | write!(f, "RSI({} days)", days) 133 | } 134 | } 135 | 136 | #[cfg(test)] 137 | mod tests { 138 | use super::*; 139 | use crate::test_helper::*; 140 | use chrono::{TimeZone, Utc}; 141 | 142 | test_indicator!(RelativeStrengthIndex); 143 | 144 | #[test] 145 | fn test_new() { 146 | // Change: Use std::time::Duration constructors 147 | assert!(RelativeStrengthIndex::new(Duration::from_secs(0)).is_err()); 148 | assert!(RelativeStrengthIndex::new(Duration::from_secs(86400)).is_ok()); 149 | // 1 day 150 | } 151 | 152 | #[test] 153 | fn test_next() { 154 | let mut rsi = RelativeStrengthIndex::new(Duration::from_secs(3 * 86400)).unwrap(); // 3 days 155 | let timestamp = Utc.ymd(2020, 1, 1).and_hms(0, 0, 0); 156 | 157 | // First value: 10.0 (no previous value, so RSI = 50) 158 | assert_eq!(rsi.next((timestamp, 10.0)), 50.0); 159 | 160 | // Second value: 10.5 (gain of 0.5, no loss) 161 | assert_eq!( 162 | rsi.next((timestamp + chrono::Duration::days(1), 10.5)) 163 | .round(), 164 | 100.0 165 | ); 166 | 167 | // Third value: 10.0 (loss of 0.5 from 10.5) 168 | // With EMA k=0.5: avg_up=0.125, avg_down=0.25, RS=0.5, RSI=33.33 169 | assert_eq!( 170 | rsi.next((timestamp + chrono::Duration::days(2), 10.0)) 171 | .round(), 172 | 33.0 173 | ); 174 | 175 | // Fourth value: 9.5 (loss of 0.5 from 10.0) 176 | // With continued losses, RSI should drop further 177 | // avg_up = 0.0625, avg_down = 0.375, RS = 0.1667, RSI = 14.3 178 | assert_eq!( 179 | rsi.next((timestamp + chrono::Duration::days(3), 9.5)) 180 | .round(), 181 | 14.0 182 | ); 183 | } 184 | 185 | #[test] 186 | fn test_reset() { 187 | let mut rsi = RelativeStrengthIndex::new(Duration::from_secs(3 * 86400)).unwrap(); // 3 days 188 | let timestamp = Utc.ymd(2020, 1, 1).and_hms(0, 0, 0); 189 | assert_eq!(rsi.next((timestamp, 10.0)), 50.0); 190 | assert_eq!( 191 | rsi.next((timestamp + chrono::Duration::days(1), 10.5)) 192 | .round(), 193 | 100.0 194 | ); 195 | 196 | rsi.reset(); 197 | assert_eq!(rsi.next((timestamp, 10.0)).round(), 50.0); 198 | assert_eq!( 199 | rsi.next((timestamp + chrono::Duration::days(1), 10.5)) 200 | .round(), 201 | 100.0 202 | ); 203 | } 204 | 205 | #[test] 206 | fn test_default() { 207 | RelativeStrengthIndex::default(); 208 | } 209 | 210 | #[test] 211 | fn test_display() { 212 | let rsi = RelativeStrengthIndex::new(Duration::from_secs(16 * 86400)).unwrap(); // 16 days 213 | assert_eq!(format!("{}", rsi), "RSI(16 days)"); 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /src/indicators/exponential_moving_average.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | use std::time::Duration; 3 | 4 | use crate::errors::{Result, TaError}; 5 | use crate::indicators::AdaptiveTimeDetector; 6 | use crate::{Next, Reset}; 7 | use chrono::{DateTime, Utc}; 8 | #[cfg(feature = "serde")] 9 | use serde::{Deserialize, Serialize}; 10 | use std::collections::VecDeque; 11 | 12 | #[doc(alias = "EMA")] 13 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 14 | #[derive(Debug, Clone)] 15 | pub struct ExponentialMovingAverage { 16 | duration: Duration, // Now std::time::Duration 17 | k: f64, 18 | window: VecDeque<(DateTime, f64)>, 19 | current: f64, 20 | is_new: bool, 21 | detector: AdaptiveTimeDetector, 22 | last_value: f64, 23 | } 24 | 25 | impl ExponentialMovingAverage { 26 | pub fn new(duration: Duration) -> Result { 27 | // std::time::Duration can't be negative, so just check if it's zero 28 | if duration.as_secs() == 0 && duration.subsec_nanos() == 0 { 29 | Err(TaError::InvalidParameter) 30 | } else { 31 | // Determine the unit for periods based on duration 32 | // If duration is less than a day, use minutes (60s) as the unit 33 | // If duration is >= 1 day, use days (86400s) as the unit 34 | let unit_seconds = if duration < Duration::from_secs(86400) { 35 | 60.0 36 | } else { 37 | 86400.0 38 | }; 39 | 40 | // Calculate number of periods 41 | let periods = duration.as_secs() as f64 / unit_seconds; 42 | 43 | Ok(Self { 44 | duration, 45 | k: 2.0 / (periods + 1.0), 46 | window: VecDeque::new(), 47 | current: 0.0, 48 | is_new: true, 49 | detector: AdaptiveTimeDetector::new(duration), 50 | last_value: 0.0, 51 | }) 52 | } 53 | } 54 | 55 | fn remove_old_data(&mut self, current_time: DateTime) { 56 | // EMA doesn't actually need to remove old data 57 | // It's a running average that only depends on the current state 58 | // Keeping the window for potential debugging, but not removing data 59 | // This was causing issues with RSI calculations 60 | 61 | // Original code commented out: 62 | while self 63 | .window 64 | .front() 65 | .map_or(false, |(time, _)| *time <= current_time - self.duration) 66 | { 67 | self.window.pop_front(); 68 | } 69 | } 70 | } 71 | 72 | impl Next for ExponentialMovingAverage { 73 | type Output = f64; 74 | 75 | fn next(&mut self, (timestamp, value): (DateTime, f64)) -> Self::Output { 76 | // Check if we should replace the last value (same time bucket) 77 | let should_replace = self.detector.should_replace(timestamp); 78 | 79 | if should_replace && !self.is_new { 80 | // Reverse the previous EMA calculation and apply new value 81 | // Previous: current = k * last_value + (1-k) * old_current 82 | // Solve for old_current: old_current = (current - k * last_value) / (1-k) 83 | let old_current = if (1.0 - self.k) != 0.0 { 84 | (self.current - self.k * self.last_value) / (1.0 - self.k) 85 | } else { 86 | self.current 87 | }; 88 | self.current = (self.k * value) + ((1.0 - self.k) * old_current); 89 | } else { 90 | // New time period 91 | // EMA doesn't need to maintain a window or remove old data 92 | // It's a running average that only depends on current state 93 | 94 | if self.is_new { 95 | self.is_new = false; 96 | self.current = value; 97 | } else { 98 | self.current = (self.k * value) + ((1.0 - self.k) * self.current); 99 | } 100 | } 101 | 102 | self.last_value = value; 103 | self.current 104 | } 105 | } 106 | 107 | impl Reset for ExponentialMovingAverage { 108 | fn reset(&mut self) { 109 | self.window.clear(); 110 | self.current = 0.0; 111 | self.is_new = true; 112 | self.detector.reset(); 113 | self.last_value = 0.0; 114 | } 115 | } 116 | 117 | impl Default for ExponentialMovingAverage { 118 | fn default() -> Self { 119 | // Use std::time::Duration constructor 120 | Self::new(Duration::from_secs(14 * 24 * 60 * 60)).unwrap() // 14 days in seconds 121 | } 122 | } 123 | 124 | impl fmt::Display for ExponentialMovingAverage { 125 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 126 | let days = self.duration.as_secs() / 86400; 127 | if days > 0 && self.duration.as_secs() % 86400 == 0 { 128 | write!(f, "EMA({} days)", days) 129 | } else { 130 | write!(f, "EMA({}s)", self.duration.as_secs()) 131 | } 132 | } 133 | } 134 | 135 | #[cfg(test)] 136 | mod tests { 137 | use super::*; 138 | use chrono::Utc; 139 | 140 | #[test] 141 | fn test_new() { 142 | assert!(ExponentialMovingAverage::new(Duration::from_secs(0)).is_err()); 143 | assert!(ExponentialMovingAverage::new(Duration::from_secs(86400)).is_ok()); 144 | // 1 day 145 | } 146 | 147 | #[test] 148 | fn test_next() { 149 | let mut ema = ExponentialMovingAverage::new(Duration::from_secs(3 * 86400)).unwrap(); // 3 days 150 | let now = Utc::now(); 151 | 152 | assert_eq!(ema.next((now, 2.0)), 2.0); 153 | assert_eq!(ema.next((now + chrono::Duration::days(1), 5.0)), 3.5); 154 | assert_eq!(ema.next((now + chrono::Duration::days(2), 1.0)), 2.25); 155 | assert_eq!(ema.next((now + chrono::Duration::days(3), 6.25)), 4.25); 156 | } 157 | 158 | #[test] 159 | fn test_reset() { 160 | let mut ema = ExponentialMovingAverage::new(Duration::from_secs(5 * 86400)).unwrap(); // 5 days 161 | let now = Utc::now(); 162 | 163 | assert_eq!(ema.next((now, 4.0)), 4.0); 164 | ema.next((now + chrono::Duration::days(1), 10.0)); 165 | ema.next((now + chrono::Duration::days(2), 15.0)); 166 | ema.next((now + chrono::Duration::days(3), 20.0)); 167 | assert_ne!(ema.next((now + chrono::Duration::days(4), 4.0)), 4.0); 168 | 169 | ema.reset(); 170 | assert_eq!(ema.next((now, 4.0)), 4.0); 171 | } 172 | 173 | #[test] 174 | fn test_default() { 175 | let _ema = ExponentialMovingAverage::default(); 176 | } 177 | 178 | #[test] 179 | fn test_display() { 180 | let ema = ExponentialMovingAverage::new(Duration::from_secs(7 * 86400)).unwrap(); // 7 days 181 | assert_eq!(format!("{}", ema), "EMA(7 days)"); 182 | } 183 | 184 | #[test] 185 | fn test_intraday_instability() { 186 | // 30 minute EMA 187 | // Old formula: k = 2 / (days + 1) = 2 / (0.02 + 1) = 1.96 (> 1.0, unstable) 188 | // New formula: k = 2 / (periods + 1) = 2 / (30 + 1) = 0.0645 (stable) 189 | let mut ema = ExponentialMovingAverage::new(Duration::from_secs(30 * 60)).unwrap(); 190 | let now = Utc::now(); 191 | 192 | // Feed constant value 100.0 193 | ema.next((now, 100.0)); 194 | 195 | // Step change to 110.0 196 | let val_step = ema.next((now + chrono::Duration::minutes(1), 110.0)); 197 | 198 | // With k ~ 0.0645: 199 | // val = 0.0645 * 110 + (1 - 0.0645) * 100 200 | // val = 7.095 + 93.55 = 100.645 201 | 202 | // With old buggy k ~ 1.96: 203 | // val = 1.96 * 110 + (1 - 1.96) * 100 204 | // val = 215.6 - 96 = 119.6 (Overshoot) 205 | 206 | assert!(val_step < 110.0, "EMA overshot the target value! Value: {}", val_step); 207 | assert!(val_step > 100.0, "EMA did not increase! Value: {}", val_step); 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /tests/test_adaptive_replacement.rs: -------------------------------------------------------------------------------- 1 | use chrono::{TimeZone, Utc}; 2 | use std::time::Duration; // Add this import for std::time::Duration 3 | use ta::indicators::{ExponentialMovingAverage, SimpleMovingAverage, StandardDeviation}; 4 | use ta::Next; 5 | 6 | #[test] 7 | fn test_daily_ohlc_no_replacement() { 8 | // Test that daily OHLC data (Open and Close) are NOT replaced 9 | let mut sma = SimpleMovingAverage::new(Duration::from_secs(3 * 86400)).unwrap(); // 3 days 10 | 11 | // Day 1: Open at 9:30 AM 12 | let day1_open = Utc.with_ymd_and_hms(2024, 1, 1, 9, 30, 0).unwrap(); 13 | let result1 = sma.next((day1_open, 100.0)); 14 | assert_eq!(result1, 100.0); // First value 15 | 16 | // Day 1: Close at 4:00 PM (6.5 hours later) - should NOT replace 17 | let day1_close = Utc.with_ymd_and_hms(2024, 1, 1, 16, 0, 0).unwrap(); 18 | let result2 = sma.next((day1_close, 105.0)); 19 | assert_eq!(result2, 102.5); // Average of 100 and 105 20 | 21 | // Day 2: Open at 9:30 AM 22 | let day2_open = Utc.with_ymd_and_hms(2024, 1, 2, 9, 30, 0).unwrap(); 23 | let result3 = sma.next((day2_open, 110.0)); 24 | assert_eq!(result3, 105.0); // Average of 100, 105, and 110 25 | 26 | // Day 2: Close at 4:00 PM - should NOT replace 27 | let day2_close = Utc.with_ymd_and_hms(2024, 1, 2, 16, 0, 0).unwrap(); 28 | let result4 = sma.next((day2_close, 108.0)); 29 | // With 3-day window, all 4 values should still be in window 30 | // Window has: 100, 105, 110, 108 31 | assert_eq!(result4, 105.75); 32 | } 33 | 34 | #[test] 35 | fn test_intraday_replacement_within_bucket() { 36 | // Test that intraday data within the same time bucket DOES get replaced 37 | let mut sma = SimpleMovingAverage::new(Duration::from_secs(15 * 60)).unwrap(); // 15 minutes 38 | 39 | let base_time = Utc.with_ymd_and_hms(2024, 1, 1, 9, 30, 0).unwrap(); 40 | 41 | // First 5-minute bar 42 | let result1 = sma.next((base_time, 100.0)); 43 | assert_eq!(result1, 100.0); 44 | 45 | // Second 5-minute bar (different bucket) 46 | let result2 = sma.next((base_time + chrono::Duration::minutes(5), 101.0)); 47 | assert_eq!(result2, 100.5); // Average of 100 and 101 48 | 49 | // Third 5-minute bar (different bucket) 50 | let result3 = sma.next((base_time + chrono::Duration::minutes(10), 102.0)); 51 | assert_eq!(result3, 101.0); // Average of 100, 101, 102 52 | 53 | // Update at minute 11 - with 5-minute bucket detection, this is a new bucket 54 | // so it won't replace 55 | let result4 = sma.next((base_time + chrono::Duration::minutes(11), 103.0)); 56 | assert_eq!(result4, 101.5); // Average of 100, 101, 102, 103 57 | 58 | // Move to next bucket (minute 15) 59 | let result5 = sma.next((base_time + chrono::Duration::minutes(15), 104.0)); 60 | // With 15-minute window, the first value (100) should drop off 61 | // Window now has: 101, 102, 103, 104 (values from minutes 5, 10, 11, 15) 62 | assert_eq!(result5, 102.5); // Average of 101, 102, 103, 104 63 | } 64 | 65 | #[test] 66 | fn test_standard_deviation_with_replacement() { 67 | // Test StandardDeviation with adaptive replacement 68 | let mut sd = StandardDeviation::new(Duration::from_secs(3600)).unwrap(); // 1 hour 69 | 70 | let base_time = Utc.with_ymd_and_hms(2024, 1, 1, 10, 0, 0).unwrap(); 71 | 72 | // Add values at 1-minute intervals (will be detected as intraday) 73 | sd.next((base_time, 10.0)); 74 | sd.next((base_time + chrono::Duration::minutes(1), 12.0)); 75 | sd.next((base_time + chrono::Duration::minutes(2), 11.0)); 76 | 77 | // Update within the same minute bucket - should replace 78 | let result1 = sd.next(( 79 | base_time + chrono::Duration::minutes(2) + chrono::Duration::seconds(30), 80 | 11.5, 81 | )); 82 | 83 | // Add another value in a new minute 84 | let result2 = sd.next((base_time + chrono::Duration::minutes(3), 10.5)); 85 | 86 | // The standard deviation should be calculated with the replaced value 87 | assert!(result2 > 0.0); // Should have some variance 88 | } 89 | 90 | #[test] 91 | #[test] 92 | fn test_transition_from_warmup_to_live() { 93 | // Simulate warming up with daily data then transitioning to intraday 94 | let mut sma = SimpleMovingAverage::new(Duration::from_secs(2 * 86400)).unwrap(); // 2 days 95 | 96 | // Warmup with daily OHLC (>4 hours apart) 97 | let day1_open = Utc.with_ymd_and_hms(2024, 1, 1, 9, 30, 0).unwrap(); 98 | sma.next((day1_open, 100.0)); 99 | 100 | let day1_close = Utc.with_ymd_and_hms(2024, 1, 1, 16, 0, 0).unwrap(); 101 | sma.next((day1_close, 102.0)); 102 | 103 | let day2_open = Utc.with_ymd_and_hms(2024, 1, 2, 9, 30, 0).unwrap(); 104 | let warmup_result = sma.next((day2_open, 104.0)); 105 | assert_eq!(warmup_result, 102.0); // Average of 100, 102, 104 106 | 107 | // Now continue with more frequent updates on day 2 108 | // With DailyOHLC and 3.4-hour gap logic, this WILL replace day2_open 109 | // since 30 minutes < 3.4 hours 110 | let day2_mid = Utc.with_ymd_and_hms(2024, 1, 2, 10, 0, 0).unwrap(); 111 | let result = sma.next((day2_mid, 105.0)); 112 | // Should replace day2_open (104) with 105 113 | assert_eq!(result, (100.0 + 102.0 + 105.0) / 3.0); // Average of 100, 102, 105 114 | } 115 | 116 | #[test] 117 | fn test_high_frequency_tick_data() { 118 | // Test with very high frequency data (sub-second) 119 | let mut sma = SimpleMovingAverage::new(Duration::from_secs(5)).unwrap(); // 5 seconds 120 | 121 | let base_time = Utc.with_ymd_and_hms(2024, 1, 1, 10, 0, 0).unwrap(); 122 | 123 | // Add tick data at sub-second intervals 124 | // These should be detected as high-frequency and each treated as unique 125 | sma.next((base_time, 100.0)); 126 | sma.next((base_time + chrono::Duration::milliseconds(100), 100.1)); 127 | sma.next((base_time + chrono::Duration::milliseconds(200), 100.2)); 128 | sma.next((base_time + chrono::Duration::milliseconds(300), 100.3)); 129 | 130 | let result = sma.next((base_time + chrono::Duration::milliseconds(400), 100.4)); 131 | // With a 5-second duration (< 5 minutes), we use second-level bucketing 132 | // All millisecond updates within the same second get replaced 133 | // So we only have the last value: 100.4 134 | assert_eq!(result, 100.4); // Only one value in the window after replacements 135 | } 136 | 137 | #[test] 138 | fn test_minute_bar_replacement() { 139 | // Test replacement within minute bars 140 | let mut sma = SimpleMovingAverage::new(Duration::from_secs(5 * 60)).unwrap(); // 5 minutes 141 | 142 | let base_time = Utc.with_ymd_and_hms(2024, 1, 1, 10, 0, 0).unwrap(); 143 | 144 | // First minute 145 | sma.next((base_time, 100.0)); 146 | 147 | // Update within the same minute (should replace if detected as 1-minute buckets) 148 | let result1 = sma.next((base_time + chrono::Duration::seconds(30), 100.5)); 149 | 150 | // Second minute 151 | let result2 = sma.next((base_time + chrono::Duration::minutes(1), 101.0)); 152 | 153 | // Third minute 154 | let result3 = sma.next((base_time + chrono::Duration::minutes(2), 102.0)); 155 | 156 | // The exact results depend on how the detector interprets the pattern 157 | // But we should have at most 3 values in the window 158 | assert!(result3 >= 100.0 && result3 <= 102.0); 159 | } 160 | 161 | #[test] 162 | fn test_weekly_data_detection() { 163 | // Test with weekly data (very long intervals) 164 | let mut sma = SimpleMovingAverage::new(Duration::from_secs(21 * 86400)).unwrap(); // 3 weeks (21 days) 165 | 166 | let week1 = Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap(); 167 | let week2 = Utc.with_ymd_and_hms(2024, 1, 8, 0, 0, 0).unwrap(); 168 | let week3 = Utc.with_ymd_and_hms(2024, 1, 15, 0, 0, 0).unwrap(); 169 | 170 | let result1 = sma.next((week1, 100.0)); 171 | assert_eq!(result1, 100.0); 172 | 173 | let result2 = sma.next((week2, 110.0)); 174 | assert_eq!(result2, 105.0); 175 | 176 | let result3 = sma.next((week3, 120.0)); 177 | assert_eq!(result3, 110.0); 178 | 179 | // Add another value a week later 180 | let week4 = Utc.with_ymd_and_hms(2024, 1, 22, 0, 0, 0).unwrap(); 181 | let result4 = sma.next((week4, 115.0)); 182 | // First value should drop off (outside 21-day window) 183 | assert_eq!(result4, 115.0); // Average of 110, 120, 115 184 | } 185 | -------------------------------------------------------------------------------- /src/indicators/adaptive.rs: -------------------------------------------------------------------------------- 1 | use chrono::{DateTime, Utc}; 2 | use std::time::Duration; 3 | 4 | #[cfg(feature = "serde")] 5 | use serde::{Deserialize, Serialize}; 6 | 7 | /// Represents the frequency mode for de-duplication 8 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 9 | #[derive(Debug, Clone, PartialEq)] 10 | pub enum DetectedFrequency { 11 | /// Still learning from initial data points (kept for backward compatibility only) 12 | Unknown, 13 | /// Daily mode: maintains 3.4 hour gap between points 14 | DailyOHLC, 15 | /// Intraday mode: minute-level bucketing 16 | Intraday(Duration), 17 | } 18 | 19 | /// Handles time-based de-duplication logic for indicators 20 | /// 21 | /// Uses a simple duration-based rule: 22 | /// - Indicators with duration < 1 day: Use minute-level bucketing (replace within same minute) 23 | /// - Indicators with duration >= 1 day: Use daily bucketing with 3.4 hour gap enforcement 24 | /// 25 | /// The 3.4 hour gap for daily indicators ensures: 26 | /// - Half-day sessions (~3.5 hours): Captures both open and close as separate points 27 | /// - Full-day sessions (~6.5 hours): Captures morning and afternoon as separate points 28 | /// - Minutely updates during market hours: Continuously updates the current slot 29 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 30 | #[derive(Debug, Clone)] 31 | pub struct AdaptiveTimeDetector { 32 | frequency: DetectedFrequency, 33 | last_minute_bucket: i64, 34 | last_timestamp: Option>, 35 | } 36 | 37 | impl AdaptiveTimeDetector { 38 | /// Create a new detector for a specific indicator duration 39 | /// 40 | /// # Arguments 41 | /// * `duration` - The indicator's time window duration, used to determine bucketing strategy: 42 | /// - Duration < 5 minutes: Uses second-level bucketing 43 | /// - Duration < 1 day: Uses minute-level bucketing 44 | /// - Duration >= 1 day: Uses daily bucketing with 3.4 hour gap enforcement 45 | pub fn new(duration: Duration) -> Self { 46 | let frequency = if duration < Duration::from_secs(5 * 60) { 47 | // Use second bucketing for very short-term indicators (< 5 minutes) 48 | DetectedFrequency::Intraday(Duration::from_secs(1)) 49 | } else if duration < Duration::from_secs(86400) { 50 | // Use minute bucketing for short-term indicators (< 1 day) 51 | DetectedFrequency::Intraday(Duration::from_secs(60)) 52 | } else { 53 | // Use daily bucketing with gap enforcement for long-term indicators 54 | DetectedFrequency::DailyOHLC 55 | }; 56 | 57 | Self { 58 | frequency, 59 | last_minute_bucket: i64::MIN, 60 | last_timestamp: None, 61 | } 62 | } 63 | 64 | /// Create a new detector with custom detection samples (DEPRECATED - use new()) 65 | #[deprecated(since = "1.0.0", note = "Use new(duration) instead")] 66 | pub fn with_samples(_detection_samples: usize, duration: Duration) -> Self { 67 | Self::new(duration) 68 | } 69 | 70 | /// Get the current frequency mode 71 | pub fn frequency(&self) -> &DetectedFrequency { 72 | &self.frequency 73 | } 74 | 75 | /// Process a new timestamp and determine if it should replace the previous value 76 | /// Returns true if this is a duplicate within the same time bucket (should replace) 77 | /// Returns false if this is a new time period (should append) 78 | pub fn should_replace(&mut self, timestamp: DateTime) -> bool { 79 | match &self.frequency { 80 | DetectedFrequency::Intraday(bucket_duration) => { 81 | // Dynamic bucketing based on bucket_duration (second or minute level) 82 | let bucket_seconds = bucket_duration.as_secs() as i64; 83 | let current_bucket = timestamp.timestamp() / bucket_seconds; 84 | 85 | // Check if we're in the same bucket as last processed 86 | let should_replace = current_bucket == self.last_minute_bucket; 87 | 88 | // Update last processed bucket 89 | self.last_minute_bucket = current_bucket; 90 | 91 | should_replace 92 | } 93 | DetectedFrequency::DailyOHLC => { 94 | // Daily bucketing with 3.4 hour gap enforcement 95 | // The 3.4 hour threshold ensures: 96 | // - Half-day sessions (~3.5 hours): Captures both open and close 97 | // - Full-day sessions (~6.5 hours): Captures morning and afternoon 98 | 99 | // 3.4 hours = 3 hours 24 minutes = 12,240 seconds 100 | let min_gap = chrono::Duration::seconds(3 * 3600 + 24 * 60); 101 | 102 | if let Some(last_ts) = self.last_timestamp { 103 | let time_diff = timestamp - last_ts; 104 | 105 | // If within 3.4 hours of the last point, replace it 106 | // This handles minutely data during market hours 107 | if time_diff > chrono::Duration::zero() && time_diff < min_gap { 108 | // Don't update last_timestamp here - we're replacing 109 | return true; 110 | } 111 | } 112 | 113 | // Either first point or more than 3.4 hours have passed 114 | // This is a new slot, so update last_timestamp 115 | self.last_timestamp = Some(timestamp); 116 | false 117 | } 118 | DetectedFrequency::Unknown => { 119 | // Shouldn't happen but default to not replacing 120 | self.last_timestamp = Some(timestamp); 121 | false 122 | } 123 | } 124 | } 125 | 126 | /// Reset the detector to initial state 127 | pub fn reset(&mut self) { 128 | self.last_minute_bucket = i64::MIN; 129 | self.last_timestamp = None; 130 | // Keep frequency as it was set based on duration 131 | } 132 | 133 | /// Check if frequency has been detected 134 | /// Always returns true since we determine mode immediately from duration 135 | pub fn is_detected(&self) -> bool { 136 | self.frequency != DetectedFrequency::Unknown 137 | } 138 | } 139 | 140 | // Default implementation removed - force explicit duration 141 | // impl Default for AdaptiveTimeDetector { 142 | // fn default() -> Self { 143 | // Self::new() 144 | // } 145 | // } 146 | 147 | #[cfg(test)] 148 | mod tests { 149 | use super::*; 150 | use chrono::TimeZone; 151 | 152 | #[test] 153 | fn test_daily_indicator_with_gap_enforcement() { 154 | let mut detector = AdaptiveTimeDetector::new(Duration::from_secs(2 * 86400)); // 2 days 155 | assert_eq!(detector.frequency(), &DetectedFrequency::DailyOHLC); 156 | 157 | let base = Utc.ymd(2024, 1, 1).and_hms(9, 30, 0); 158 | 159 | // First point 160 | assert!(!detector.should_replace(base)); 161 | 162 | // Point 6.5 hours later (full trading day) - new slot 163 | assert!(!detector 164 | .should_replace(base + chrono::Duration::hours(6) + chrono::Duration::minutes(30))); 165 | 166 | // Next day - new slot 167 | assert!(!detector.should_replace(base + chrono::Duration::days(1))); 168 | 169 | // Now test the 3.4 hour gap enforcement 170 | let market_open = base + chrono::Duration::days(2); 171 | assert!(!detector.should_replace(market_open)); // New day, new slot 172 | 173 | // Minutely updates within 3.4 hours should replace 174 | assert!(detector.should_replace(market_open + chrono::Duration::minutes(1))); 175 | assert!(detector.should_replace(market_open + chrono::Duration::minutes(30))); 176 | assert!(detector.should_replace(market_open + chrono::Duration::hours(3))); 177 | 178 | // After 3.4 hours, should create new slot 179 | assert!(!detector.should_replace( 180 | market_open + chrono::Duration::hours(3) + chrono::Duration::minutes(25) 181 | )); 182 | } 183 | 184 | #[test] 185 | fn test_half_day_trading() { 186 | let mut detector = AdaptiveTimeDetector::new(Duration::from_secs(3 * 86400)); // 3 days 187 | assert_eq!(detector.frequency(), &DetectedFrequency::DailyOHLC); 188 | 189 | // Simulate half-day trading (market closes at 1:00 PM, ~3.5 hours) 190 | let half_day_open = Utc.ymd(2024, 1, 2).and_hms(9, 30, 0); 191 | assert!(!detector.should_replace(half_day_open)); // New slot for open price 192 | 193 | // All updates during first 3.4 hours should replace the open price 194 | assert!(detector.should_replace(half_day_open + chrono::Duration::minutes(30))); 195 | assert!(detector.should_replace(half_day_open + chrono::Duration::hours(2))); 196 | assert!(detector.should_replace(half_day_open + chrono::Duration::hours(3))); 197 | assert!(detector.should_replace( 198 | half_day_open + chrono::Duration::hours(3) + chrono::Duration::minutes(20) 199 | )); // Still within 3.4 hours 200 | 201 | // Close at 1:00 PM (3.5 hours) should create NEW slot since 3.5 > 3.4 202 | // This captures the closing price as a separate data point 203 | let half_day_close = 204 | half_day_open + chrono::Duration::hours(3) + chrono::Duration::minutes(30); 205 | assert!(!detector.should_replace(half_day_close)); // New slot for close price 206 | 207 | // If more updates come in near close, they replace the close price 208 | assert!(detector.should_replace(half_day_close + chrono::Duration::minutes(1))); 209 | 210 | // Next day should be a new slot 211 | let next_day = half_day_open + chrono::Duration::days(1); 212 | assert!(!detector.should_replace(next_day)); 213 | } 214 | 215 | #[test] 216 | fn test_intraday_indicator() { 217 | let mut detector = AdaptiveTimeDetector::new(Duration::from_secs(15 * 60)); // 15 minutes 218 | assert!(matches!( 219 | detector.frequency(), 220 | DetectedFrequency::Intraday(d) if d.as_secs() == 60 221 | )); 222 | 223 | let base = Utc.ymd(2024, 1, 1).and_hms(9, 30, 0); 224 | 225 | // First data point 226 | assert!(!detector.should_replace(base)); 227 | 228 | // Same minute - should replace 229 | assert!(detector.should_replace(base + chrono::Duration::seconds(30))); 230 | 231 | // Next minute - new slot 232 | assert!(!detector.should_replace(base + chrono::Duration::minutes(1))); 233 | 234 | // Within same minute - should replace 235 | assert!(detector 236 | .should_replace(base + chrono::Duration::minutes(1) + chrono::Duration::seconds(15))); 237 | 238 | // Next minute - new slot 239 | assert!(!detector.should_replace(base + chrono::Duration::minutes(2))); 240 | } 241 | 242 | #[test] 243 | fn test_full_trading_day_with_minutely_updates() { 244 | let mut detector = AdaptiveTimeDetector::new(Duration::from_secs(5 * 86400)); // 5 days 245 | 246 | // Full trading day: 9:30 AM to 4:00 PM (6.5 hours) 247 | let market_open = Utc.ymd(2024, 1, 2).and_hms(9, 30, 0); 248 | 249 | // First data point at market open 250 | assert!(!detector.should_replace(market_open)); 251 | 252 | // Minutely updates for first 3 hours should all replace 253 | for minutes in 1..=180 { 254 | assert!( 255 | detector.should_replace(market_open + chrono::Duration::minutes(minutes)), 256 | "Should replace at {} minutes after open", 257 | minutes 258 | ); 259 | } 260 | 261 | // After 3.4 hours (204 minutes), should create new slot 262 | assert!(!detector.should_replace(market_open + chrono::Duration::minutes(205))); 263 | 264 | // Subsequent updates should replace this new slot 265 | assert!(detector.should_replace(market_open + chrono::Duration::minutes(210))); 266 | assert!(detector.should_replace( 267 | market_open + chrono::Duration::hours(6) // Near market close 268 | )); 269 | } 270 | 271 | #[test] 272 | fn test_reset() { 273 | let mut detector = AdaptiveTimeDetector::new(Duration::from_secs(86400)); 274 | let base = Utc.ymd(2024, 1, 1).and_hms(9, 30, 0); 275 | 276 | // Use detector 277 | detector.should_replace(base); 278 | detector.should_replace(base + chrono::Duration::hours(1)); 279 | 280 | // Reset 281 | detector.reset(); 282 | 283 | // Frequency should remain but state should be cleared 284 | assert_eq!(detector.frequency(), &DetectedFrequency::DailyOHLC); 285 | assert!(detector.last_timestamp.is_none()); 286 | 287 | // Should work normally after reset 288 | assert!(!detector.should_replace(base + chrono::Duration::days(1))); 289 | } 290 | 291 | #[test] 292 | fn test_memory_footprint_for_trading_sessions() { 293 | let mut detector = AdaptiveTimeDetector::new(Duration::from_secs(10 * 86400)); // 10 days 294 | 295 | // Track which timestamps would be kept (not replaced) 296 | let mut kept_timestamps = Vec::new(); 297 | 298 | // Half-day: Should keep exactly 2 points (open and close) 299 | let half_day_open = Utc.ymd(2024, 1, 2).and_hms(9, 30, 0); 300 | if !detector.should_replace(half_day_open) { 301 | kept_timestamps.push(half_day_open); 302 | } 303 | 304 | // Minutely updates that get replaced 305 | for minutes in 1..209 { 306 | let ts = half_day_open + chrono::Duration::minutes(minutes); 307 | if !detector.should_replace(ts) { 308 | kept_timestamps.push(ts); 309 | } 310 | } 311 | 312 | // Close at 1:00 PM (210 minutes = 3.5 hours) 313 | let half_day_close = half_day_open + chrono::Duration::minutes(210); 314 | if !detector.should_replace(half_day_close) { 315 | kept_timestamps.push(half_day_close); 316 | } 317 | 318 | // We should have exactly 2 timestamps for the half-day 319 | assert_eq!( 320 | kept_timestamps.len(), 321 | 2, 322 | "Half-day should keep exactly 2 points" 323 | ); 324 | 325 | // Full trading day test 326 | kept_timestamps.clear(); 327 | detector.reset(); 328 | 329 | let full_day_open = Utc.ymd(2024, 1, 3).and_hms(9, 30, 0); 330 | if !detector.should_replace(full_day_open) { 331 | kept_timestamps.push(full_day_open); 332 | } 333 | 334 | // Minutely updates for 6.5 hours (390 minutes) 335 | for minutes in 1..=390 { 336 | let ts = full_day_open + chrono::Duration::minutes(minutes); 337 | if !detector.should_replace(ts) { 338 | kept_timestamps.push(ts); 339 | } 340 | } 341 | 342 | // We should have exactly 2 timestamps for the full day 343 | // One at open, one after 3.4 hours (204 minutes) 344 | assert_eq!( 345 | kept_timestamps.len(), 346 | 2, 347 | "Full day should keep exactly 2 points" 348 | ); 349 | 350 | // Verify the gap between kept points is > 3.4 hours 351 | let gap = kept_timestamps[1] - kept_timestamps[0]; 352 | assert!( 353 | gap >= chrono::Duration::minutes(204), 354 | "Gap between points should be >= 3.4 hours" 355 | ); 356 | } 357 | } 358 | --------------------------------------------------------------------------------