├── .buildkite └── pipeline.yaml ├── .envrc ├── .github └── pull_request_template.md ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── benches ├── alma.rs ├── center_of_gravity.rs ├── correlation_trend_indicator.rs ├── cumulative.rs ├── cyber_cycle.rs ├── ehlers_fisher_transform.rs ├── ema.rs ├── hl_normalizer.rs ├── laguerre_filter.rs ├── laguerre_rsi.rs ├── my_rsi.rs ├── noise_elimination_technology.rs ├── polarized_fractal_efficiency.rs ├── re_flex.rs ├── roc.rs ├── roofing_filter.rs ├── rsi.rs ├── sma.rs ├── super_smoother.rs ├── trend_flex.rs ├── vsct.rs ├── vst.rs └── welford_online.rs ├── examples ├── basic_chainable_view.rs └── basic_single_view.rs ├── flake.lock ├── flake.nix ├── img ├── agplv3.png ├── alma.png ├── center_of_gravity.png ├── center_of_gravity_normalized.png ├── correlation_trend_indicator.png ├── cumulative.png ├── cyber_cycle.png ├── cyber_cycle_normalized.png ├── echo.png ├── ehlers_fisher_transform.png ├── ema.png ├── laguerre_filter.png ├── laguerre_rsi.png ├── monero_donations_qrcode.png ├── my_rsi.png ├── net_my_rsi.png ├── polarized_fractal_efficiency.png ├── re_flex.png ├── re_flex_normalized.png ├── roc.png ├── roc_normalized.png ├── roofing_filter.png ├── rsi.png ├── rsi_normalized.png ├── sma.png ├── super_smoother.png ├── trend_flex.png ├── trend_flex_normalized.png ├── vsct.png └── welford_online_sliding.png ├── mprocs.yaml └── src ├── lib.rs ├── plot.rs ├── pure_functions ├── add.rs ├── constant.rs ├── divide.rs ├── echo.rs ├── gte.rs ├── lte.rs ├── mod.rs ├── multiply.rs ├── subtract.rs └── tanh.rs ├── rolling ├── drawdown.rs ├── ln_return.rs ├── mod.rs └── welford_rolling.rs ├── sliding_windows ├── alma.rs ├── binary_entropy.rs ├── center_of_gravity.rs ├── correlation_trend_indicator.rs ├── cumulative.rs ├── cyber_cycle.rs ├── ehlers_fisher_transform.rs ├── ema.rs ├── hl_normalizer.rs ├── laguerre_filter.rs ├── laguerre_rsi.rs ├── max.rs ├── min.rs ├── mod.rs ├── my_rsi.rs ├── noise_elimination_technology.rs ├── polarized_fractal_efficiency.rs ├── re_flex.rs ├── roc.rs ├── roofing_filter.rs ├── rsi.rs ├── sma.rs ├── super_smoother.rs ├── trend_flex.rs ├── variance_stabilizing_transformation.rs ├── vsct.rs └── welford_online.rs └── test_data.rs /.buildkite/pipeline.yaml: -------------------------------------------------------------------------------- 1 | steps: 2 | - label: ":nixos: :rust: :buildkite: check" 3 | command: RUSTFLAGS="-D warnings" nix develop --command bash -c "cargo check" 4 | agents: 5 | queue: nixos 6 | 7 | - label: ":nixos: :rust: :buildkite: clippy" 8 | command: RUSTFLAGS="-D warnings" nix develop --command bash -c "cargo clippy" 9 | agents: 10 | queue: nixos 11 | 12 | - label: ":nixos: :rust: :buildkite: test" 13 | command: RUSTFLAGS="-D warnings" nix develop --command bash -c "cargo test" 14 | agents: 15 | queue: nixos 16 | 17 | - label: ":nixos: :rust: :buildkite: fmt" 18 | command: RUSTFLAGS="-D warnings" nix develop --command bash -c "cargo fmt --check" 19 | agents: 20 | queue: nixos 21 | 22 | - label: ":nixos: :rust: :buildkite: doc" 23 | command: nix develop --command bash -c "cargo doc --no-deps --workspace" 24 | agents: 25 | queue: nixos 26 | -------------------------------------------------------------------------------- /.envrc: -------------------------------------------------------------------------------- 1 | use flake 2 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Changes: 2 | 3 | 6 | 7 | * 🩹 Bug Fix 8 | * 🦚 Feature 9 | * 📙 Documentation 10 | * 🪣 Misc 11 | 12 | ## References: 13 | 14 | 17 | 18 | ## Changes proposed by this PR: 19 | 20 | 23 | 24 | ## Notes to reviewer: 25 | 26 | 32 | 33 | ## 📜 Checklist 34 | 35 | * [ ] The PR scope is bounded 36 | * [ ] Relevant issues and discussions are referenced 37 | * [ ] Test coverage is excellent and passes 38 | * [ ] Tests test the desired behavior 39 | * [ ] Documentation is thorough 40 | 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /.idea 3 | /vendor 4 | /img 5 | /.direnv 6 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | cargo-features = ["edition2024"] 2 | 3 | [package] 4 | name = "sliding_features" 5 | version = "7.0.0" 6 | authors = ["MathisWellmann "] 7 | edition = "2024" 8 | license-file = "LICENSE" 9 | description = "Modular sliding window with various signal processing functions and technical indicators" 10 | repository = "https://github.com/MathisWellmann/sliding_features-rs" 11 | readme = "README.md" 12 | keywords = [ 13 | "sliding-window", 14 | "signal-processing", 15 | "modular", 16 | "technical-analysis", 17 | "indicators", 18 | ] 19 | categories = ["algorithms", "mathematics"] 20 | exclude = ["img/"] 21 | 22 | [dependencies] 23 | getset = "0.1" 24 | num = "0.4" 25 | 26 | [dev-dependencies] 27 | time_series_generator = "0.4.1" 28 | rand = "0.9" 29 | round = "0.1" 30 | plotters = "0.3" 31 | criterion = "0.5" 32 | 33 | [[bench]] 34 | name = "alma" 35 | harness = false 36 | 37 | [[bench]] 38 | name = "center_of_gravity" 39 | harness = false 40 | 41 | [[bench]] 42 | name = "correlation_trend_indicator" 43 | harness = false 44 | 45 | [[bench]] 46 | name = "cumulative" 47 | harness = false 48 | 49 | [[bench]] 50 | name = "cyber_cycle" 51 | harness = false 52 | 53 | [[bench]] 54 | name = "ehlers_fisher_transform" 55 | harness = false 56 | 57 | [[bench]] 58 | name = "ema" 59 | harness = false 60 | 61 | [[bench]] 62 | name = "hl_normalizer" 63 | harness = false 64 | 65 | [[bench]] 66 | name = "laguerre_filter" 67 | harness = false 68 | 69 | [[bench]] 70 | name = "laguerre_rsi" 71 | harness = false 72 | 73 | [[bench]] 74 | name = "my_rsi" 75 | harness = false 76 | 77 | [[bench]] 78 | name = "noise_elimination_technology" 79 | harness = false 80 | 81 | [[bench]] 82 | name = "polarized_fractal_efficiency" 83 | harness = false 84 | 85 | [[bench]] 86 | name = "re_flex" 87 | harness = false 88 | 89 | [[bench]] 90 | name = "roc" 91 | harness = false 92 | 93 | [[bench]] 94 | name = "roofing_filter" 95 | harness = false 96 | 97 | [[bench]] 98 | name = "rsi" 99 | harness = false 100 | 101 | [[bench]] 102 | name = "sma" 103 | harness = false 104 | 105 | [[bench]] 106 | name = "super_smoother" 107 | harness = false 108 | 109 | [[bench]] 110 | name = "trend_flex" 111 | harness = false 112 | 113 | [[bench]] 114 | name = "vst" 115 | harness = false 116 | 117 | [[bench]] 118 | name = "vsct" 119 | harness = false 120 | 121 | [[bench]] 122 | name = "welford_online" 123 | harness = false 124 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # :chains: Chainable Tree-like Sliding Features 2 | Modular, chainable sliding windows with various signal processing functions and technical indicators. 3 | A [`View`](https://docs.rs/sliding_features/2.5.2/sliding_features/trait.View.html) defines the function which processes the incoming values and provides an output value. 4 | `View`'s can easily be added by implementing the Trait which requires two methods: 5 | - `update(&mut self, val: f64)`: Call whenever you have a new value with which to update the View 6 | - `last(&self) -> f64`: Retrieve the last value from the View 7 | 8 | This enables multiple `View`'s to be chained together to apply many signal processing functions consecutively with zero-overhead thanks to Rust's zero-cost abstractions. 9 | For example you may want to compose a chained function that firstly smoothes the input values using an [`EMA`](https://docs.rs/sliding_features/2.5.2/sliding_features/struct.EMA.html), 10 | applies the rate of change [`ROC`](https://docs.rs/sliding_features/2.5.2/sliding_features/struct.ROC.html) function and finally applies normalization to it 11 | [`HLNormalizer`](https://docs.rs/sliding_features/2.5.2/sliding_features/struct.HLNormalizer.html). 12 | This can be achieved as such: 13 | ```ignore 14 | let mut chain = HLNormalizer::new(ROC:new(EMA::new(Echo::new(), 10), 15), 20); 15 | ``` 16 | Imagine this process as a tree with nodes (which is more accurate) as you can merge multiple `Views` together. 17 | An example of such a combining node is the [`Add`](https://docs.rs/sliding_features/2.5.2/sliding_features/struct.Add.html) node for example. 18 | This is one possible example to visualize the tree-like nature of this chaining process. 19 | ```mermaid 20 | flowchart TD 21 | A[Echo] --> B[EMA] 22 | C[Echo] --> D[SMA] 23 | B --> E[ROC] 24 | D --> F[RSI] 25 | E --> G[Add] 26 | F --> G 27 | G --> H[HLNormalize] 28 | ``` 29 | 30 | ### How to use 31 | To use this crate in your project add this to your Cargo.toml: 32 | ```toml 33 | sliding_features = "2.5.3" 34 | ``` 35 | 36 | To create a new View, call the appropriate constructor as such: 37 | ``` ignore 38 | let mut rsi = RSI::new(Echo::new(), 16); 39 | ``` 40 | This creates an [`RSI`](https://docs.rs/sliding_features/2.5.3/sliding_features/struct.RSI.html) 41 | indicator with window length of 16. Notice that [`Echo`](https://docs.rs/sliding_features/2.5.3/sliding_features/struct.Echo.html) 42 | will always be at the end of a View chain, as it just returns the latest observed value. 43 | Now to update the values of the chain, assuming test_values contains f64 values: 44 | 45 | ``` ignore 46 | for v in &test_values { 47 | rsi.update(v); 48 | let last = rsi.last(); 49 | println!("latest rsi value: {}", last); 50 | } 51 | ``` 52 | Each View will first call it's chained View to get it's last value, which will then be used to update the state of the View. 53 | Some Views have additional parameters such as ALMA. 54 | 55 | ### Examples 56 | See examples folder for some code ideas 57 | ```shell 58 | cargo run --release --example basic_single_view 59 | cargo run --release --example basic_chainable_view 60 | ``` 61 | 62 | ### Views 63 | A View defines the function which processes value updates. They currently include: 64 | * Echo 65 | * Technical Indicators 66 | * Center of Gravity 67 | * Cyber Cycle 68 | * Laguerre RSI 69 | * Laguerre Filter 70 | * ReFlex 71 | * TrendFlex 72 | * ROC 73 | * RSI 74 | * MyRSI (RSI in range [-1.0, 1.0]) 75 | * NET (John Ehlers noise elimination technology using kendall correlation) 76 | * Correlation Trend Indicator (CTI) 77 | * Polarized Fractal Efficiency 78 | * Ehlers Fisher Transform 79 | * SuperSmoother by JohnEhlers 80 | * RoofingFilter by JohnEhlers 81 | * Normalization / variance / mean standardization 82 | * HLNormalizer, a sliding high-low normalizer 83 | * Variance Stabilizing Transform (VST) 84 | * Variance Stabilizing Centering Transform (VSCT) 85 | * Moving Averages 86 | * ALMA (Arnaux Legoux Moving Average) 87 | * SMA (Simple Moving Average) 88 | * EMA (Exponential Moving Average) 89 | * Math combinations of Views 90 | * Add 91 | * Subtract 92 | * Multiply 93 | * Divide 94 | * Math functions 95 | * Tanh 96 | * GTE - Greater Than or Equal clipping function 97 | * LTE - Lower Than or Equal clipping function 98 | * Standard deviation sliding window estimation using WelfordOnlineSliding 99 | * Cumulative 100 | * Entropy 101 | 102 | ### Images 103 | Underlying data synthetically generated by 104 | [MathisWellmann/time_series_generator-rs](https://www.github.com/MathisWellmann/time_series_generator-rs) 105 | using a standard normal (gaussian) process. 106 | Note that each run uses common test data from test_data.rs for consistency. 107 | 108 | 109 | 112 | 113 | 114 | 115 | 118 | 119 | 120 | 121 | 124 | 125 | 126 | 127 | 130 | 131 | 132 | 133 | 136 | 137 | 138 | 139 | 142 | 143 | 144 | 145 | 148 | 149 | 150 | 151 | 154 | 155 | 156 | 157 | 160 | 161 | 162 | 163 | 166 | 167 | 168 | 169 | 172 | 173 | 174 | 175 | 178 | 179 | 180 | 181 | 184 | 185 | 186 | 187 | 190 | 191 | 192 | 193 | 196 | 197 | 198 | 199 | 202 | 203 | 204 | 205 | 208 | 209 | 210 | 211 | 214 | 215 | 216 | 217 | 220 | 221 | 222 | 223 | 226 | 227 | 228 | 229 | 232 | 233 | 234 | 235 | 238 | 239 | 240 | 241 | 244 | 245 | 246 | ### TODOs: 247 | Feel free to implement the following and create a PR for some easy open-source contributions: 248 | - FRAMA 249 | - MAMA 250 | - FAMA 251 | - Stochastic 252 | - Zero Lag 253 | - gaussian filter 254 | - correlation cycle indicator 255 | - some indicators can be built with const sized arrays, for better performance 256 | - add Default impl for all 257 | - maybe even throw in a generic numeric type 258 | - and so much more... 259 | 260 | ### Contributing 261 | If you have a sliding window function or indicator which you would like to integrate, 262 | feel free to create a pull request. Any help is highly appreciated. 263 | Let's build the greatest sliding window library together :handshake: 264 | 265 | ### Donations :moneybag: :money_with_wings: 266 | I you would like to support the development of this crate, feel free to send over a donation: 267 | 268 | Monero (XMR) address: 269 | ```plain 270 | 47xMvxNKsCKMt2owkDuN1Bci2KMiqGrAFCQFSLijWLs49ua67222Wu3LZryyopDVPYgYmAnYkSZSz9ZW2buaDwdyKTWGwwb 271 | ``` 272 | 273 | ![monero](img/monero_donations_qrcode.png) 274 | 275 | ## License 276 | Copyright (C) 2020 277 | 278 | This program is free software: you can redistribute it and/or modify 279 | it under the terms of the GNU Affero General Public License as published by 280 | the Free Software Foundation, either version 3 of the License, or 281 | (at your option) any later version. 282 | 283 | This program is distributed in the hope that it will be useful, 284 | but WITHOUT ANY WARRANTY; without even the implied warranty of 285 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 286 | GNU Affero General Public License for more details. 287 | 288 | You should have received a copy of the GNU Affero General Public License 289 | along with this program. If not, see . 290 | 291 | ![GNU AGPLv3](img/agplv3.png) 292 | -------------------------------------------------------------------------------- /benches/alma.rs: -------------------------------------------------------------------------------- 1 | use std::num::NonZeroUsize; 2 | 3 | use criterion::{black_box, criterion_group, criterion_main, Criterion}; 4 | use rand::{rng, Rng}; 5 | use sliding_features::{pure_functions::Echo, sliding_windows::Alma, View}; 6 | 7 | fn criterion_benchmark(c: &mut Criterion) { 8 | let mut rng = rng(); 9 | const N: usize = 100_000; 10 | 11 | let mut group = c.benchmark_group("alma_100k"); 12 | group.bench_function("f64", |b| { 13 | let vals = Vec::::from_iter((0..N).map(|_| rng.random())); 14 | b.iter(|| { 15 | let mut view = Alma::::new(Echo::new(), NonZeroUsize::new(1024).unwrap()); 16 | for v in vals.iter() { 17 | view.update(*v); 18 | let _ = black_box(view.last()); 19 | } 20 | }) 21 | }); 22 | group.bench_function("f32", |b| { 23 | let vals = Vec::::from_iter((0..N).map(|_| rng.random())); 24 | b.iter(|| { 25 | let mut view = Alma::::new(Echo::new(), NonZeroUsize::new(1024).unwrap()); 26 | for v in vals.iter() { 27 | view.update(*v); 28 | let _ = black_box(view.last()); 29 | } 30 | }) 31 | }); 32 | } 33 | 34 | criterion_group!(benches, criterion_benchmark); 35 | criterion_main!(benches); 36 | -------------------------------------------------------------------------------- /benches/center_of_gravity.rs: -------------------------------------------------------------------------------- 1 | use std::num::NonZeroUsize; 2 | 3 | use criterion::{black_box, criterion_group, criterion_main, Criterion}; 4 | use rand::{rng, Rng}; 5 | use sliding_features::{pure_functions::Echo, sliding_windows::CenterOfGravity, View}; 6 | 7 | fn criterion_benchmark(c: &mut Criterion) { 8 | let mut rng = rng(); 9 | const N: usize = 100_000; 10 | 11 | let mut group = c.benchmark_group("center_of_gravity_100k"); 12 | group.bench_function("f64", |b| { 13 | let vals = Vec::::from_iter((0..N).map(|_| rng.random())); 14 | b.iter(|| { 15 | let mut view = 16 | CenterOfGravity::::new(Echo::new(), NonZeroUsize::new(1024).unwrap()); 17 | for v in vals.iter() { 18 | view.update(*v); 19 | let _ = black_box(view.last()); 20 | } 21 | }) 22 | }); 23 | group.bench_function("f32", |b| { 24 | let vals = Vec::::from_iter((0..N).map(|_| rng.random())); 25 | b.iter(|| { 26 | let mut view = 27 | CenterOfGravity::::new(Echo::new(), NonZeroUsize::new(1024).unwrap()); 28 | for v in vals.iter() { 29 | view.update(*v); 30 | let _ = black_box(view.last()); 31 | } 32 | }) 33 | }); 34 | } 35 | 36 | criterion_group!(benches, criterion_benchmark); 37 | criterion_main!(benches); 38 | -------------------------------------------------------------------------------- /benches/correlation_trend_indicator.rs: -------------------------------------------------------------------------------- 1 | use std::num::NonZeroUsize; 2 | 3 | use criterion::{black_box, criterion_group, criterion_main, Criterion}; 4 | use rand::{rng, Rng}; 5 | use sliding_features::{pure_functions::Echo, sliding_windows::CorrelationTrendIndicator, View}; 6 | 7 | fn criterion_benchmark(c: &mut Criterion) { 8 | let mut rng = rng(); 9 | const N: usize = 100_000; 10 | 11 | let mut group = c.benchmark_group("correlation_trend_indicator_100k"); 12 | group.bench_function("f64", |b| { 13 | let vals = Vec::::from_iter((0..N).map(|_| rng.random())); 14 | b.iter(|| { 15 | let mut view = CorrelationTrendIndicator::::new( 16 | Echo::new(), 17 | NonZeroUsize::new(1024).unwrap(), 18 | ); 19 | for v in vals.iter() { 20 | view.update(*v); 21 | let _ = black_box(view.last()); 22 | } 23 | }) 24 | }); 25 | group.bench_function("f32", |b| { 26 | let vals = Vec::::from_iter((0..N).map(|_| rng.random())); 27 | b.iter(|| { 28 | let mut view = CorrelationTrendIndicator::::new( 29 | Echo::new(), 30 | NonZeroUsize::new(1024).unwrap(), 31 | ); 32 | for v in vals.iter() { 33 | view.update(*v); 34 | let _ = black_box(view.last()); 35 | } 36 | }) 37 | }); 38 | } 39 | 40 | criterion_group!(benches, criterion_benchmark); 41 | criterion_main!(benches); 42 | -------------------------------------------------------------------------------- /benches/cumulative.rs: -------------------------------------------------------------------------------- 1 | use std::num::NonZeroUsize; 2 | 3 | use criterion::{black_box, criterion_group, criterion_main, Criterion}; 4 | use rand::{rng, Rng}; 5 | use sliding_features::{pure_functions::Echo, sliding_windows::Cumulative, View}; 6 | 7 | fn criterion_benchmark(c: &mut Criterion) { 8 | let mut rng = rng(); 9 | const N: usize = 100_000; 10 | 11 | let mut group = c.benchmark_group("cumulative_100k"); 12 | group.bench_function("f64", |b| { 13 | let vals = Vec::::from_iter((0..N).map(|_| rng.random())); 14 | b.iter(|| { 15 | let mut view = Cumulative::::new(Echo::new(), NonZeroUsize::new(1024).unwrap()); 16 | for v in vals.iter() { 17 | view.update(*v); 18 | let _ = black_box(view.last()); 19 | } 20 | }) 21 | }); 22 | group.bench_function("f32", |b| { 23 | let vals = Vec::::from_iter((0..N).map(|_| rng.random())); 24 | b.iter(|| { 25 | let mut view = Cumulative::::new(Echo::new(), NonZeroUsize::new(1024).unwrap()); 26 | for v in vals.iter() { 27 | view.update(*v); 28 | let _ = black_box(view.last()); 29 | } 30 | }) 31 | }); 32 | } 33 | 34 | criterion_group!(benches, criterion_benchmark); 35 | criterion_main!(benches); 36 | -------------------------------------------------------------------------------- /benches/cyber_cycle.rs: -------------------------------------------------------------------------------- 1 | use std::num::NonZeroUsize; 2 | 3 | use criterion::{black_box, criterion_group, criterion_main, Criterion}; 4 | use rand::{rng, Rng}; 5 | use sliding_features::{pure_functions::Echo, sliding_windows::CyberCycle, View}; 6 | 7 | fn criterion_benchmark(c: &mut Criterion) { 8 | let mut rng = rng(); 9 | const N: usize = 100_000; 10 | 11 | let mut group = c.benchmark_group("cyber_cycle_100k"); 12 | group.bench_function("f64", |b| { 13 | let vals = Vec::::from_iter((0..N).map(|_| rng.random())); 14 | b.iter(|| { 15 | let mut view = CyberCycle::::new(Echo::new(), NonZeroUsize::new(1024).unwrap()); 16 | for v in vals.iter() { 17 | view.update(*v); 18 | let _ = black_box(view.last()); 19 | } 20 | }) 21 | }); 22 | group.bench_function("f32", |b| { 23 | let vals = Vec::::from_iter((0..N).map(|_| rng.random())); 24 | b.iter(|| { 25 | let mut view = CyberCycle::::new(Echo::new(), NonZeroUsize::new(1024).unwrap()); 26 | for v in vals.iter() { 27 | view.update(*v); 28 | let _ = black_box(view.last()); 29 | } 30 | }) 31 | }); 32 | } 33 | 34 | criterion_group!(benches, criterion_benchmark); 35 | criterion_main!(benches); 36 | -------------------------------------------------------------------------------- /benches/ehlers_fisher_transform.rs: -------------------------------------------------------------------------------- 1 | use std::num::NonZeroUsize; 2 | 3 | use criterion::{black_box, criterion_group, criterion_main, Criterion}; 4 | use rand::{rng, rngs::SmallRng, Rng, SeedableRng}; 5 | use sliding_features::{ 6 | pure_functions::Echo, 7 | sliding_windows::{EhlersFisherTransform, Ema}, 8 | View, 9 | }; 10 | use time_series_generator::generate_standard_normal; 11 | 12 | fn criterion_benchmark(c: &mut Criterion) { 13 | let mut rng = rng(); 14 | const N: usize = 100_000; 15 | 16 | let mut group = c.benchmark_group("ehlers_fisher_transform_100k"); 17 | group.bench_function("f64", |b| { 18 | let vals = Vec::::from_iter((0..N).map(|_| rng.random())); 19 | b.iter(|| { 20 | let mut view = EhlersFisherTransform::::new( 21 | Echo::new(), 22 | Ema::new(Echo::new(), NonZeroUsize::new(1024).unwrap()), 23 | NonZeroUsize::new(1024).unwrap(), 24 | ); 25 | for v in vals.iter() { 26 | view.update(*v); 27 | let _ = black_box(view.last()); 28 | } 29 | }) 30 | }); 31 | group.bench_function("f32", |b| { 32 | let vals = Vec::::from_iter((0..N).map(|_| rng.random())); 33 | b.iter(|| { 34 | let mut view = EhlersFisherTransform::::new( 35 | Echo::new(), 36 | Ema::new(Echo::new(), NonZeroUsize::new(1024).unwrap()), 37 | NonZeroUsize::new(1024).unwrap(), 38 | ); 39 | for v in vals.iter() { 40 | view.update(*v); 41 | let _ = black_box(view.last()); 42 | } 43 | }) 44 | }); 45 | 46 | let mut rng = SmallRng::seed_from_u64(0); 47 | group.bench_function("brownian_motion_f64", |b| { 48 | let motion = generate_standard_normal(&mut rng, N, 1000.0); 49 | b.iter(|| { 50 | let mut view = EhlersFisherTransform::::new( 51 | Echo::new(), 52 | Ema::new(Echo::new(), NonZeroUsize::new(1024).unwrap()), 53 | NonZeroUsize::new(1024).unwrap(), 54 | ); 55 | for v in motion.iter() { 56 | view.update(*v); 57 | let _ = black_box(view.last()); 58 | } 59 | }) 60 | }); 61 | group.bench_function("brownian_motion_f32", |b| { 62 | let motion = generate_standard_normal(&mut rng, N, 1000.0); 63 | b.iter(|| { 64 | let mut view = EhlersFisherTransform::::new( 65 | Echo::new(), 66 | Ema::new(Echo::new(), NonZeroUsize::new(1024).unwrap()), 67 | NonZeroUsize::new(1024).unwrap(), 68 | ); 69 | for v in motion.iter() { 70 | view.update(*v); 71 | let _ = black_box(view.last()); 72 | } 73 | }) 74 | }); 75 | } 76 | 77 | criterion_group!(benches, criterion_benchmark); 78 | criterion_main!(benches); 79 | -------------------------------------------------------------------------------- /benches/ema.rs: -------------------------------------------------------------------------------- 1 | use std::num::NonZeroUsize; 2 | 3 | use criterion::{black_box, criterion_group, criterion_main, Criterion}; 4 | use rand::{rng, Rng}; 5 | use sliding_features::{pure_functions::Echo, sliding_windows::Ema, View}; 6 | 7 | fn criterion_benchmark(c: &mut Criterion) { 8 | let mut rng = rng(); 9 | const N: usize = 100_000; 10 | 11 | let mut group = c.benchmark_group("ema_100k"); 12 | group.bench_function("f64", |b| { 13 | let vals = Vec::::from_iter((0..N).map(|_| rng.random())); 14 | b.iter(|| { 15 | let mut view = Ema::::new(Echo::new(), NonZeroUsize::new(1024).unwrap()); 16 | for v in vals.iter() { 17 | view.update(*v); 18 | let _ = black_box(view.last()); 19 | } 20 | }) 21 | }); 22 | group.bench_function("f32", |b| { 23 | let vals = Vec::::from_iter((0..N).map(|_| rng.random())); 24 | b.iter(|| { 25 | let mut view = Ema::::new(Echo::new(), NonZeroUsize::new(1024).unwrap()); 26 | for v in vals.iter() { 27 | view.update(*v); 28 | let _ = black_box(view.last()); 29 | } 30 | }) 31 | }); 32 | } 33 | 34 | criterion_group!(benches, criterion_benchmark); 35 | criterion_main!(benches); 36 | -------------------------------------------------------------------------------- /benches/hl_normalizer.rs: -------------------------------------------------------------------------------- 1 | use std::num::NonZeroUsize; 2 | 3 | use criterion::{black_box, criterion_group, criterion_main, Criterion}; 4 | use rand::{rng, Rng}; 5 | use sliding_features::{pure_functions::Echo, sliding_windows::HLNormalizer, View}; 6 | 7 | fn criterion_benchmark(c: &mut Criterion) { 8 | let mut rng = rng(); 9 | const N: usize = 100_000; 10 | 11 | let mut group = c.benchmark_group("hl_normalizer_100k"); 12 | group.bench_function("f64", |b| { 13 | let vals = Vec::::from_iter((0..N).map(|_| rng.random())); 14 | b.iter(|| { 15 | let mut view = 16 | HLNormalizer::::new(Echo::new(), NonZeroUsize::new(1024).unwrap()); 17 | for v in vals.iter() { 18 | view.update(*v); 19 | let _ = black_box(view.last()); 20 | } 21 | }) 22 | }); 23 | group.bench_function("f32", |b| { 24 | let vals = Vec::::from_iter((0..N).map(|_| rng.random())); 25 | b.iter(|| { 26 | let mut view = 27 | HLNormalizer::::new(Echo::new(), NonZeroUsize::new(1024).unwrap()); 28 | for v in vals.iter() { 29 | view.update(*v); 30 | let _ = black_box(view.last()); 31 | } 32 | }) 33 | }); 34 | } 35 | 36 | criterion_group!(benches, criterion_benchmark); 37 | criterion_main!(benches); 38 | -------------------------------------------------------------------------------- /benches/laguerre_filter.rs: -------------------------------------------------------------------------------- 1 | use criterion::{black_box, criterion_group, criterion_main, Criterion}; 2 | use rand::{rng, Rng}; 3 | use sliding_features::{pure_functions::Echo, sliding_windows::LaguerreFilter, View}; 4 | 5 | fn criterion_benchmark(c: &mut Criterion) { 6 | let mut rng = rng(); 7 | const N: usize = 100_000; 8 | 9 | let mut group = c.benchmark_group("laguerre_filter_100k"); 10 | group.bench_function("f64", |b| { 11 | let vals = Vec::::from_iter((0..N).map(|_| rng.random())); 12 | b.iter(|| { 13 | let mut view = LaguerreFilter::::new(Echo::new(), 0.5); 14 | for v in vals.iter() { 15 | view.update(*v); 16 | let _ = black_box(view.last()); 17 | } 18 | }) 19 | }); 20 | group.bench_function("f32", |b| { 21 | let vals = Vec::::from_iter((0..N).map(|_| rng.random())); 22 | b.iter(|| { 23 | let mut view = LaguerreFilter::::new(Echo::new(), 0.5); 24 | for v in vals.iter() { 25 | view.update(*v); 26 | let _ = black_box(view.last()); 27 | } 28 | }) 29 | }); 30 | } 31 | 32 | criterion_group!(benches, criterion_benchmark); 33 | criterion_main!(benches); 34 | -------------------------------------------------------------------------------- /benches/laguerre_rsi.rs: -------------------------------------------------------------------------------- 1 | use std::num::NonZeroUsize; 2 | 3 | use criterion::{black_box, criterion_group, criterion_main, Criterion}; 4 | use rand::{rng, Rng}; 5 | use sliding_features::{pure_functions::Echo, sliding_windows::LaguerreRSI, View}; 6 | 7 | fn criterion_benchmark(c: &mut Criterion) { 8 | let mut rng = rng(); 9 | const N: usize = 100_000; 10 | 11 | let mut group = c.benchmark_group("laguerre_rsi_100k"); 12 | group.bench_function("f64", |b| { 13 | let vals = Vec::::from_iter((0..N).map(|_| rng.random())); 14 | b.iter(|| { 15 | let mut view = 16 | LaguerreRSI::::new(Echo::new(), NonZeroUsize::new(1024).unwrap()); 17 | for v in vals.iter() { 18 | view.update(*v); 19 | let _ = black_box(view.last()); 20 | } 21 | }) 22 | }); 23 | group.bench_function("f32", |b| { 24 | let vals = Vec::::from_iter((0..N).map(|_| rng.random())); 25 | b.iter(|| { 26 | let mut view = 27 | LaguerreRSI::::new(Echo::new(), NonZeroUsize::new(1024).unwrap()); 28 | for v in vals.iter() { 29 | view.update(*v); 30 | let _ = black_box(view.last()); 31 | } 32 | }) 33 | }); 34 | } 35 | 36 | criterion_group!(benches, criterion_benchmark); 37 | criterion_main!(benches); 38 | -------------------------------------------------------------------------------- /benches/my_rsi.rs: -------------------------------------------------------------------------------- 1 | use std::num::NonZeroUsize; 2 | 3 | use criterion::{black_box, criterion_group, criterion_main, Criterion}; 4 | use rand::{rng, Rng}; 5 | use sliding_features::{pure_functions::Echo, sliding_windows::MyRSI, View}; 6 | 7 | fn criterion_benchmark(c: &mut Criterion) { 8 | let mut rng = rng(); 9 | const N: usize = 100_000; 10 | 11 | let mut group = c.benchmark_group("my_rsi_100k"); 12 | group.bench_function("f64", |b| { 13 | let vals = Vec::::from_iter((0..N).map(|_| rng.random())); 14 | b.iter(|| { 15 | let mut view = MyRSI::::new(Echo::new(), NonZeroUsize::new(1024).unwrap()); 16 | for v in vals.iter() { 17 | view.update(*v); 18 | let _ = black_box(view.last()); 19 | } 20 | }) 21 | }); 22 | group.bench_function("f32", |b| { 23 | let vals = Vec::::from_iter((0..N).map(|_| rng.random())); 24 | b.iter(|| { 25 | let mut view = MyRSI::::new(Echo::new(), NonZeroUsize::new(1024).unwrap()); 26 | for v in vals.iter() { 27 | view.update(*v); 28 | let _ = black_box(view.last()); 29 | } 30 | }) 31 | }); 32 | } 33 | 34 | criterion_group!(benches, criterion_benchmark); 35 | criterion_main!(benches); 36 | -------------------------------------------------------------------------------- /benches/noise_elimination_technology.rs: -------------------------------------------------------------------------------- 1 | use std::num::NonZeroUsize; 2 | 3 | use criterion::{black_box, criterion_group, criterion_main, Criterion}; 4 | use rand::{rng, Rng}; 5 | use sliding_features::{pure_functions::Echo, sliding_windows::NoiseEliminationTechnology, View}; 6 | 7 | fn criterion_benchmark(c: &mut Criterion) { 8 | let mut rng = rng(); 9 | const N: usize = 10_000; 10 | 11 | let mut group = c.benchmark_group("noise_elimination_technology_10k"); 12 | group.bench_function("f64", |b| { 13 | let vals = Vec::::from_iter((0..N).map(|_| rng.random())); 14 | b.iter(|| { 15 | let mut view = NoiseEliminationTechnology::::new( 16 | Echo::new(), 17 | NonZeroUsize::new(1024).unwrap(), 18 | ); 19 | for v in vals.iter() { 20 | view.update(*v); 21 | let _ = black_box(view.last()); 22 | } 23 | }) 24 | }); 25 | group.bench_function("f32", |b| { 26 | let vals = Vec::::from_iter((0..N).map(|_| rng.random())); 27 | b.iter(|| { 28 | let mut view = NoiseEliminationTechnology::::new( 29 | Echo::new(), 30 | NonZeroUsize::new(1024).unwrap(), 31 | ); 32 | for v in vals.iter() { 33 | view.update(*v); 34 | let _ = black_box(view.last()); 35 | } 36 | }) 37 | }); 38 | } 39 | 40 | criterion_group!(benches, criterion_benchmark); 41 | criterion_main!(benches); 42 | -------------------------------------------------------------------------------- /benches/polarized_fractal_efficiency.rs: -------------------------------------------------------------------------------- 1 | use std::num::NonZeroUsize; 2 | 3 | use criterion::{black_box, criterion_group, criterion_main, Criterion}; 4 | use rand::{rng, Rng}; 5 | use sliding_features::{ 6 | pure_functions::Echo, 7 | sliding_windows::{Ema, PolarizedFractalEfficiency}, 8 | View, 9 | }; 10 | 11 | fn criterion_benchmark(c: &mut Criterion) { 12 | let mut rng = rng(); 13 | const N: usize = 100_000; 14 | 15 | let mut group = c.benchmark_group("polarized_fractal_efficiency_100k"); 16 | group.bench_function("f64", |b| { 17 | let vals = Vec::::from_iter((0..N).map(|_| rng.random())); 18 | b.iter(|| { 19 | let mut view = PolarizedFractalEfficiency::::new( 20 | Echo::new(), 21 | Ema::new(Echo::new(), NonZeroUsize::new(1024).unwrap()), 22 | NonZeroUsize::new(1024).unwrap(), 23 | ); 24 | for v in vals.iter() { 25 | view.update(*v); 26 | let _ = black_box(view.last()); 27 | } 28 | }) 29 | }); 30 | group.bench_function("f32", |b| { 31 | let vals = Vec::::from_iter((0..N).map(|_| rng.random())); 32 | b.iter(|| { 33 | let mut view = PolarizedFractalEfficiency::::new( 34 | Echo::new(), 35 | Ema::new(Echo::new(), NonZeroUsize::new(1024).unwrap()), 36 | NonZeroUsize::new(1024).unwrap(), 37 | ); 38 | for v in vals.iter() { 39 | view.update(*v); 40 | let _ = black_box(view.last()); 41 | } 42 | }) 43 | }); 44 | } 45 | 46 | criterion_group!(benches, criterion_benchmark); 47 | criterion_main!(benches); 48 | -------------------------------------------------------------------------------- /benches/re_flex.rs: -------------------------------------------------------------------------------- 1 | use std::num::NonZeroUsize; 2 | 3 | use criterion::{black_box, criterion_group, criterion_main, Criterion}; 4 | use rand::{rng, Rng}; 5 | use sliding_features::{pure_functions::Echo, sliding_windows::ReFlex, View}; 6 | 7 | fn criterion_benchmark(c: &mut Criterion) { 8 | let mut rng = rng(); 9 | const N: usize = 100_000; 10 | 11 | let mut group = c.benchmark_group("re_flex_100k"); 12 | group.bench_function("f64", |b| { 13 | let vals = Vec::::from_iter((0..N).map(|_| rng.random())); 14 | b.iter(|| { 15 | let mut view = ReFlex::::new(Echo::new(), NonZeroUsize::new(1024).unwrap()); 16 | for v in vals.iter() { 17 | view.update(*v); 18 | let _ = black_box(view.last()); 19 | } 20 | }) 21 | }); 22 | group.bench_function("f32", |b| { 23 | let vals = Vec::::from_iter((0..N).map(|_| rng.random())); 24 | b.iter(|| { 25 | let mut view = ReFlex::::new(Echo::new(), NonZeroUsize::new(1024).unwrap()); 26 | for v in vals.iter() { 27 | view.update(*v); 28 | let _ = black_box(view.last()); 29 | } 30 | }) 31 | }); 32 | } 33 | 34 | criterion_group!(benches, criterion_benchmark); 35 | criterion_main!(benches); 36 | -------------------------------------------------------------------------------- /benches/roc.rs: -------------------------------------------------------------------------------- 1 | use std::num::NonZeroUsize; 2 | 3 | use criterion::{black_box, criterion_group, criterion_main, Criterion}; 4 | use rand::{rng, Rng}; 5 | use sliding_features::{pure_functions::Echo, sliding_windows::Roc, View}; 6 | 7 | fn criterion_benchmark(c: &mut Criterion) { 8 | let mut rng = rng(); 9 | const N: usize = 100_000; 10 | 11 | let mut group = c.benchmark_group("roc_100k"); 12 | group.bench_function("f64", |b| { 13 | let vals = Vec::::from_iter((0..N).map(|_| rng.random())); 14 | b.iter(|| { 15 | let mut view = Roc::::new(Echo::new(), NonZeroUsize::new(1024).unwrap()); 16 | for v in vals.iter() { 17 | view.update(*v); 18 | let _ = black_box(view.last()); 19 | } 20 | }) 21 | }); 22 | group.bench_function("f32", |b| { 23 | let vals = Vec::::from_iter((0..N).map(|_| rng.random())); 24 | b.iter(|| { 25 | let mut view = Roc::::new(Echo::new(), NonZeroUsize::new(1024).unwrap()); 26 | for v in vals.iter() { 27 | view.update(*v); 28 | let _ = black_box(view.last()); 29 | } 30 | }) 31 | }); 32 | } 33 | 34 | criterion_group!(benches, criterion_benchmark); 35 | criterion_main!(benches); 36 | -------------------------------------------------------------------------------- /benches/roofing_filter.rs: -------------------------------------------------------------------------------- 1 | use std::num::NonZeroUsize; 2 | 3 | use criterion::{black_box, criterion_group, criterion_main, Criterion}; 4 | use rand::{rng, Rng}; 5 | use sliding_features::{pure_functions::Echo, sliding_windows::RoofingFilter, View}; 6 | 7 | fn criterion_benchmark(c: &mut Criterion) { 8 | let mut rng = rng(); 9 | const N: usize = 100_000; 10 | 11 | let mut group = c.benchmark_group("roofing_filter_100k"); 12 | group.bench_function("f64", |b| { 13 | let vals = Vec::::from_iter((0..N).map(|_| rng.random())); 14 | b.iter(|| { 15 | let mut view = RoofingFilter::::new( 16 | Echo::new(), 17 | NonZeroUsize::new(256).unwrap(), 18 | NonZeroUsize::new(1024).unwrap(), 19 | ); 20 | for v in vals.iter() { 21 | view.update(*v); 22 | let _ = black_box(view.last()); 23 | } 24 | }) 25 | }); 26 | group.bench_function("f32", |b| { 27 | let vals = Vec::::from_iter((0..N).map(|_| rng.random())); 28 | b.iter(|| { 29 | let mut view = RoofingFilter::::new( 30 | Echo::new(), 31 | NonZeroUsize::new(256).unwrap(), 32 | NonZeroUsize::new(1024).unwrap(), 33 | ); 34 | for v in vals.iter() { 35 | view.update(*v); 36 | let _ = black_box(view.last()); 37 | } 38 | }) 39 | }); 40 | } 41 | 42 | criterion_group!(benches, criterion_benchmark); 43 | criterion_main!(benches); 44 | -------------------------------------------------------------------------------- /benches/rsi.rs: -------------------------------------------------------------------------------- 1 | use std::num::NonZeroUsize; 2 | 3 | use criterion::{black_box, criterion_group, criterion_main, Criterion}; 4 | use rand::{rng, Rng}; 5 | use sliding_features::{pure_functions::Echo, sliding_windows::Rsi, View}; 6 | 7 | fn criterion_benchmark(c: &mut Criterion) { 8 | let mut rng = rng(); 9 | const N: usize = 100_000; 10 | 11 | let mut group = c.benchmark_group("rsi_100k"); 12 | group.bench_function("f64", |b| { 13 | let vals = Vec::::from_iter((0..N).map(|_| rng.random())); 14 | b.iter(|| { 15 | let mut view = Rsi::::new(Echo::new(), NonZeroUsize::new(1024).unwrap()); 16 | for v in vals.iter() { 17 | view.update(*v); 18 | let _ = black_box(view.last()); 19 | } 20 | }) 21 | }); 22 | group.bench_function("f32", |b| { 23 | let vals = Vec::::from_iter((0..N).map(|_| rng.random())); 24 | b.iter(|| { 25 | let mut view = Rsi::::new(Echo::new(), NonZeroUsize::new(1024).unwrap()); 26 | for v in vals.iter() { 27 | view.update(*v); 28 | let _ = black_box(view.last()); 29 | } 30 | }) 31 | }); 32 | } 33 | 34 | criterion_group!(benches, criterion_benchmark); 35 | criterion_main!(benches); 36 | -------------------------------------------------------------------------------- /benches/sma.rs: -------------------------------------------------------------------------------- 1 | use std::num::NonZeroUsize; 2 | 3 | use criterion::{black_box, criterion_group, criterion_main, Criterion}; 4 | use rand::{rng, Rng}; 5 | use sliding_features::{pure_functions::Echo, sliding_windows::Sma, View}; 6 | 7 | fn criterion_benchmark(c: &mut Criterion) { 8 | let mut rng = rng(); 9 | const N: usize = 100_000; 10 | 11 | let mut group = c.benchmark_group("sma_100k"); 12 | group.bench_function("f64", |b| { 13 | let vals = Vec::::from_iter((0..N).map(|_| rng.random())); 14 | b.iter(|| { 15 | let mut view = Sma::::new(Echo::new(), NonZeroUsize::new(1024).unwrap()); 16 | for v in vals.iter() { 17 | view.update(*v); 18 | let _ = black_box(view.last()); 19 | } 20 | }) 21 | }); 22 | group.bench_function("f32", |b| { 23 | let vals = Vec::::from_iter((0..N).map(|_| rng.random())); 24 | b.iter(|| { 25 | let mut view = Sma::::new(Echo::new(), NonZeroUsize::new(1024).unwrap()); 26 | for v in vals.iter() { 27 | view.update(*v); 28 | let _ = black_box(view.last()); 29 | } 30 | }) 31 | }); 32 | } 33 | 34 | criterion_group!(benches, criterion_benchmark); 35 | criterion_main!(benches); 36 | -------------------------------------------------------------------------------- /benches/super_smoother.rs: -------------------------------------------------------------------------------- 1 | use std::num::NonZeroUsize; 2 | 3 | use criterion::{black_box, criterion_group, criterion_main, Criterion}; 4 | use rand::{rng, Rng}; 5 | use sliding_features::{pure_functions::Echo, sliding_windows::SuperSmoother, View}; 6 | 7 | fn criterion_benchmark(c: &mut Criterion) { 8 | let mut rng = rng(); 9 | const N: usize = 100_000; 10 | 11 | let mut group = c.benchmark_group("super_smoother_100k"); 12 | group.bench_function("f64", |b| { 13 | let vals = Vec::::from_iter((0..N).map(|_| rng.random())); 14 | b.iter(|| { 15 | let mut view = 16 | SuperSmoother::::new(Echo::new(), NonZeroUsize::new(1024).unwrap()); 17 | for v in vals.iter() { 18 | view.update(*v); 19 | let _ = black_box(view.last()); 20 | } 21 | }) 22 | }); 23 | group.bench_function("f32", |b| { 24 | let vals = Vec::::from_iter((0..N).map(|_| rng.random())); 25 | b.iter(|| { 26 | let mut view = 27 | SuperSmoother::::new(Echo::new(), NonZeroUsize::new(1024).unwrap()); 28 | for v in vals.iter() { 29 | view.update(*v); 30 | let _ = black_box(view.last()); 31 | } 32 | }) 33 | }); 34 | } 35 | 36 | criterion_group!(benches, criterion_benchmark); 37 | criterion_main!(benches); 38 | -------------------------------------------------------------------------------- /benches/trend_flex.rs: -------------------------------------------------------------------------------- 1 | use std::num::NonZeroUsize; 2 | 3 | use criterion::{black_box, criterion_group, criterion_main, Criterion}; 4 | use rand::{rng, Rng}; 5 | use sliding_features::{pure_functions::Echo, sliding_windows::TrendFlex, View}; 6 | 7 | fn criterion_benchmark(c: &mut Criterion) { 8 | let mut rng = rng(); 9 | const N: usize = 100_000; 10 | 11 | let mut group = c.benchmark_group("trend_flex_100k"); 12 | group.bench_function("f64", |b| { 13 | let vals = Vec::::from_iter((0..N).map(|_| rng.random())); 14 | b.iter(|| { 15 | let mut view = TrendFlex::::new(Echo::new(), NonZeroUsize::new(1024).unwrap()); 16 | for v in vals.iter() { 17 | view.update(*v); 18 | let _ = black_box(view.last()); 19 | } 20 | }) 21 | }); 22 | group.bench_function("f32", |b| { 23 | let vals = Vec::::from_iter((0..N).map(|_| rng.random())); 24 | b.iter(|| { 25 | let mut view = TrendFlex::::new(Echo::new(), NonZeroUsize::new(1024).unwrap()); 26 | for v in vals.iter() { 27 | view.update(*v); 28 | let _ = black_box(view.last()); 29 | } 30 | }) 31 | }); 32 | } 33 | 34 | criterion_group!(benches, criterion_benchmark); 35 | criterion_main!(benches); 36 | -------------------------------------------------------------------------------- /benches/vsct.rs: -------------------------------------------------------------------------------- 1 | use std::num::NonZeroUsize; 2 | 3 | use criterion::{black_box, criterion_group, criterion_main, Criterion}; 4 | use rand::{rng, Rng}; 5 | use sliding_features::{pure_functions::Echo, sliding_windows::Vsct, View}; 6 | 7 | fn criterion_benchmark(c: &mut Criterion) { 8 | let mut rng = rng(); 9 | const N: usize = 100_000; 10 | 11 | let mut group = c.benchmark_group("vsct_100k"); 12 | group.bench_function("f64", |b| { 13 | let vals = Vec::::from_iter((0..N).map(|_| rng.random())); 14 | b.iter(|| { 15 | let mut view = Vsct::::new(Echo::new(), NonZeroUsize::new(1024).unwrap()); 16 | for v in vals.iter() { 17 | view.update(*v); 18 | let _ = black_box(view.last()); 19 | } 20 | }) 21 | }); 22 | group.bench_function("f32", |b| { 23 | let vals = Vec::::from_iter((0..N).map(|_| rng.random())); 24 | b.iter(|| { 25 | let mut view = Vsct::::new(Echo::new(), NonZeroUsize::new(1024).unwrap()); 26 | for v in vals.iter() { 27 | view.update(*v); 28 | let _ = black_box(view.last()); 29 | } 30 | }) 31 | }); 32 | } 33 | 34 | criterion_group!(benches, criterion_benchmark); 35 | criterion_main!(benches); 36 | -------------------------------------------------------------------------------- /benches/vst.rs: -------------------------------------------------------------------------------- 1 | use std::num::NonZeroUsize; 2 | 3 | use criterion::{black_box, criterion_group, criterion_main, Criterion}; 4 | use rand::{rng, Rng}; 5 | use sliding_features::{pure_functions::Echo, sliding_windows::Vst, View}; 6 | 7 | fn criterion_benchmark(c: &mut Criterion) { 8 | let mut rng = rng(); 9 | const N: usize = 100_000; 10 | 11 | let mut group = c.benchmark_group("vst_100k"); 12 | group.bench_function("f64", |b| { 13 | let vals = Vec::::from_iter((0..N).map(|_| rng.random())); 14 | b.iter(|| { 15 | let mut view = Vst::::new(Echo::new(), NonZeroUsize::new(1024).unwrap()); 16 | for v in vals.iter() { 17 | view.update(*v); 18 | let _ = black_box(view.last()); 19 | } 20 | }) 21 | }); 22 | group.bench_function("f32", |b| { 23 | let vals = Vec::::from_iter((0..N).map(|_| rng.random())); 24 | b.iter(|| { 25 | let mut view = Vst::::new(Echo::new(), NonZeroUsize::new(1024).unwrap()); 26 | for v in vals.iter() { 27 | view.update(*v); 28 | let _ = black_box(view.last()); 29 | } 30 | }) 31 | }); 32 | } 33 | 34 | criterion_group!(benches, criterion_benchmark); 35 | criterion_main!(benches); 36 | -------------------------------------------------------------------------------- /benches/welford_online.rs: -------------------------------------------------------------------------------- 1 | use std::num::NonZeroUsize; 2 | 3 | use criterion::{black_box, criterion_group, criterion_main, Criterion}; 4 | use rand::{rng, Rng}; 5 | use sliding_features::{pure_functions::Echo, sliding_windows::WelfordOnline, View}; 6 | 7 | fn criterion_benchmark(c: &mut Criterion) { 8 | let mut rng = rng(); 9 | const N: usize = 100_000; 10 | 11 | let mut group = c.benchmark_group("welford_online_100k"); 12 | group.bench_function("f64", |b| { 13 | let vals = Vec::::from_iter((0..N).map(|_| rng.random())); 14 | b.iter(|| { 15 | let mut view = 16 | WelfordOnline::::new(Echo::new(), NonZeroUsize::new(1024).unwrap()); 17 | for v in vals.iter() { 18 | view.update(*v); 19 | let _ = black_box(view.last()); 20 | } 21 | }) 22 | }); 23 | group.bench_function("f32", |b| { 24 | let vals = Vec::::from_iter((0..N).map(|_| rng.random())); 25 | b.iter(|| { 26 | let mut view = 27 | WelfordOnline::::new(Echo::new(), NonZeroUsize::new(1024).unwrap()); 28 | for v in vals.iter() { 29 | view.update(*v); 30 | let _ = black_box(view.last()); 31 | } 32 | }) 33 | }); 34 | } 35 | 36 | criterion_group!(benches, criterion_benchmark); 37 | criterion_main!(benches); 38 | -------------------------------------------------------------------------------- /examples/basic_chainable_view.rs: -------------------------------------------------------------------------------- 1 | /// This Example provides a basic overview of chainable View definitions. 2 | /// Assume you want to first transform your values with a Variance Stabilizing Centering Transform 3 | // and after that, smooth the values with an ALMA 4 | use std::num::NonZeroUsize; 5 | 6 | // import the needed structs, and the View trait 7 | use sliding_features::{ 8 | pure_functions::Echo, 9 | sliding_windows::{Alma, Vsct}, 10 | View, 11 | }; 12 | 13 | fn main() { 14 | // generate random value shifted up by 100.0 and scaled by 20.0, 15 | // a series which is neither centered around 0 nor variance stabilized 16 | let rands: Vec = (0..100) 17 | .map(|_| rand::random::() * 20.0 + 100.0) 18 | .collect(); 19 | println!("rands: {:?}", rands); 20 | 21 | let window_len = NonZeroUsize::new(20).unwrap(); 22 | let mut chain = Alma::new( 23 | // first, define the last function which gets applied in the chain 24 | Vsct::new(Echo::new(), window_len), // Make the first transformation in the chain a VSCT 25 | window_len, 26 | ); 27 | for v in &rands { 28 | // the chain will first call the inner most view, which is Echo. 29 | // after that it will apply the VSCT transform 30 | // and finally apply an Arnaux Legoux moving average 31 | chain.update(*v); 32 | if let Some(last_value) = chain.last() { 33 | println!("transformed value: {}", last_value); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /examples/basic_single_view.rs: -------------------------------------------------------------------------------- 1 | /// Example showing how to use a single View 2 | extern crate time_series_generator; 3 | 4 | use std::num::NonZeroUsize; 5 | 6 | use rand::{rngs::SmallRng, SeedableRng}; 7 | use sliding_features::{pure_functions::Echo, sliding_windows::Rsi, View}; 8 | use time_series_generator::generate_standard_normal; 9 | 10 | fn main() { 11 | let mut rsi = Rsi::new(Echo::new(), NonZeroUsize::new(14).unwrap()); 12 | 13 | // generate dummy values 14 | let mut rng = SmallRng::seed_from_u64(0); 15 | let vals = generate_standard_normal(&mut rng, 1024, 100.0); 16 | for r in &vals { 17 | rsi.update(*r); // update the rsi computation with the newest value 18 | let last = rsi.last(); // get the latest rsi value 19 | println!("last rsi value: {:?}", last); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "inputs": { 5 | "systems": "systems" 6 | }, 7 | "locked": { 8 | "lastModified": 1710146030, 9 | "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", 10 | "owner": "numtide", 11 | "repo": "flake-utils", 12 | "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", 13 | "type": "github" 14 | }, 15 | "original": { 16 | "owner": "numtide", 17 | "repo": "flake-utils", 18 | "type": "github" 19 | } 20 | }, 21 | "nixpkgs": { 22 | "locked": { 23 | "lastModified": 0, 24 | "narHash": "sha256-Vr3Qi346M+8CjedtbyUevIGDZW8LcA1fTG0ugPY/Hic=", 25 | "path": "/nix/store/vdyq3cl640n39sclkc8br4xgd09fdj2w-source", 26 | "type": "path" 27 | }, 28 | "original": { 29 | "id": "nixpkgs", 30 | "type": "indirect" 31 | } 32 | }, 33 | "nixpkgs_2": { 34 | "locked": { 35 | "lastModified": 1736320768, 36 | "narHash": "sha256-nIYdTAiKIGnFNugbomgBJR+Xv5F1ZQU+HfaBqJKroC0=", 37 | "owner": "NixOS", 38 | "repo": "nixpkgs", 39 | "rev": "4bc9c909d9ac828a039f288cf872d16d38185db8", 40 | "type": "github" 41 | }, 42 | "original": { 43 | "owner": "NixOS", 44 | "ref": "nixpkgs-unstable", 45 | "repo": "nixpkgs", 46 | "type": "github" 47 | } 48 | }, 49 | "nixpks": { 50 | "locked": { 51 | "lastModified": 1711703276, 52 | "narHash": "sha256-iMUFArF0WCatKK6RzfUJknjem0H9m4KgorO/p3Dopkk=", 53 | "owner": "NixOS", 54 | "repo": "nixpkgs", 55 | "rev": "d8fe5e6c92d0d190646fb9f1056741a229980089", 56 | "type": "github" 57 | }, 58 | "original": { 59 | "owner": "NixOS", 60 | "ref": "nixos-unstable", 61 | "repo": "nixpkgs", 62 | "type": "github" 63 | } 64 | }, 65 | "root": { 66 | "inputs": { 67 | "flake-utils": "flake-utils", 68 | "nixpkgs": "nixpkgs", 69 | "nixpks": "nixpks", 70 | "rust-overlay": "rust-overlay" 71 | } 72 | }, 73 | "rust-overlay": { 74 | "inputs": { 75 | "nixpkgs": "nixpkgs_2" 76 | }, 77 | "locked": { 78 | "lastModified": 1740969088, 79 | "narHash": "sha256-BajboqzFnDhxVT0SXTDKVJCKtFP96lZXccBlT/43mao=", 80 | "owner": "oxalica", 81 | "repo": "rust-overlay", 82 | "rev": "20fdb02098fdda9a25a2939b975abdd7bc03f62d", 83 | "type": "github" 84 | }, 85 | "original": { 86 | "owner": "oxalica", 87 | "repo": "rust-overlay", 88 | "type": "github" 89 | } 90 | }, 91 | "systems": { 92 | "locked": { 93 | "lastModified": 1681028828, 94 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 95 | "owner": "nix-systems", 96 | "repo": "default", 97 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 98 | "type": "github" 99 | }, 100 | "original": { 101 | "owner": "nix-systems", 102 | "repo": "default", 103 | "type": "github" 104 | } 105 | } 106 | }, 107 | "root": "root", 108 | "version": 7 109 | } 110 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "Flake for sliding_features-rs"; 3 | 4 | inputs = { 5 | nixpks.url = "github:NixOS/nixpkgs/nixos-unstable"; 6 | rust-overlay.url = "github:oxalica/rust-overlay"; 7 | flake-utils.url = "github:numtide/flake-utils"; 8 | }; 9 | 10 | outputs = { 11 | nixpkgs, 12 | rust-overlay, 13 | flake-utils, 14 | ... 15 | }: 16 | flake-utils.lib.eachDefaultSystem ( 17 | system: let 18 | overlays = [(import rust-overlay)]; 19 | pkgs = import nixpkgs { 20 | inherit system overlays; 21 | }; 22 | rust = ( 23 | pkgs.rust-bin.stable.latest.default.override { 24 | extensions = [ 25 | "rust-src" 26 | "rust-analyzer" 27 | "clippy" 28 | ]; 29 | targets = ["x86_64-unknown-linux-gnu"]; 30 | } 31 | ); 32 | in 33 | with pkgs; { 34 | devShells.default = mkShell { 35 | buildInputs = [ 36 | openssl 37 | protobuf 38 | clang 39 | pkg-config 40 | fontconfig 41 | cmake 42 | # Use nightly formatter, but otherwise stable channel 43 | (lib.hiPrio rust-bin.nightly."2024-04-01".rustfmt) 44 | rust 45 | taplo 46 | ]; 47 | }; 48 | } 49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /img/agplv3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MathisWellmann/sliding_features-rs/01c38b0b6b29d79ce34e9c726aae968699c0aef4/img/agplv3.png -------------------------------------------------------------------------------- /img/alma.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MathisWellmann/sliding_features-rs/01c38b0b6b29d79ce34e9c726aae968699c0aef4/img/alma.png -------------------------------------------------------------------------------- /img/center_of_gravity.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MathisWellmann/sliding_features-rs/01c38b0b6b29d79ce34e9c726aae968699c0aef4/img/center_of_gravity.png -------------------------------------------------------------------------------- /img/center_of_gravity_normalized.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MathisWellmann/sliding_features-rs/01c38b0b6b29d79ce34e9c726aae968699c0aef4/img/center_of_gravity_normalized.png -------------------------------------------------------------------------------- /img/correlation_trend_indicator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MathisWellmann/sliding_features-rs/01c38b0b6b29d79ce34e9c726aae968699c0aef4/img/correlation_trend_indicator.png -------------------------------------------------------------------------------- /img/cumulative.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MathisWellmann/sliding_features-rs/01c38b0b6b29d79ce34e9c726aae968699c0aef4/img/cumulative.png -------------------------------------------------------------------------------- /img/cyber_cycle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MathisWellmann/sliding_features-rs/01c38b0b6b29d79ce34e9c726aae968699c0aef4/img/cyber_cycle.png -------------------------------------------------------------------------------- /img/cyber_cycle_normalized.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MathisWellmann/sliding_features-rs/01c38b0b6b29d79ce34e9c726aae968699c0aef4/img/cyber_cycle_normalized.png -------------------------------------------------------------------------------- /img/echo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MathisWellmann/sliding_features-rs/01c38b0b6b29d79ce34e9c726aae968699c0aef4/img/echo.png -------------------------------------------------------------------------------- /img/ehlers_fisher_transform.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MathisWellmann/sliding_features-rs/01c38b0b6b29d79ce34e9c726aae968699c0aef4/img/ehlers_fisher_transform.png -------------------------------------------------------------------------------- /img/ema.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MathisWellmann/sliding_features-rs/01c38b0b6b29d79ce34e9c726aae968699c0aef4/img/ema.png -------------------------------------------------------------------------------- /img/laguerre_filter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MathisWellmann/sliding_features-rs/01c38b0b6b29d79ce34e9c726aae968699c0aef4/img/laguerre_filter.png -------------------------------------------------------------------------------- /img/laguerre_rsi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MathisWellmann/sliding_features-rs/01c38b0b6b29d79ce34e9c726aae968699c0aef4/img/laguerre_rsi.png -------------------------------------------------------------------------------- /img/monero_donations_qrcode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MathisWellmann/sliding_features-rs/01c38b0b6b29d79ce34e9c726aae968699c0aef4/img/monero_donations_qrcode.png -------------------------------------------------------------------------------- /img/my_rsi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MathisWellmann/sliding_features-rs/01c38b0b6b29d79ce34e9c726aae968699c0aef4/img/my_rsi.png -------------------------------------------------------------------------------- /img/net_my_rsi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MathisWellmann/sliding_features-rs/01c38b0b6b29d79ce34e9c726aae968699c0aef4/img/net_my_rsi.png -------------------------------------------------------------------------------- /img/polarized_fractal_efficiency.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MathisWellmann/sliding_features-rs/01c38b0b6b29d79ce34e9c726aae968699c0aef4/img/polarized_fractal_efficiency.png -------------------------------------------------------------------------------- /img/re_flex.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MathisWellmann/sliding_features-rs/01c38b0b6b29d79ce34e9c726aae968699c0aef4/img/re_flex.png -------------------------------------------------------------------------------- /img/re_flex_normalized.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MathisWellmann/sliding_features-rs/01c38b0b6b29d79ce34e9c726aae968699c0aef4/img/re_flex_normalized.png -------------------------------------------------------------------------------- /img/roc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MathisWellmann/sliding_features-rs/01c38b0b6b29d79ce34e9c726aae968699c0aef4/img/roc.png -------------------------------------------------------------------------------- /img/roc_normalized.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MathisWellmann/sliding_features-rs/01c38b0b6b29d79ce34e9c726aae968699c0aef4/img/roc_normalized.png -------------------------------------------------------------------------------- /img/roofing_filter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MathisWellmann/sliding_features-rs/01c38b0b6b29d79ce34e9c726aae968699c0aef4/img/roofing_filter.png -------------------------------------------------------------------------------- /img/rsi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MathisWellmann/sliding_features-rs/01c38b0b6b29d79ce34e9c726aae968699c0aef4/img/rsi.png -------------------------------------------------------------------------------- /img/rsi_normalized.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MathisWellmann/sliding_features-rs/01c38b0b6b29d79ce34e9c726aae968699c0aef4/img/rsi_normalized.png -------------------------------------------------------------------------------- /img/sma.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MathisWellmann/sliding_features-rs/01c38b0b6b29d79ce34e9c726aae968699c0aef4/img/sma.png -------------------------------------------------------------------------------- /img/super_smoother.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MathisWellmann/sliding_features-rs/01c38b0b6b29d79ce34e9c726aae968699c0aef4/img/super_smoother.png -------------------------------------------------------------------------------- /img/trend_flex.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MathisWellmann/sliding_features-rs/01c38b0b6b29d79ce34e9c726aae968699c0aef4/img/trend_flex.png -------------------------------------------------------------------------------- /img/trend_flex_normalized.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MathisWellmann/sliding_features-rs/01c38b0b6b29d79ce34e9c726aae968699c0aef4/img/trend_flex_normalized.png -------------------------------------------------------------------------------- /img/vsct.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MathisWellmann/sliding_features-rs/01c38b0b6b29d79ce34e9c726aae968699c0aef4/img/vsct.png -------------------------------------------------------------------------------- /img/welford_online_sliding.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MathisWellmann/sliding_features-rs/01c38b0b6b29d79ce34e9c726aae968699c0aef4/img/welford_online_sliding.png -------------------------------------------------------------------------------- /mprocs.yaml: -------------------------------------------------------------------------------- 1 | procs: 2 | cargo-check: 3 | shell: "cargo check" 4 | cargo-test: 5 | shell: "cargo test" 6 | cargo-bench: 7 | shell: "cargo bench" 8 | cargo-fmt: 9 | shell: "cargo fmt" 10 | cargo-clippy: 11 | shell: "cargo clippy" 12 | cargo-doc: 13 | shell: "cargo doc" 14 | taplo: 15 | shell: "taplo fmt" 16 | semver-checks: 17 | shell: "cargo semver-checks" 18 | example-basic-single-view: 19 | shell: "cargo run --example basic_single_view" 20 | example-basic-chainable-view: 21 | shell: "cargo run --example basic_chainable_view" 22 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![deny( 2 | missing_docs, 3 | rustdoc::missing_crate_level_docs, 4 | unused_crate_dependencies 5 | )] 6 | #![warn(clippy::all)] 7 | #![doc = include_str!("../README.md")] 8 | 9 | //! The sliding_features crate provides modular, chainable sliding windows 10 | //! for various signal processing function and technical indicators 11 | 12 | pub mod pure_functions; 13 | pub mod rolling; 14 | pub mod sliding_windows; 15 | 16 | #[cfg(test)] 17 | mod plot; 18 | #[cfg(test)] 19 | mod test_data; 20 | 21 | /// The most important Trait, defining methods which each sliding feature needs to implement 22 | pub trait View { 23 | /// Update the state with a new value 24 | fn update(&mut self, val: T); 25 | 26 | /// Return the last value, if `Some`, then its ready. 27 | fn last(&self) -> Option; 28 | } 29 | 30 | #[cfg(test)] 31 | mod tests { 32 | // Used in benchmarks. 33 | #[allow(unused_imports)] 34 | use criterion::*; 35 | } 36 | -------------------------------------------------------------------------------- /src/plot.rs: -------------------------------------------------------------------------------- 1 | use plotters::prelude::*; 2 | 3 | /// prepare_vec returns a 2d vector suitable for plotting and also min, max values of input vector 4 | fn prepare_vec(vals: Vec) -> (Vec<(f64, f64)>, f64, f64) { 5 | assert!(!vals.is_empty()); 6 | let mut out = vec![(0.0, 0.0); vals.len()]; 7 | let mut min = vals[0]; 8 | let mut max = vals[0]; 9 | 10 | for i in 0..vals.len() { 11 | out[i] = (i as f64, vals[i]); 12 | if vals[i] > max { 13 | max = vals[i] 14 | } else if vals[i] < min { 15 | min = vals[i] 16 | } 17 | } 18 | return (out, min, max); 19 | } 20 | 21 | /// Plots the given values in a single plot to filename 22 | /// returns an Error if there has been an error 23 | /// Used for graphing the timeseries 24 | pub(crate) fn plot_values( 25 | vals: Vec, 26 | filename: &str, 27 | ) -> Result<(), Box> { 28 | let (vec2d, min, max) = prepare_vec(vals); 29 | 30 | let root = BitMapBackend::new(filename, (640, 480)).into_drawing_area(); 31 | root.fill(&WHITE)?; 32 | let mut chart = ChartBuilder::on(&root) 33 | .caption(filename, ("sans-serif", 30).into_font()) 34 | .margin(5) 35 | .x_label_area_size(30) 36 | .y_label_area_size(30) 37 | .build_cartesian_2d(0f64..vec2d.len() as f64, min..max)?; 38 | 39 | chart.configure_mesh().draw()?; 40 | 41 | chart 42 | .draw_series(LineSeries::new(vec2d, &RED))? 43 | .label(filename); 44 | 45 | chart 46 | .configure_series_labels() 47 | .background_style(&WHITE.mix(0.8)) 48 | .border_style(&BLACK) 49 | .draw()?; 50 | 51 | Ok(()) 52 | } 53 | -------------------------------------------------------------------------------- /src/pure_functions/add.rs: -------------------------------------------------------------------------------- 1 | use crate::View; 2 | use num::Float; 3 | 4 | /// Add View a to b 5 | #[derive(Debug)] 6 | pub struct Add { 7 | a: A, 8 | b: B, 9 | _marker: std::marker::PhantomData, 10 | } 11 | 12 | impl Add 13 | where 14 | A: View, 15 | B: View, 16 | T: Float, 17 | { 18 | /// Create a new instance with Views a and b 19 | #[inline] 20 | pub fn new(a: A, b: B) -> Self { 21 | Self { 22 | a, 23 | b, 24 | _marker: Default::default(), 25 | } 26 | } 27 | } 28 | 29 | impl View for Add 30 | where 31 | A: View, 32 | B: View, 33 | T: Float, 34 | { 35 | #[inline] 36 | fn update(&mut self, val: T) { 37 | debug_assert!(val.is_finite(), "value must be finite"); 38 | self.a.update(val); 39 | self.b.update(val); 40 | } 41 | 42 | #[inline] 43 | fn last(&self) -> Option { 44 | match (self.a.last(), self.b.last()) { 45 | (Some(a), Some(b)) => { 46 | debug_assert!(a.is_finite(), "value must be finite"); 47 | debug_assert!(b.is_finite(), "value must be finite"); 48 | Some(a + b) 49 | } 50 | (None, None) | (None, Some(_)) | (Some(_), None) => None, 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/pure_functions/constant.rs: -------------------------------------------------------------------------------- 1 | use crate::View; 2 | 3 | /// Provides a float value to other views 4 | #[derive(Default, Debug, Clone)] 5 | pub struct Constant { 6 | val: T, 7 | } 8 | 9 | impl Constant { 10 | /// Create a new instance with the given value 11 | pub fn new(val: T) -> Self { 12 | Self { val } 13 | } 14 | } 15 | 16 | impl View for Constant { 17 | fn update(&mut self, _val: T) {} 18 | 19 | fn last(&self) -> Option { 20 | Some(self.val) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/pure_functions/divide.rs: -------------------------------------------------------------------------------- 1 | use crate::View; 2 | use num::Float; 3 | 4 | /// Divide View a by b 5 | #[derive(Debug, Clone)] 6 | pub struct Divide { 7 | a: A, 8 | b: B, 9 | _marker: std::marker::PhantomData, 10 | } 11 | 12 | impl Divide 13 | where 14 | A: View, 15 | B: View, 16 | T: Float, 17 | { 18 | /// Create a new instance with Views a and b 19 | pub fn new(a: A, b: B) -> Self { 20 | Self { 21 | a, 22 | b, 23 | _marker: std::marker::PhantomData, 24 | } 25 | } 26 | } 27 | 28 | impl View for Divide 29 | where 30 | A: View, 31 | B: View, 32 | T: Float + std::fmt::Debug, 33 | { 34 | fn update(&mut self, val: T) { 35 | debug_assert!(val.is_finite(), "value must be finite"); 36 | self.a.update(val); 37 | self.b.update(val); 38 | } 39 | 40 | fn last(&self) -> Option { 41 | match (self.a.last(), self.b.last()) { 42 | (Some(a), Some(b)) => { 43 | debug_assert!(a.is_finite(), "value must be finite"); 44 | debug_assert!(b.is_finite(), "value must be finite"); 45 | debug_assert_ne!(b, T::zero(), "cannot divide by zero"); 46 | Some(a / b) 47 | } 48 | (None, None) | (None, Some(_)) | (Some(_), None) => None, 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/pure_functions/echo.rs: -------------------------------------------------------------------------------- 1 | //! Echo always return the last value just like an echo 2 | 3 | use crate::View; 4 | 5 | #[derive(Default, Clone, Debug)] 6 | /// Echo always return the last value just like an echo 7 | pub struct Echo { 8 | out: Option, 9 | } 10 | 11 | impl Echo { 12 | /// Create a new Echo View 13 | #[inline(always)] 14 | pub fn new() -> Echo { 15 | Echo { out: None } 16 | } 17 | } 18 | 19 | impl View for Echo { 20 | fn update(&mut self, val: T) { 21 | debug_assert!(val.is_finite(), "value must be finite"); 22 | self.out = Some(val); 23 | } 24 | 25 | fn last(&self) -> Option { 26 | self.out 27 | } 28 | } 29 | 30 | #[cfg(test)] 31 | mod tests { 32 | use super::*; 33 | use crate::plot::plot_values; 34 | use crate::test_data::TEST_DATA; 35 | 36 | #[test] 37 | fn echo_plot() { 38 | let mut echo = Echo::new(); 39 | let mut out: Vec = Vec::new(); 40 | for v in &TEST_DATA { 41 | echo.update(*v); 42 | out.push(echo.last().unwrap()); 43 | } 44 | let filename = "img/echo.png"; 45 | plot_values(out, filename).unwrap(); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/pure_functions/gte.rs: -------------------------------------------------------------------------------- 1 | use crate::View; 2 | use num::Float; 3 | 4 | /// Greater Than or Equal 5 | /// Will allow values >= clipping_point through and clip other values to the clipping point 6 | #[derive(Debug, Clone)] 7 | pub struct GTE { 8 | view: V, 9 | clipping_point: T, 10 | out: Option, 11 | } 12 | 13 | impl GTE 14 | where 15 | V: View, 16 | T: Float, 17 | { 18 | /// Create a new instance with a chained View and a given clipping point 19 | pub fn new(view: V, clipping_point: T) -> Self { 20 | debug_assert!(clipping_point.is_finite(), "value must be finite"); 21 | Self { 22 | view, 23 | clipping_point, 24 | out: None, 25 | } 26 | } 27 | } 28 | 29 | impl View for GTE 30 | where 31 | V: View, 32 | T: Float, 33 | { 34 | fn update(&mut self, val: T) { 35 | debug_assert!(val.is_finite(), "value must be finite"); 36 | self.view.update(val); 37 | let Some(val) = self.view.last() else { return }; 38 | debug_assert!(val.is_finite(), "value must be finite"); 39 | 40 | if val >= self.clipping_point { 41 | self.out = Some(val); 42 | } else { 43 | self.out = Some(self.clipping_point); 44 | } 45 | } 46 | 47 | fn last(&self) -> Option { 48 | self.out 49 | } 50 | } 51 | 52 | #[cfg(test)] 53 | mod tests { 54 | use crate::pure_functions::Echo; 55 | 56 | use super::*; 57 | 58 | #[test] 59 | fn gte() { 60 | let mut gte = GTE::new(Echo::new(), 1.0); 61 | gte.update(2.0); 62 | assert_eq!(gte.last().unwrap(), 2.0); 63 | gte.update(0.5); 64 | assert_eq!(gte.last().unwrap(), 1.0); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/pure_functions/lte.rs: -------------------------------------------------------------------------------- 1 | use crate::View; 2 | use num::Float; 3 | 4 | /// Lower Than or Equal filter, 5 | /// which only allows values lower than the specified clipping point through 6 | #[derive(Debug, Clone)] 7 | pub struct LTE { 8 | view: V, 9 | clipping_value: T, 10 | out: Option, 11 | } 12 | 13 | impl LTE 14 | where 15 | V: View, 16 | T: Float, 17 | { 18 | /// Create a new instance with a chained View 19 | pub fn new(view: V, clipping_value: T) -> Self { 20 | debug_assert!(clipping_value.is_finite(), "value must be finite"); 21 | Self { 22 | view, 23 | clipping_value, 24 | out: None, 25 | } 26 | } 27 | } 28 | 29 | impl View for LTE 30 | where 31 | V: View, 32 | T: Float, 33 | { 34 | fn update(&mut self, val: T) { 35 | debug_assert!(val.is_finite(), "value must be finite"); 36 | self.view.update(val); 37 | let Some(val) = self.view.last() else { return }; 38 | debug_assert!(val.is_finite(), "value must be finite"); 39 | 40 | if val <= self.clipping_value { 41 | self.out = Some(val); 42 | } else { 43 | self.out = Some(self.clipping_value); 44 | } 45 | } 46 | 47 | fn last(&self) -> Option { 48 | self.out 49 | } 50 | } 51 | 52 | #[cfg(test)] 53 | mod tests { 54 | use crate::pure_functions::Echo; 55 | 56 | use super::*; 57 | 58 | #[test] 59 | fn lte() { 60 | let mut lte = LTE::new(Echo::new(), 1.0); 61 | lte.update(0.5); 62 | assert_eq!(lte.last().unwrap(), 0.5); 63 | lte.update(1.5); 64 | assert_eq!(lte.last().unwrap(), 1.0); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/pure_functions/mod.rs: -------------------------------------------------------------------------------- 1 | //! This module contains `View` implementations that act like pure functions. 2 | //! They don't really rely on an internal state, but rather process value from other `View`s 3 | 4 | mod add; 5 | mod constant; 6 | mod divide; 7 | mod echo; 8 | mod gte; 9 | mod lte; 10 | mod multiply; 11 | mod subtract; 12 | mod tanh; 13 | 14 | pub use add::Add; 15 | pub use constant::Constant; 16 | pub use divide::Divide; 17 | pub use echo::Echo; 18 | pub use gte::GTE; 19 | pub use lte::LTE; 20 | pub use multiply::Multiply; 21 | pub use subtract::Subtract; 22 | pub use tanh::Tanh; 23 | -------------------------------------------------------------------------------- /src/pure_functions/multiply.rs: -------------------------------------------------------------------------------- 1 | use crate::View; 2 | use num::Float; 3 | 4 | /// Multiply View a by b 5 | #[derive(Debug, Clone)] 6 | pub struct Multiply { 7 | a: A, 8 | b: B, 9 | _marker: std::marker::PhantomData, 10 | } 11 | 12 | impl Multiply 13 | where 14 | A: View, 15 | B: View, 16 | T: Float, 17 | { 18 | /// Create a new Instance with Views a and b 19 | pub fn new(a: A, b: B) -> Self { 20 | Self { 21 | a, 22 | b, 23 | _marker: std::marker::PhantomData, 24 | } 25 | } 26 | } 27 | 28 | impl View for Multiply 29 | where 30 | A: View, 31 | B: View, 32 | T: Float, 33 | { 34 | fn update(&mut self, val: T) { 35 | debug_assert!(val.is_finite(), "value must be finite"); 36 | self.a.update(val); 37 | self.b.update(val); 38 | } 39 | 40 | fn last(&self) -> Option { 41 | match (self.a.last(), self.b.last()) { 42 | (Some(a), Some(b)) => { 43 | debug_assert!(a.is_finite(), "value must be finite"); 44 | debug_assert!(b.is_finite(), "value must be finite"); 45 | Some(a * b) 46 | } 47 | (None, None) | (None, Some(_)) | (Some(_), None) => None, 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/pure_functions/subtract.rs: -------------------------------------------------------------------------------- 1 | use crate::View; 2 | use num::Float; 3 | 4 | /// Subtract View a from b 5 | #[derive(Debug, Clone)] 6 | pub struct Subtract { 7 | a: A, 8 | b: B, 9 | _marker: std::marker::PhantomData, 10 | } 11 | 12 | impl Subtract 13 | where 14 | A: View, 15 | B: View, 16 | T: Float, 17 | { 18 | /// Create a new instance with Views a and b 19 | pub fn new(a: A, b: B) -> Self { 20 | Self { 21 | a, 22 | b, 23 | _marker: std::marker::PhantomData, 24 | } 25 | } 26 | } 27 | 28 | impl View for Subtract 29 | where 30 | A: View, 31 | B: View, 32 | T: Float, 33 | { 34 | fn update(&mut self, val: T) { 35 | debug_assert!(val.is_finite(), "value must be finite"); 36 | self.a.update(val); 37 | self.b.update(val); 38 | } 39 | 40 | fn last(&self) -> Option { 41 | match (self.a.last(), self.b.last()) { 42 | (Some(a), Some(b)) => { 43 | debug_assert!(a.is_finite(), "value must be finite"); 44 | debug_assert!(b.is_finite(), "value must be finite"); 45 | Some(a - b) 46 | } 47 | (None, None) | (None, Some(_)) | (Some(_), None) => None, 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/pure_functions/tanh.rs: -------------------------------------------------------------------------------- 1 | use crate::View; 2 | use num::Float; 3 | 4 | /// Applies the Tanh function to the output of its View component 5 | #[derive(Debug, Clone)] 6 | pub struct Tanh { 7 | view: V, 8 | _marker: std::marker::PhantomData, 9 | } 10 | 11 | impl Tanh 12 | where 13 | V: View, 14 | T: Float, 15 | { 16 | /// Create a new instance with a chained View 17 | pub fn new(view: V) -> Self { 18 | Self { 19 | view, 20 | _marker: std::marker::PhantomData, 21 | } 22 | } 23 | } 24 | 25 | impl View for Tanh 26 | where 27 | V: View, 28 | T: Float, 29 | { 30 | fn update(&mut self, val: T) { 31 | debug_assert!(val.is_finite(), "value must be finite"); 32 | self.view.update(val); 33 | } 34 | 35 | fn last(&self) -> Option { 36 | self.view.last().map(|v| { 37 | debug_assert!(v.is_finite(), "value must be finite"); 38 | v.tanh() 39 | }) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/rolling/drawdown.rs: -------------------------------------------------------------------------------- 1 | use crate::{pure_functions::Echo, View}; 2 | use num::{Float, Zero}; 3 | 4 | /// Keep track of the current peak to valley. 5 | #[derive(Debug, Clone)] 6 | pub struct Drawdown { 7 | view: V, 8 | max_drawdown: T, 9 | peak: T, 10 | min_after_peak: T, 11 | } 12 | 13 | impl Default for Drawdown> { 14 | fn default() -> Self { 15 | Self::new(Echo::new()) 16 | } 17 | } 18 | 19 | impl Drawdown 20 | where 21 | V: View, 22 | T: Float, 23 | { 24 | /// Create a new instance of `Self` with a chained `View`, so that the `view` will be updated first and its value will be used by `Self`. 25 | pub fn new(view: V) -> Self { 26 | Self { 27 | view, 28 | max_drawdown: T::zero(), 29 | // The highest value observed. 30 | peak: T::min_value(), 31 | // The minimum value observed after the peak. 32 | min_after_peak: T::max_value(), 33 | } 34 | } 35 | } 36 | 37 | impl View for Drawdown 38 | where 39 | V: View, 40 | T: Float, 41 | { 42 | fn update(&mut self, val: T) { 43 | debug_assert!(val.is_finite(), "value must be finite"); 44 | self.view.update(val); 45 | let Some(val) = self.view.last() else { return }; 46 | debug_assert!(val.is_finite(), "value must be finite"); 47 | 48 | if val > self.peak { 49 | self.peak = val; 50 | self.min_after_peak = val; 51 | } 52 | if val < self.min_after_peak { 53 | self.min_after_peak = val; 54 | } 55 | debug_assert!(self.peak != Zero::zero()); 56 | let dd = (self.peak - self.min_after_peak) / self.peak; 57 | if dd > self.max_drawdown { 58 | self.max_drawdown = dd; 59 | } 60 | } 61 | 62 | #[inline(always)] 63 | fn last(&self) -> Option { 64 | debug_assert!(self.max_drawdown.is_finite(), "value must be finite"); 65 | Some(self.max_drawdown) 66 | } 67 | } 68 | 69 | #[cfg(test)] 70 | mod tests { 71 | use super::*; 72 | 73 | #[test] 74 | fn drawdown() { 75 | let mut dd = Drawdown::default(); 76 | dd.update(100.0); 77 | assert_eq!(dd.last().unwrap(), 0.0); 78 | dd.update(80.0); 79 | assert_eq!(dd.last().unwrap(), 0.2); 80 | dd.update(110.0); 81 | assert_eq!(dd.last().unwrap(), 0.2); 82 | dd.update(95.0); 83 | assert_eq!(dd.last().unwrap(), 0.2); 84 | dd.update(87.0); 85 | assert_eq!(dd.last().unwrap(), 0.20909090909090908); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/rolling/ln_return.rs: -------------------------------------------------------------------------------- 1 | use crate::{pure_functions::Echo, View}; 2 | use num::Float; 3 | 4 | /// Computes the natural logarithm and keep track of the last value. 5 | /// Usually applied to price data. 6 | #[derive(Debug, Clone)] 7 | pub struct LnReturn { 8 | view: V, 9 | last_val: T, 10 | current_val: T, 11 | } 12 | 13 | impl Default for LnReturn> { 14 | fn default() -> Self { 15 | Self::new(Echo::new()) 16 | } 17 | } 18 | 19 | impl LnReturn 20 | where 21 | T: Float, 22 | V: View, 23 | { 24 | /// Create a new instance of `Self` with a chained `View`, whose output will be used to feed the ln return computation. 25 | pub fn new(view: V) -> Self { 26 | Self { 27 | view, 28 | last_val: T::zero(), 29 | current_val: T::zero(), 30 | } 31 | } 32 | } 33 | 34 | impl View for LnReturn 35 | where 36 | T: Float, 37 | V: View, 38 | { 39 | fn update(&mut self, val: T) { 40 | debug_assert!(val.is_finite(), "value must be finite"); 41 | self.view.update(val); 42 | let Some(val) = self.view.last() else { return }; 43 | debug_assert!(val.is_finite(), "value must be finite"); 44 | 45 | self.last_val = self.current_val; 46 | self.current_val = val; 47 | } 48 | 49 | fn last(&self) -> Option { 50 | if self.last_val == T::zero() { 51 | return None; 52 | } 53 | 54 | let out = (self.current_val / self.last_val).ln(); 55 | debug_assert!(out.is_finite(), "value must be finite"); 56 | Some(out) 57 | } 58 | } 59 | 60 | #[cfg(test)] 61 | mod tests { 62 | use super::*; 63 | 64 | #[test] 65 | fn ln_return() { 66 | let mut ln_return = LnReturn::new(Echo::new()); 67 | ln_return.update(100.0); 68 | assert!(ln_return.last().is_none()); 69 | ln_return.update(110.0); 70 | assert_eq!(ln_return.last().unwrap(), 0.09531017980432493); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/rolling/mod.rs: -------------------------------------------------------------------------------- 1 | //! This module contains `View` implementations that are updated on a rolling basis, but don't maintain a sliding window with history. 2 | 3 | mod drawdown; 4 | mod ln_return; 5 | mod welford_rolling; 6 | 7 | pub use drawdown::Drawdown; 8 | pub use ln_return::LnReturn; 9 | pub use welford_rolling::WelfordRolling; 10 | -------------------------------------------------------------------------------- /src/rolling/welford_rolling.rs: -------------------------------------------------------------------------------- 1 | //! Welford online algorithm for computing mean and variance on-the-fly 2 | //! over a sliding window 3 | 4 | use crate::{pure_functions::Echo, View}; 5 | use getset::CopyGetters; 6 | use num::Float; 7 | 8 | /// Welford online algorithm for computing mean and variance on-the-fly 9 | /// over a sliding window 10 | #[derive(Debug, Clone, CopyGetters)] 11 | pub struct WelfordRolling { 12 | view: V, 13 | /// The mean of the observed samples 14 | #[getset(get_copy = "pub")] 15 | mean: T, 16 | s: T, 17 | n: usize, 18 | } 19 | 20 | impl Default for WelfordRolling> { 21 | fn default() -> Self { 22 | Self::new(Echo::new()) 23 | } 24 | } 25 | 26 | impl WelfordRolling 27 | where 28 | V: View, 29 | T: Float, 30 | { 31 | /// Create a WelfordOnline struct with a chained View 32 | pub fn new(view: V) -> Self { 33 | Self { 34 | view, 35 | mean: T::zero(), 36 | s: T::zero(), 37 | n: 0, 38 | } 39 | } 40 | 41 | /// Return the variance of the sliding window using biased estimator. 42 | pub fn variance(&self) -> T { 43 | if self.n > 1 { 44 | self.s / T::from(self.n).expect("Can convert") 45 | } else { 46 | T::zero() 47 | } 48 | } 49 | } 50 | 51 | impl View for WelfordRolling 52 | where 53 | V: View, 54 | T: Float, 55 | { 56 | fn update(&mut self, val: T) { 57 | debug_assert!(val.is_finite(), "value must be finite"); 58 | self.view.update(val); 59 | let Some(val) = self.view.last() else { return }; 60 | debug_assert!(val.is_finite(), "value must be finite"); 61 | 62 | self.n += 1; 63 | let old_mean = self.mean; 64 | self.mean = self.mean + (val - old_mean) / T::from(self.n).expect("Can convert"); 65 | self.s = self.s + (val - old_mean) * (val - self.mean); 66 | } 67 | 68 | fn last(&self) -> Option { 69 | if self.n == 0 { 70 | return None; 71 | } 72 | let out = self.variance().sqrt(); 73 | debug_assert!(out.is_finite(), "value must be finite"); 74 | Some(out) 75 | } 76 | } 77 | 78 | #[cfg(test)] 79 | mod tests { 80 | use super::*; 81 | use crate::plot::plot_values; 82 | use crate::test_data::TEST_DATA; 83 | use round::round; 84 | 85 | #[test] 86 | fn welford_online() { 87 | let mut wo = WelfordRolling::default(); 88 | for v in &TEST_DATA { 89 | wo.update(*v); 90 | if let Some(val) = wo.last() { 91 | assert!(!val.is_nan()); 92 | } 93 | } 94 | let w_std_dev = wo.last().expect("Is some"); 95 | 96 | // compute the standard deviation with the regular formula 97 | let avg: f64 = TEST_DATA.iter().sum::() / TEST_DATA.len() as f64; 98 | let std_dev: f64 = ((1.0 / (TEST_DATA.len() as f64)) 99 | * TEST_DATA.iter().map(|v| (v - avg).powi(2)).sum::()) 100 | .sqrt(); 101 | 102 | assert_eq!(round(w_std_dev, 4), round(std_dev, 4)); 103 | } 104 | 105 | #[test] 106 | fn welford_online_plot() { 107 | let mut wo = WelfordRolling::default(); 108 | let mut out: Vec = Vec::new(); 109 | for v in &TEST_DATA { 110 | wo.update(*v); 111 | if let Some(val) = wo.last() { 112 | out.push(val); 113 | } 114 | } 115 | let filename = "img/welford_online_sliding.png"; 116 | plot_values(out, filename).unwrap(); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/sliding_windows/alma.rs: -------------------------------------------------------------------------------- 1 | //! ALMA - Arnaud Legoux Moving Average 2 | //! reference: 3 | 4 | use getset::CopyGetters; 5 | use num::Float; 6 | use std::{collections::VecDeque, num::NonZeroUsize}; 7 | 8 | use crate::View; 9 | 10 | /// ALMA - Arnaud Legoux Moving Average 11 | /// reference: 12 | #[derive(Clone, Debug, CopyGetters)] 13 | pub struct Alma { 14 | view: V, 15 | /// The configured window length. 16 | #[getset(get_copy = "pub")] 17 | window_len: NonZeroUsize, 18 | wtd_sum: T, 19 | cum_wt: T, 20 | m: T, 21 | s: T, 22 | q_vals: VecDeque, 23 | q_wtd: VecDeque, 24 | q_out: VecDeque, 25 | } 26 | 27 | impl Alma 28 | where 29 | V: View, 30 | T: Float, 31 | { 32 | /// Create a new Arnaud Legoux Moving Average with a chained View 33 | /// and a given window length 34 | pub fn new(view: V, window_len: NonZeroUsize) -> Self { 35 | Alma::new_custom( 36 | view, 37 | window_len, 38 | T::from(6.0).expect("Can convert"), 39 | T::from(0.85).expect("Can convert"), 40 | ) 41 | } 42 | 43 | /// Create a Arnaud Legoux Moving Average with custom parameters 44 | pub fn new_custom(view: V, window_len: NonZeroUsize, sigma: T, offset: T) -> Self { 45 | let wl = T::from(window_len.get()).expect("can convert"); 46 | let m = offset * (wl + T::one()); 47 | let s = wl / sigma; 48 | Alma { 49 | view, 50 | window_len, 51 | m, 52 | s, 53 | wtd_sum: T::zero(), 54 | cum_wt: T::zero(), 55 | q_vals: VecDeque::with_capacity(window_len.get()), 56 | q_wtd: VecDeque::with_capacity(window_len.get()), 57 | q_out: VecDeque::with_capacity(window_len.get()), 58 | } 59 | } 60 | } 61 | 62 | impl View for Alma 63 | where 64 | V: View, 65 | T: Float, 66 | { 67 | fn update(&mut self, val: T) { 68 | debug_assert!(val.is_finite(), "value must be finite"); 69 | // first, apply the internal view update 70 | self.view.update(val); 71 | let Some(val) = self.view.last() else { return }; 72 | debug_assert!(val.is_finite(), "value must be finite"); 73 | 74 | if self.q_vals.len() >= self.window_len.get() { 75 | let old_val = self.q_vals.front().unwrap(); 76 | let old_wtd = self.q_wtd.front().unwrap(); 77 | self.wtd_sum = self.wtd_sum - *old_wtd * *old_val; 78 | self.cum_wt = self.cum_wt - *old_wtd; 79 | 80 | self.q_vals.pop_front(); 81 | self.q_wtd.pop_front(); 82 | self.q_out.pop_front(); 83 | } 84 | let count = T::from(self.q_vals.len()).expect("can convert"); 85 | let wtd = (-(count - self.m).powi(2) 86 | / (T::from(2.0).expect("can convert") * self.s * self.s)) 87 | .exp(); 88 | self.wtd_sum = self.wtd_sum + wtd * val; 89 | self.cum_wt = self.cum_wt + wtd; 90 | 91 | self.q_vals.push_back(val); 92 | self.q_wtd.push_back(wtd); 93 | 94 | let ala = self.wtd_sum / self.cum_wt; 95 | debug_assert!(ala.is_finite(), "value must be finite"); 96 | self.q_out.push_back(ala); 97 | } 98 | 99 | fn last(&self) -> Option { 100 | self.q_out.back().copied() 101 | } 102 | } 103 | 104 | #[cfg(test)] 105 | mod tests { 106 | use super::*; 107 | use crate::test_data::TEST_DATA; 108 | use crate::{plot::plot_values, pure_functions::Echo}; 109 | use rand::{rng, Rng}; 110 | 111 | #[test] 112 | fn alma() { 113 | let mut rng = rng(); 114 | let mut alma = Alma::new(Echo::new(), NonZeroUsize::new(16).unwrap()); 115 | for _ in 0..1_000_000 { 116 | let v = rng.random::(); 117 | alma.update(v); 118 | let last = alma.last().unwrap(); 119 | 120 | assert!(last >= 0.0); 121 | assert!(last <= 1.0); 122 | } 123 | } 124 | 125 | #[test] 126 | fn alma_plot() { 127 | let mut alma = Alma::new(Echo::new(), NonZeroUsize::new(16).unwrap()); 128 | let mut out: Vec = Vec::new(); 129 | for v in &TEST_DATA { 130 | alma.update(*v); 131 | out.push(alma.last().unwrap()) 132 | } 133 | let filename = "img/alma.png"; 134 | plot_values(out, filename).unwrap(); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/sliding_windows/binary_entropy.rs: -------------------------------------------------------------------------------- 1 | //! Shannon entropy sliding window over values, 2 | //! where a positive / negative values are interpreted as true / false 3 | 4 | use num::Float; 5 | use std::{collections::VecDeque, num::NonZeroUsize}; 6 | 7 | use crate::View; 8 | 9 | #[derive(Debug, Clone)] 10 | /// Shannon entropy sliding window over values, 11 | /// where a positive / negative values are interpreted as true / false 12 | pub struct BinaryEntropy { 13 | view: V, 14 | window_len: NonZeroUsize, 15 | q_vals: VecDeque, 16 | // number of positive values 17 | p: usize, 18 | } 19 | 20 | impl BinaryEntropy 21 | where 22 | V: View, 23 | T: Float, 24 | { 25 | /// Create a new Entropy Sliding Window 26 | pub fn new(view: V, window_len: NonZeroUsize) -> Self { 27 | Self { 28 | view, 29 | window_len, 30 | q_vals: VecDeque::new(), 31 | p: 0, 32 | } 33 | } 34 | } 35 | 36 | impl View for BinaryEntropy 37 | where 38 | V: View, 39 | T: Float, 40 | { 41 | /// Update the Entropy calculation with a new boolean value 42 | fn update(&mut self, val: T) { 43 | debug_assert!(val.is_finite(), "value must be finite"); 44 | self.view.update(val); 45 | let Some(val) = self.view.last() else { return }; 46 | debug_assert!(val.is_finite(), "value must be finite"); 47 | 48 | if self.q_vals.len() >= self.window_len.get() { 49 | let old_val = self.q_vals.pop_back().unwrap(); 50 | if old_val >= T::zero() { 51 | self.p -= 1; 52 | } 53 | } 54 | if val >= T::zero() { 55 | self.p += 1; 56 | } 57 | self.q_vals.push_front(val); 58 | } 59 | 60 | /// Get the latest entropy value of the sliding window 61 | fn last(&self) -> Option { 62 | if self.q_vals.is_empty() { 63 | return None; 64 | } 65 | let pt = T::from(self.p).expect("can convert") 66 | / T::from(self.q_vals.len()).expect("can convert"); // probability of positive value 67 | let pn = T::one() - pt; // probability of negative value 68 | 69 | let mut value = pt * pt.log2() + pn * pn.log2(); 70 | if value.is_nan() { 71 | value = T::zero() 72 | } 73 | Some(-value) 74 | } 75 | } 76 | 77 | #[cfg(test)] 78 | mod tests { 79 | use crate::pure_functions::Echo; 80 | 81 | use super::*; 82 | 83 | #[test] 84 | fn binary_entropy() { 85 | let vals: Vec = vec![1.0; 10]; 86 | let mut e = BinaryEntropy::new(Echo::new(), NonZeroUsize::new(10).unwrap()); 87 | for v in &vals { 88 | e.update(*v); 89 | let last = e.last().unwrap(); 90 | assert_eq!(last, 0.0); 91 | } 92 | let vals: Vec = vec![1.0; 10]; 93 | let mut e = BinaryEntropy::new(Echo::new(), NonZeroUsize::new(10).unwrap()); 94 | for v in &vals { 95 | e.update(*v); 96 | let last = e.last().unwrap(); 97 | assert_eq!(last, 0.0); 98 | } 99 | 100 | let vals: Vec = vec![-1.0, 1.0, -1.0, 1.0]; 101 | let mut e = BinaryEntropy::new(Echo::new(), NonZeroUsize::new(4).unwrap()); 102 | for v in &vals { 103 | e.update(*v); 104 | let last = e.last().unwrap(); 105 | println!("last: {}", last); 106 | assert!(last >= 0.0); 107 | } 108 | let last = e.last().unwrap(); 109 | assert_eq!(last, 1.0); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/sliding_windows/center_of_gravity.rs: -------------------------------------------------------------------------------- 1 | //! John Ehlers Center of Gravity Indicator 2 | //! from: 3 | 4 | use getset::CopyGetters; 5 | use num::Float; 6 | use std::{collections::VecDeque, num::NonZeroUsize}; 7 | 8 | use crate::View; 9 | 10 | /// John Ehlers Center of Gravity Indicator 11 | /// from: 12 | #[derive(Clone, Debug, CopyGetters)] 13 | pub struct CenterOfGravity { 14 | view: V, 15 | /// The length of the sliding window. 16 | #[getset(get_copy = "pub")] 17 | window_len: NonZeroUsize, 18 | q_vals: VecDeque, 19 | out: Option, 20 | } 21 | 22 | impl CenterOfGravity 23 | where 24 | V: View, 25 | T: Float, 26 | { 27 | /// Create a Center of Gravity Indicator with a chained View 28 | /// and a given sliding window length 29 | pub fn new(view: V, window_len: NonZeroUsize) -> Self { 30 | Self { 31 | view, 32 | window_len, 33 | q_vals: VecDeque::with_capacity(window_len.get()), 34 | out: None, 35 | } 36 | } 37 | } 38 | 39 | impl View for CenterOfGravity 40 | where 41 | V: View, 42 | T: Float, 43 | { 44 | // update receives a new value and updates its internal state 45 | fn update(&mut self, val: T) { 46 | debug_assert!(val.is_finite(), "value must be finite"); 47 | self.view.update(val); 48 | let Some(val) = self.view.last() else { return }; 49 | debug_assert!(val.is_finite(), "value must be finite"); 50 | 51 | if self.q_vals.len() >= self.window_len.get() { 52 | self.q_vals.pop_front(); 53 | } 54 | self.q_vals.push_back(val); 55 | 56 | let mut denom = T::zero(); 57 | let mut num = T::zero(); 58 | let q_len = self.q_vals.len(); 59 | for (i, val) in self.q_vals.iter().enumerate() { 60 | let weight = q_len - i; 61 | num = num + T::from(weight).expect("can convert") * *val; 62 | denom = denom + *val; 63 | } 64 | if denom != T::zero() { 65 | let out = -num / denom 66 | + (T::from(q_len).expect("can convert") + T::one()) 67 | / T::from(2.0).expect("can convert"); 68 | 69 | debug_assert!(out.is_finite(), "value must be finite"); 70 | self.out = Some(out); 71 | } else { 72 | self.out = Some(T::zero()); 73 | } 74 | } 75 | 76 | #[inline(always)] 77 | fn last(&self) -> Option { 78 | self.out 79 | } 80 | } 81 | 82 | #[cfg(test)] 83 | mod tests { 84 | use super::*; 85 | use crate::plot::plot_values; 86 | use crate::pure_functions::Echo; 87 | use crate::test_data::TEST_DATA; 88 | 89 | #[test] 90 | fn center_of_gravity_plot() { 91 | let mut cgo = CenterOfGravity::new(Echo::new(), NonZeroUsize::new(16).unwrap()); 92 | let mut out: Vec = Vec::new(); 93 | for v in &TEST_DATA { 94 | cgo.update(*v); 95 | out.push(cgo.last().unwrap()); 96 | } 97 | let filename = "img/center_of_gravity.png"; 98 | plot_values(out, filename).unwrap(); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/sliding_windows/correlation_trend_indicator.rs: -------------------------------------------------------------------------------- 1 | //! John Ehlers Correlation Trend Indicator 2 | //! from: 3 | 4 | use crate::View; 5 | use getset::CopyGetters; 6 | use num::Float; 7 | use std::{collections::VecDeque, num::NonZeroUsize}; 8 | 9 | /// John Ehlers Correlation Trend Indicator 10 | /// from: 11 | #[derive(Clone, Debug, CopyGetters)] 12 | pub struct CorrelationTrendIndicator { 13 | view: V, 14 | /// The sliding window length. 15 | #[getset(get_copy = "pub")] 16 | window_len: NonZeroUsize, 17 | q_vals: VecDeque, 18 | } 19 | 20 | impl CorrelationTrendIndicator 21 | where 22 | V: View, 23 | T: Float, 24 | { 25 | /// Create a new Correlation Trend Indicator with a chained View 26 | /// and a given sliding window length 27 | #[inline] 28 | pub fn new(view: V, window_len: NonZeroUsize) -> Self { 29 | Self { 30 | view, 31 | window_len, 32 | q_vals: VecDeque::with_capacity(window_len.get()), 33 | } 34 | } 35 | } 36 | 37 | impl View for CorrelationTrendIndicator 38 | where 39 | V: View, 40 | T: Float, 41 | { 42 | fn update(&mut self, val: T) { 43 | debug_assert!(val.is_finite(), "value must be finite"); 44 | self.view.update(val); 45 | let Some(val) = self.view.last() else { return }; 46 | debug_assert!(val.is_finite(), "value must be finite"); 47 | 48 | if self.q_vals.len() >= self.window_len.get() { 49 | let _ = self.q_vals.pop_front().unwrap(); 50 | } 51 | self.q_vals.push_back(val); 52 | } 53 | 54 | fn last(&self) -> Option { 55 | let mut sx = T::zero(); 56 | let mut sy = T::zero(); 57 | let mut sxx = T::zero(); 58 | let mut sxy = T::zero(); 59 | let mut syy = T::zero(); 60 | 61 | for (i, v) in self.q_vals.iter().enumerate() { 62 | let count = T::from(i).expect("can convert"); 63 | sx = sx + *v; 64 | sy = sy + count; 65 | sxx = sxx + v.powi(2); 66 | sxy = sxy + *v * count; 67 | syy = syy + count.powi(2); 68 | } 69 | let window_len = T::from(self.window_len.get()).expect("Can convert"); 70 | if window_len * sxx - sx.powi(2) > T::zero() && window_len * syy - sy.powi(2) > T::zero() { 71 | let out = (window_len * sxy - sx * sy) 72 | / ((window_len * sxx - sx.powi(2)) * (window_len * syy - sy.powi(2))).sqrt(); 73 | debug_assert!(out.is_finite(), "value must be finite"); 74 | return Some(out); 75 | } 76 | Some(T::zero()) 77 | } 78 | } 79 | 80 | #[cfg(test)] 81 | mod tests { 82 | use super::*; 83 | use crate::plot::plot_values; 84 | use crate::pure_functions::Echo; 85 | use crate::test_data::TEST_DATA; 86 | 87 | #[test] 88 | fn correlation_trend_indicator() { 89 | // Test if indicator is bounded in range [-1, 1.0] 90 | let mut cti = CorrelationTrendIndicator::new(Echo::new(), NonZeroUsize::new(10).unwrap()); 91 | for v in &TEST_DATA { 92 | cti.update(*v); 93 | let last = cti.last().unwrap(); 94 | assert!(last <= 1.0); 95 | assert!(last >= -1.0); 96 | } 97 | } 98 | 99 | #[test] 100 | fn correlation_trend_indicator_plot() { 101 | let mut cti = CorrelationTrendIndicator::new(Echo::new(), NonZeroUsize::new(16).unwrap()); 102 | let mut outs: Vec = Vec::new(); 103 | for v in &TEST_DATA { 104 | cti.update(*v); 105 | let last = cti.last().unwrap(); 106 | assert!(last <= 1.0); 107 | assert!(last >= -1.0); 108 | outs.push(last); 109 | } 110 | 111 | let filename = "./img/correlation_trend_indicator.png"; 112 | plot_values(outs, filename).unwrap(); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/sliding_windows/cumulative.rs: -------------------------------------------------------------------------------- 1 | //! Cumulative sliding window 2 | 3 | use getset::CopyGetters; 4 | use num::Float; 5 | use std::{collections::VecDeque, num::NonZeroUsize}; 6 | 7 | use crate::View; 8 | 9 | /// Cumulative Sliding Window with a chained view 10 | #[derive(Debug, Clone, CopyGetters)] 11 | pub struct Cumulative { 12 | view: V, 13 | /// The length of the sliding window. 14 | #[getset(get_copy = "pub")] 15 | window_len: NonZeroUsize, 16 | q_vals: VecDeque, 17 | out: Option, 18 | } 19 | 20 | impl Cumulative 21 | where 22 | V: View, 23 | T: Float, 24 | { 25 | /// Create a new cumulative sliding window with a chained view and a window length 26 | pub fn new(view: V, window_len: NonZeroUsize) -> Self { 27 | Self { 28 | view, 29 | window_len, 30 | q_vals: VecDeque::with_capacity(window_len.get()), 31 | out: None, 32 | } 33 | } 34 | } 35 | 36 | impl View for Cumulative 37 | where 38 | V: View, 39 | T: Float, 40 | { 41 | fn update(&mut self, val: T) { 42 | debug_assert!(val.is_finite(), "value must be finite"); 43 | self.view.update(val); 44 | let Some(val) = self.view.last() else { return }; 45 | debug_assert!(val.is_finite(), "value must be finite"); 46 | 47 | if self.out.is_none() { 48 | self.out = Some(val); 49 | } 50 | 51 | if self.q_vals.len() >= self.window_len.get() { 52 | let old = self.q_vals.pop_front().unwrap(); 53 | let out = self.out.as_mut().expect("Is some at this point"); 54 | *out = *out - old; 55 | } 56 | self.q_vals.push_back(val); 57 | let out = self.out.as_mut().expect("Is some at this point"); 58 | *out = *out + val; 59 | debug_assert!(out.is_finite(), "value must be finite"); 60 | } 61 | 62 | #[inline(always)] 63 | fn last(&self) -> Option { 64 | self.out 65 | } 66 | } 67 | 68 | #[cfg(test)] 69 | mod tests { 70 | use crate::{plot::plot_values, pure_functions::Echo, test_data::TEST_DATA}; 71 | 72 | use super::*; 73 | 74 | #[test] 75 | fn cumulative_plot() { 76 | let mut cum = Cumulative::new(Echo::new(), NonZeroUsize::new(TEST_DATA.len()).unwrap()); 77 | let mut out: Vec = Vec::with_capacity(TEST_DATA.len()); 78 | for v in &TEST_DATA { 79 | cum.update(*v); 80 | out.push(cum.last().unwrap()); 81 | } 82 | let filename = "img/cumulative.png"; 83 | plot_values(out, filename).unwrap(); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/sliding_windows/cyber_cycle.rs: -------------------------------------------------------------------------------- 1 | //! John Ehlers Cyber Cycle Indicator 2 | //! from: 3 | 4 | use getset::CopyGetters; 5 | use num::Float; 6 | use std::{collections::VecDeque, num::NonZeroUsize}; 7 | 8 | use crate::View; 9 | 10 | /// John Ehlers Cyber Cycle Indicator 11 | /// from: 12 | #[derive(Clone, Debug, CopyGetters)] 13 | pub struct CyberCycle { 14 | view: V, 15 | /// The sliding window length 16 | #[getset(get_copy = "pub")] 17 | window_len: NonZeroUsize, 18 | alpha: T, 19 | vals: VecDeque, 20 | out: VecDeque, 21 | // avoid allocation in `update` step by re-using this buffer. 22 | smooth: Vec, 23 | } 24 | 25 | impl CyberCycle 26 | where 27 | V: View, 28 | T: Float, 29 | { 30 | /// Create a new Cyber Cycle Indicator with a chained View 31 | /// and a given window length 32 | #[inline] 33 | pub fn new(view: V, window_len: NonZeroUsize) -> Self { 34 | CyberCycle { 35 | view, 36 | window_len, 37 | alpha: T::from(2.0).expect("can convert") 38 | / (T::from(window_len.get()).expect("can convert") + T::one()), 39 | vals: VecDeque::with_capacity(window_len.get()), 40 | out: VecDeque::with_capacity(window_len.get()), 41 | smooth: vec![T::zero(); window_len.get()], 42 | } 43 | } 44 | } 45 | 46 | impl View for CyberCycle 47 | where 48 | V: View, 49 | T: Float, 50 | { 51 | fn update(&mut self, val: T) { 52 | debug_assert!(val.is_finite(), "value must be finite"); 53 | self.view.update(val); 54 | let Some(val) = self.view.last() else { return }; 55 | debug_assert!(val.is_finite(), "value must be finite"); 56 | 57 | if self.vals.len() >= self.window_len.get() { 58 | self.vals.pop_front(); 59 | self.out.pop_front(); 60 | } 61 | self.vals.push_back(val); 62 | 63 | if self.vals.len() < self.window_len.get() { 64 | self.out.push_back(T::zero()); 65 | return; 66 | } 67 | let last = self.vals.len() - 1; 68 | let two = T::from(2.0).expect("can convert"); 69 | for (i, v) in self 70 | .smooth 71 | .iter_mut() 72 | .enumerate() 73 | .take(self.vals.len()) 74 | .skip(3) 75 | { 76 | *v = (val 77 | + two * *self.vals.get(i - 1).unwrap() 78 | + two * *self.vals.get(i - 2).unwrap() 79 | + *self.vals.get(i - 3).unwrap()) 80 | / T::from(6.0).expect("can convert") 81 | } 82 | let cc = (T::one() - T::from(0.5).expect("can convert") * self.alpha).powi(2) 83 | * (self.smooth[last] - two * self.smooth[last - 1] + self.smooth[last - 2]) 84 | + two * (T::one() - self.alpha) * *self.out.get(last - 1).unwrap() 85 | - (T::one() - self.alpha).powi(2) * *self.out.get(last - 2).unwrap(); 86 | debug_assert!(cc.is_finite(), "value must be finite"); 87 | self.out.push_back(cc); 88 | } 89 | 90 | #[inline(always)] 91 | fn last(&self) -> Option { 92 | self.out.back().copied() 93 | } 94 | } 95 | 96 | #[cfg(test)] 97 | mod tests { 98 | use super::*; 99 | use crate::plot::plot_values; 100 | use crate::pure_functions::Echo; 101 | use crate::test_data::TEST_DATA; 102 | 103 | #[test] 104 | fn cyber_cycle_plot() { 105 | let mut cc = CyberCycle::new(Echo::new(), NonZeroUsize::new(16).unwrap()); 106 | let mut out: Vec = Vec::new(); 107 | for v in &TEST_DATA { 108 | cc.update(*v); 109 | out.push(cc.last().unwrap()); 110 | } 111 | let filename = "img/cyber_cycle.png"; 112 | plot_values(out, filename).unwrap(); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/sliding_windows/ehlers_fisher_transform.rs: -------------------------------------------------------------------------------- 1 | //! John Ehlers Fisher Transform Indicator 2 | //! from: 3 | 4 | use crate::View; 5 | use getset::CopyGetters; 6 | use num::Float; 7 | use std::{cmp::Ordering, collections::VecDeque, num::NonZeroUsize}; 8 | 9 | /// John Ehlers Fisher Transform Indicator 10 | /// from: 11 | #[derive(Clone, Debug, CopyGetters)] 12 | pub struct EhlersFisherTransform { 13 | view: V, 14 | moving_average: M, 15 | /// The sliding window length. 16 | #[getset(get_copy = "pub")] 17 | window_len: NonZeroUsize, 18 | q_vals: VecDeque, 19 | high: T, 20 | low: T, 21 | q_out: VecDeque, 22 | } 23 | 24 | impl EhlersFisherTransform 25 | where 26 | V: View, 27 | M: View, 28 | T: Float, 29 | { 30 | /// Create a new indicator with a view, moving average and window length 31 | #[inline] 32 | pub fn new(view: V, ma: M, window_len: NonZeroUsize) -> Self { 33 | Self { 34 | view, 35 | moving_average: ma, 36 | window_len, 37 | q_vals: VecDeque::with_capacity(window_len.get()), 38 | high: T::zero(), 39 | low: T::zero(), 40 | q_out: VecDeque::with_capacity(window_len.get()), 41 | } 42 | } 43 | } 44 | 45 | impl View for EhlersFisherTransform 46 | where 47 | V: View, 48 | M: View, 49 | T: Float, 50 | { 51 | fn update(&mut self, val: T) { 52 | debug_assert!(val.is_finite(), "value must be finite"); 53 | self.view.update(val); 54 | let Some(val) = self.view.last() else { return }; 55 | debug_assert!(val.is_finite(), "value must be finite"); 56 | 57 | if self.q_vals.is_empty() { 58 | self.high = val; 59 | self.low = val; 60 | } 61 | if self.q_vals.len() >= self.window_len.get() { 62 | let old_val = self.q_vals.pop_front().unwrap(); 63 | // update high and low values if needed 64 | if old_val >= self.high { 65 | // re-compute high 66 | self.high = *self 67 | .q_vals 68 | .iter() 69 | .max_by(|x, y| x.partial_cmp(y).unwrap_or(Ordering::Equal)) 70 | .unwrap(); 71 | } 72 | if old_val <= self.low { 73 | // re-compute low 74 | self.low = *self 75 | .q_vals 76 | .iter() 77 | .min_by(|x, y| x.partial_cmp(y).unwrap_or(Ordering::Equal)) 78 | .unwrap(); 79 | } 80 | } 81 | self.q_vals.push_back(val); 82 | if val > self.high { 83 | self.high = val; 84 | } else if val < self.low { 85 | self.low = val; 86 | } 87 | 88 | if self.high == self.low { 89 | self.q_out.push_back(T::zero()); 90 | return; 91 | } 92 | let half = T::from(0.5).expect("can convert"); 93 | let val = 94 | T::from(2.0).expect("can convert") * ((val - self.low) / (self.high - self.low) - half); 95 | // smooth with moving average 96 | self.moving_average.update(val); 97 | let Some(mut smoothed) = self.moving_average.last() else { 98 | return; 99 | }; 100 | smoothed = smoothed.clamp( 101 | T::from(-0.99).expect("can convert"), 102 | T::from(0.99).expect("can convert"), 103 | ); 104 | 105 | if self.q_out.is_empty() { 106 | // do not insert values when there are not enough values yet 107 | self.q_out.push_back(T::zero()); 108 | return; 109 | } 110 | let fish = half * ((T::one() + smoothed) / (T::one() - smoothed)).ln() 111 | + half * *self.q_out.back().unwrap(); 112 | debug_assert!(fish.is_finite(), "value must be finite"); 113 | self.q_out.push_back(fish); 114 | } 115 | 116 | #[inline(always)] 117 | fn last(&self) -> Option { 118 | self.q_out.back().copied() 119 | } 120 | } 121 | 122 | #[cfg(test)] 123 | mod tests { 124 | use super::*; 125 | use crate::plot::plot_values; 126 | use crate::pure_functions::Echo; 127 | use crate::sliding_windows::Ema; 128 | use crate::test_data::TEST_DATA; 129 | 130 | #[test] 131 | fn ehlers_fisher_transform_plot() { 132 | let mut eft = EhlersFisherTransform::new( 133 | Echo::new(), 134 | Ema::new(Echo::new(), NonZeroUsize::new(16).unwrap()), 135 | NonZeroUsize::new(16).unwrap(), 136 | ); 137 | let mut out: Vec = Vec::new(); 138 | for v in &TEST_DATA { 139 | eft.update(*v); 140 | out.push(eft.last().unwrap()); 141 | } 142 | println!("out: {:?}", out); 143 | let filename = "img/ehlers_fisher_transform.png"; 144 | plot_values(out, filename).unwrap(); 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/sliding_windows/ema.rs: -------------------------------------------------------------------------------- 1 | //! EMA - Exponential Moving Average 2 | 3 | use std::num::NonZeroUsize; 4 | 5 | use crate::View; 6 | use num::Float; 7 | 8 | #[derive(Clone, Debug)] 9 | /// EMA - Exponential Moving Average 10 | pub struct Ema { 11 | view: V, 12 | window_len: usize, 13 | alpha: T, 14 | last_ema: T, 15 | out: T, 16 | n_observed_values: usize, 17 | } 18 | 19 | impl Ema 20 | where 21 | V: View, 22 | T: Float, 23 | { 24 | /// Create a new EMA with a chained view and a given window length 25 | /// and a default alpha value of 2.0 26 | pub fn new(view: V, window_len: NonZeroUsize) -> Self { 27 | Self::with_alpha(view, window_len, T::from(2.0).expect("can convert")) 28 | } 29 | 30 | /// Create a new EMA with a custom alpha as well 31 | pub fn with_alpha(view: V, window_len: NonZeroUsize, alpha: T) -> Self { 32 | Self { 33 | view, 34 | window_len: window_len.get(), 35 | alpha, 36 | last_ema: T::zero(), 37 | out: T::zero(), 38 | n_observed_values: 0, 39 | } 40 | } 41 | } 42 | 43 | impl View for Ema 44 | where 45 | V: View, 46 | T: Float, 47 | { 48 | fn update(&mut self, val: T) { 49 | debug_assert!(val.is_finite(), "value must be finite"); 50 | self.view.update(val); 51 | let Some(val) = self.view.last() else { return }; 52 | debug_assert!(val.is_finite(), "value must be finite"); 53 | 54 | self.n_observed_values += 1; 55 | let weight = self.alpha / (T::one() + T::from(self.window_len).expect("can convert")); 56 | 57 | if self.last_ema == T::zero() { 58 | self.out = val; 59 | self.last_ema = val; 60 | return; 61 | } 62 | 63 | self.out = val * weight + self.last_ema * (T::one() - weight); 64 | self.last_ema = self.out; 65 | } 66 | 67 | fn last(&self) -> Option { 68 | if self.n_observed_values < self.window_len { 69 | return None; 70 | } 71 | debug_assert!(self.out.is_finite(), "value must be finite"); 72 | Some(self.out) 73 | } 74 | } 75 | 76 | #[cfg(test)] 77 | mod tests { 78 | use super::*; 79 | use crate::plot::plot_values; 80 | use crate::pure_functions::Echo; 81 | use crate::test_data::TEST_DATA; 82 | 83 | #[test] 84 | fn ema_plot() { 85 | let mut ema = Ema::new(Echo::new(), NonZeroUsize::new(16).unwrap()); 86 | let mut out: Vec = Vec::new(); 87 | for v in &TEST_DATA { 88 | ema.update(*v); 89 | if let Some(ema) = ema.last() { 90 | out.push(ema); 91 | } 92 | } 93 | let filename = "img/ema.png"; 94 | plot_values(out, filename).unwrap(); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/sliding_windows/hl_normalizer.rs: -------------------------------------------------------------------------------- 1 | //! A sliding High - Low Normalizer 2 | 3 | use getset::CopyGetters; 4 | use num::Float; 5 | use std::{collections::VecDeque, num::NonZeroUsize}; 6 | 7 | use crate::View; 8 | 9 | /// A sliding High - Low Normalizer 10 | #[derive(Clone, Debug, CopyGetters)] 11 | pub struct HLNormalizer { 12 | view: V, 13 | /// The sliding window length 14 | #[getset(get_copy = "pub")] 15 | window_len: NonZeroUsize, 16 | q_vals: VecDeque, 17 | min: T, 18 | max: T, 19 | last: T, 20 | init: bool, 21 | } 22 | 23 | impl HLNormalizer 24 | where 25 | V: View, 26 | T: Float, 27 | { 28 | /// Create a new HLNormalizer with a chained View 29 | /// and a given sliding window length 30 | pub fn new(view: V, window_len: NonZeroUsize) -> Self { 31 | HLNormalizer { 32 | view, 33 | window_len, 34 | q_vals: VecDeque::with_capacity(window_len.get()), 35 | min: T::zero(), 36 | max: T::zero(), 37 | last: T::zero(), 38 | init: true, 39 | } 40 | } 41 | } 42 | 43 | fn extent_queue(q: &VecDeque) -> (T, T) { 44 | let mut min = *q.front().unwrap(); 45 | let mut max = *q.front().unwrap(); 46 | 47 | for i in 0..q.len() { 48 | let val = *q.get(i).unwrap(); 49 | if val > max { 50 | max = val; 51 | } 52 | if val < min { 53 | min = val; 54 | } 55 | } 56 | 57 | (min, max) 58 | } 59 | 60 | impl View for HLNormalizer 61 | where 62 | V: View, 63 | T: Float, 64 | { 65 | fn update(&mut self, val: T) { 66 | debug_assert!(val.is_finite(), "value must be finite"); 67 | self.view.update(val); 68 | let Some(view_last) = self.view.last() else { 69 | return; 70 | }; 71 | debug_assert!(val.is_finite(), "value must be finite"); 72 | 73 | if self.init { 74 | self.init = false; 75 | self.min = view_last; 76 | self.max = view_last; 77 | self.last = view_last; 78 | } 79 | if self.q_vals.len() >= self.window_len.get() { 80 | let old = *self.q_vals.front().unwrap(); 81 | if old <= self.min || old >= self.max { 82 | let (min, max) = extent_queue(&self.q_vals); 83 | self.min = min; 84 | self.max = max; 85 | } 86 | self.q_vals.pop_front(); 87 | } 88 | self.q_vals.push_back(view_last); 89 | if view_last > self.max { 90 | self.max = view_last; 91 | } 92 | if view_last < self.min { 93 | self.min = view_last; 94 | } 95 | self.last = view_last; 96 | } 97 | 98 | fn last(&self) -> Option { 99 | if self.last == self.min && self.last == self.max { 100 | Some(T::zero()) 101 | } else { 102 | let out = -T::one() 103 | + (((self.last - self.min) * T::from(2.0).expect("can convert")) 104 | / (self.max - self.min)); 105 | 106 | debug_assert!(out.is_finite(), "value must be finite"); 107 | Some(out) 108 | } 109 | } 110 | } 111 | 112 | #[cfg(test)] 113 | mod tests { 114 | use super::*; 115 | use crate::{pure_functions::Echo, test_data::TEST_DATA}; 116 | 117 | #[test] 118 | fn normalizer() { 119 | let mut n = HLNormalizer::new(Echo::new(), NonZeroUsize::new(16).unwrap()); 120 | for v in &TEST_DATA { 121 | n.update(*v); 122 | let last = n.last().unwrap(); 123 | assert!(last <= 1.0); 124 | assert!(last >= -1.0); 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/sliding_windows/laguerre_filter.rs: -------------------------------------------------------------------------------- 1 | //! John Ehlers Laguerre Filter 2 | //! from: 3 | 4 | use crate::View; 5 | use num::Float; 6 | 7 | /// John Ehlers Laguerre Filter 8 | /// from: 9 | #[derive(Debug, Clone)] 10 | pub struct LaguerreFilter 11 | where 12 | V: View, 13 | T: Float, 14 | { 15 | view: V, 16 | gamma: T, 17 | // TODO: use `VecDeque` and remove unused values again. 18 | l0s: Vec, 19 | l1s: Vec, 20 | l2s: Vec, 21 | l3s: Vec, 22 | filts: Vec, 23 | } 24 | 25 | impl LaguerreFilter 26 | where 27 | V: View, 28 | T: Float, 29 | { 30 | /// Create a new LaguerreFilter with a chained View 31 | /// and a gamma parameter 32 | pub fn new(view: V, gamma: T) -> Self { 33 | LaguerreFilter { 34 | view, 35 | gamma, 36 | l0s: Vec::new(), 37 | l1s: Vec::new(), 38 | l2s: Vec::new(), 39 | l3s: Vec::new(), 40 | filts: Vec::new(), 41 | } 42 | } 43 | } 44 | 45 | impl View for LaguerreFilter 46 | where 47 | V: View, 48 | T: Float, 49 | { 50 | fn update(&mut self, val: T) { 51 | debug_assert!(val.is_finite(), "value must be finite"); 52 | self.view.update(val); 53 | let Some(val) = self.view.last() else { return }; 54 | debug_assert!(val.is_finite(), "value must be finite"); 55 | 56 | let two = T::from(2.0).expect("can convert"); 57 | if self.l0s.is_empty() { 58 | self.l0s.push(val); 59 | self.l1s.push(val); 60 | self.l2s.push(val); 61 | self.l3s.push(val); 62 | self.filts.push( 63 | (self.l0s[0] + two * self.l1s[0] + two * self.l2s[0] + self.l3s[0]) 64 | / T::from(6.0).expect("can convert"), 65 | ); 66 | return; 67 | } 68 | self.l0s 69 | .push((T::one() - self.gamma) * val + self.gamma * self.l0s[self.l0s.len() - 1]); 70 | self.l1s.push( 71 | -self.gamma * self.l0s[self.l0s.len() - 1] 72 | + self.l0s[self.l0s.len() - 2] 73 | + self.gamma * self.l1s[self.l1s.len() - 1], 74 | ); 75 | self.l2s.push( 76 | -self.gamma * self.l1s[self.l1s.len() - 1] 77 | + self.l1s[self.l1s.len() - 2] 78 | + self.gamma * self.l2s[self.l2s.len() - 1], 79 | ); 80 | self.l3s.push( 81 | -self.gamma * self.l2s[self.l2s.len() - 1] 82 | + self.l2s[self.l2s.len() - 2] 83 | + self.gamma * self.l3s[self.l3s.len() - 1], 84 | ); 85 | let out = (self.l0s[self.l0s.len() - 1] 86 | + two * self.l1s[self.l1s.len() - 1] 87 | + two * self.l2s[self.l2s.len() - 1] 88 | + self.l3s[self.l3s.len() - 1]) 89 | / T::from(6.0).expect("can convert"); 90 | debug_assert!(out.is_finite(), "value must be finite"); 91 | self.filts.push(out); 92 | } 93 | 94 | fn last(&self) -> Option { 95 | self.filts.last().copied() 96 | } 97 | } 98 | 99 | #[cfg(test)] 100 | mod tests { 101 | use super::*; 102 | use crate::test_data::TEST_DATA; 103 | use crate::{plot::plot_values, pure_functions::Echo}; 104 | use rand::{rng, Rng}; 105 | 106 | #[test] 107 | fn laguerre_filter() { 108 | let mut laguerre = LaguerreFilter::new(Echo::new(), 0.8); 109 | let mut rng = rng(); 110 | for _ in 0..10_000 { 111 | let v = rng.random::(); 112 | 113 | laguerre.update(v); 114 | let last = laguerre.last().unwrap(); 115 | 116 | assert!(last <= 1.0); 117 | assert!(last >= 0.0); 118 | } 119 | } 120 | 121 | #[test] 122 | fn laguerre_filter_plot() { 123 | let mut laguerre = LaguerreFilter::new(Echo::new(), 0.8); 124 | let mut out: Vec = Vec::new(); 125 | for v in &TEST_DATA { 126 | laguerre.update(*v); 127 | out.push(laguerre.last().unwrap()); 128 | } 129 | let filename = "img/laguerre_filter.png"; 130 | plot_values(out, filename).unwrap(); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/sliding_windows/laguerre_rsi.rs: -------------------------------------------------------------------------------- 1 | //! John Ehlers LaguerreRSI 2 | //! from: 3 | 4 | use getset::CopyGetters; 5 | use num::Float; 6 | use std::{collections::VecDeque, num::NonZeroUsize}; 7 | 8 | use crate::View; 9 | 10 | /// John Ehlers LaguerreRSI 11 | /// from: 12 | #[derive(Debug, Clone, CopyGetters)] 13 | pub struct LaguerreRSI { 14 | view: V, 15 | value: Option, 16 | gamma: T, 17 | l0s: VecDeque, 18 | l1s: VecDeque, 19 | l2s: VecDeque, 20 | l3s: VecDeque, 21 | /// The sliding window length. 22 | #[getset(get_copy = "pub")] 23 | window_len: NonZeroUsize, 24 | } 25 | 26 | impl LaguerreRSI 27 | where 28 | V: View, 29 | T: Float, 30 | { 31 | /// Create a new LaguerreRSI with a chained View 32 | /// and a given sliding window length 33 | pub fn new(view: V, window_len: NonZeroUsize) -> Self { 34 | LaguerreRSI { 35 | view, 36 | value: None, 37 | gamma: T::from(2.0).expect("can convert") 38 | / (T::from(window_len.get()).expect("can convert") + T::one()), 39 | l0s: VecDeque::with_capacity(window_len.get()), 40 | l1s: VecDeque::with_capacity(window_len.get()), 41 | l2s: VecDeque::with_capacity(window_len.get()), 42 | l3s: VecDeque::with_capacity(window_len.get()), 43 | window_len, 44 | } 45 | } 46 | } 47 | 48 | impl View for LaguerreRSI 49 | where 50 | V: View, 51 | T: Float, 52 | { 53 | fn update(&mut self, val: T) { 54 | debug_assert!(val.is_finite(), "value must be finite"); 55 | self.view.update(val); 56 | let Some(val) = self.view.last() else { return }; 57 | debug_assert!(val.is_finite(), "value must be finite"); 58 | 59 | if self.l0s.len() >= 3 { 60 | self.l0s.pop_front(); 61 | self.l1s.pop_front(); 62 | self.l2s.pop_front(); 63 | self.l3s.pop_front(); 64 | } 65 | 66 | if self.l0s.len() < 2 { 67 | self.l0s.push_back(T::zero()); 68 | self.l1s.push_back(T::zero()); 69 | self.l2s.push_back(T::zero()); 70 | self.l3s.push_back(T::zero()); 71 | return; 72 | } else { 73 | let last = self.l0s.len() - 1; 74 | self.l0s.push_back( 75 | (T::one() - self.gamma) * val + self.gamma * *self.l0s.get(last - 1).unwrap(), 76 | ); 77 | self.l1s.push_back( 78 | -self.gamma * *self.l0s.get(last).unwrap() 79 | + *self.l0s.get(last - 1).unwrap() 80 | + self.gamma * *self.l1s.get(last - 1).unwrap(), 81 | ); 82 | self.l2s.push_back( 83 | -self.gamma * *self.l1s.get(last).unwrap() 84 | + *self.l1s.get(last - 1).unwrap() 85 | + self.gamma * *self.l2s.get(last - 1).unwrap(), 86 | ); 87 | self.l3s.push_back( 88 | -self.gamma * *self.l2s.get(last).unwrap() 89 | + *self.l2s.get(last - 1).unwrap() 90 | + self.gamma * *self.l3s.get(last - 1).unwrap(), 91 | ); 92 | } 93 | let last = self.l0s.len() - 1; 94 | 95 | let mut cu = T::zero(); 96 | let mut cd = T::zero(); 97 | if self.l0s.get(last) >= self.l1s.get(last) { 98 | cu = *self.l0s.get(last).unwrap() - *self.l1s.get(last).unwrap(); 99 | } else { 100 | cd = *self.l1s.get(last).unwrap() - *self.l0s.get(last).unwrap(); 101 | } 102 | if self.l1s.get(last) >= self.l2s.get(last) { 103 | cu = cu + (*self.l1s.get(last).unwrap() - *self.l2s.get(last).unwrap()); 104 | } else { 105 | cd = cd + (*self.l2s.get(last).unwrap() - *self.l1s.get(last).unwrap()); 106 | } 107 | if self.l2s.get(last) >= self.l3s.get(last) { 108 | cu = cu + (*self.l2s.get(last).unwrap() - *self.l3s.get(last).unwrap()); 109 | } else { 110 | cd = cd + (*self.l3s.get(last).unwrap() - *self.l2s.get(last).unwrap()); 111 | } 112 | 113 | if cu + cd != T::zero() { 114 | let value = cu / (cu + cd); 115 | debug_assert!(value.is_finite(), "value must be finite"); 116 | self.value = Some(value); 117 | } 118 | } 119 | 120 | #[inline(always)] 121 | fn last(&self) -> Option { 122 | self.value 123 | } 124 | } 125 | 126 | #[cfg(test)] 127 | mod tests { 128 | use super::*; 129 | use crate::plot::plot_values; 130 | use crate::pure_functions::Echo; 131 | use crate::test_data::TEST_DATA; 132 | 133 | #[test] 134 | fn laguerre_rsi() { 135 | let mut lrsi = LaguerreRSI::new(Echo::new(), NonZeroUsize::new(16).unwrap()); 136 | for v in &TEST_DATA { 137 | lrsi.update(*v); 138 | if let Some(last) = lrsi.last() { 139 | assert!(last <= 1.0); 140 | assert!(last >= -1.0); 141 | } 142 | } 143 | } 144 | 145 | #[test] 146 | fn laguerre_rsi_plot() { 147 | let mut lrsi = LaguerreRSI::new(Echo::new(), NonZeroUsize::new(16).unwrap()); 148 | let mut out: Vec = Vec::new(); 149 | for v in &TEST_DATA { 150 | lrsi.update(*v); 151 | if let Some(val) = lrsi.last() { 152 | out.push(val); 153 | } 154 | } 155 | // graph the results 156 | let filename = "img/laguerre_rsi.png"; 157 | plot_values(out, filename).unwrap(); 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/sliding_windows/max.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::VecDeque, num::NonZeroUsize}; 2 | 3 | use getset::CopyGetters; 4 | use num::Float; 5 | 6 | use crate::View; 7 | 8 | /// Keep track of the maximum value observed over the sliding window. 9 | #[derive(Clone, Debug, CopyGetters)] 10 | pub struct Max { 11 | view: V, 12 | opt_max: Option, 13 | q_vals: VecDeque, 14 | /// The sliding window length. 15 | #[getset(get_copy = "pub")] 16 | window_len: NonZeroUsize, 17 | } 18 | 19 | impl Max 20 | where 21 | T: Float, 22 | V: View, 23 | { 24 | /// Create a new instance with a chained `View` and a sliding window length. 25 | pub fn new(view: V, window_len: NonZeroUsize) -> Self { 26 | Self { 27 | view, 28 | opt_max: None, 29 | q_vals: VecDeque::with_capacity(window_len.get()), 30 | window_len, 31 | } 32 | } 33 | } 34 | 35 | impl View for Max 36 | where 37 | T: Float, 38 | V: View, 39 | { 40 | fn update(&mut self, val: T) { 41 | debug_assert!(val.is_finite(), "value must be finite"); 42 | self.view.update(val); 43 | let Some(val) = self.view.last() else { return }; 44 | debug_assert!(val.is_finite(), "value must be finite"); 45 | 46 | if self.q_vals.len() >= self.window_len.get() { 47 | let popped = self.q_vals.pop_front().expect("There is a value"); 48 | if popped == self.opt_max.expect("Has a minimum value") { 49 | // re-compute the max value. 50 | self.opt_max = self 51 | .q_vals 52 | .iter() 53 | .copied() 54 | .max_by(|a, b| a.partial_cmp(b).expect("Can compare elements")); 55 | } 56 | } 57 | self.q_vals.push_back(val); 58 | if let Some(max) = self.opt_max.as_mut() { 59 | if val > *max { 60 | *max = val; 61 | } 62 | } else { 63 | self.opt_max = Some(val); 64 | } 65 | } 66 | 67 | #[inline(always)] 68 | fn last(&self) -> Option { 69 | self.opt_max 70 | } 71 | } 72 | 73 | #[cfg(test)] 74 | mod test { 75 | use crate::pure_functions::Echo; 76 | 77 | use super::*; 78 | 79 | #[test] 80 | fn max() { 81 | const WINDOW_LEN: usize = 3; 82 | let mut v = Max::new(Echo::new(), NonZeroUsize::new(3).unwrap()); 83 | assert_eq!(v.last(), None); 84 | v.update(1.0); 85 | assert_eq!(v.window_len(), NonZeroUsize::new(WINDOW_LEN).unwrap()); 86 | assert_eq!(v.last(), Some(1.0)); 87 | v.update(2.0); 88 | assert_eq!(v.last(), Some(2.0)); 89 | assert_eq!(v.window_len(), NonZeroUsize::new(WINDOW_LEN).unwrap()); 90 | v.update(0.5); 91 | assert_eq!(v.last(), Some(2.0)); 92 | assert_eq!(v.window_len(), NonZeroUsize::new(WINDOW_LEN).unwrap()); 93 | v.update(1.1); 94 | assert_eq!(v.last(), Some(2.0)); 95 | assert_eq!(v.window_len(), NonZeroUsize::new(WINDOW_LEN).unwrap()); 96 | v.update(1.2); 97 | assert_eq!(v.last(), Some(1.2)); 98 | assert_eq!(v.window_len(), NonZeroUsize::new(WINDOW_LEN).unwrap()); 99 | v.update(1.3); 100 | assert_eq!(v.last(), Some(1.3)); 101 | assert_eq!(v.window_len(), NonZeroUsize::new(WINDOW_LEN).unwrap()); 102 | v.update(1.4); 103 | assert_eq!(v.last(), Some(1.4)); 104 | assert_eq!(v.window_len(), NonZeroUsize::new(WINDOW_LEN).unwrap()); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/sliding_windows/min.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::VecDeque, num::NonZeroUsize}; 2 | 3 | use getset::CopyGetters; 4 | use num::Float; 5 | 6 | use crate::View; 7 | 8 | /// Keep track of the minimum value observed over the sliding window. 9 | #[derive(Clone, Debug, CopyGetters)] 10 | pub struct Min { 11 | view: V, 12 | opt_min: Option, 13 | q_vals: VecDeque, 14 | /// The sliding window length. 15 | #[getset(get_copy = "pub")] 16 | window_len: NonZeroUsize, 17 | } 18 | 19 | impl Min 20 | where 21 | T: Float, 22 | V: View, 23 | { 24 | /// Create a new instance with a chained `View` and a sliding window length. 25 | pub fn new(view: V, window_len: NonZeroUsize) -> Self { 26 | Self { 27 | view, 28 | opt_min: None, 29 | q_vals: VecDeque::with_capacity(window_len.get()), 30 | window_len, 31 | } 32 | } 33 | } 34 | 35 | impl View for Min 36 | where 37 | T: Float, 38 | V: View, 39 | { 40 | fn update(&mut self, val: T) { 41 | debug_assert!(val.is_finite(), "value must be finite"); 42 | self.view.update(val); 43 | let Some(val) = self.view.last() else { return }; 44 | debug_assert!(val.is_finite(), "value must be finite"); 45 | 46 | if self.q_vals.len() >= self.window_len.get() { 47 | let popped = self.q_vals.pop_front().expect("There is a value"); 48 | if popped == self.opt_min.expect("Has a minimum value") { 49 | // re-compute the min value. 50 | self.opt_min = self 51 | .q_vals 52 | .iter() 53 | .copied() 54 | .min_by(|a, b| a.partial_cmp(b).expect("Can compare elements")); 55 | } 56 | } 57 | self.q_vals.push_back(val); 58 | if let Some(min) = self.opt_min.as_mut() { 59 | if val < *min { 60 | *min = val; 61 | } 62 | } else { 63 | self.opt_min = Some(val); 64 | } 65 | } 66 | 67 | #[inline(always)] 68 | fn last(&self) -> Option { 69 | self.opt_min 70 | } 71 | } 72 | 73 | #[cfg(test)] 74 | mod test { 75 | use crate::pure_functions::Echo; 76 | 77 | use super::*; 78 | 79 | #[test] 80 | fn min() { 81 | const WINDOW_LEN: NonZeroUsize = NonZeroUsize::new(3).unwrap(); 82 | let mut v = Min::new(Echo::new(), WINDOW_LEN); 83 | assert_eq!(v.last(), None); 84 | v.update(1.0); 85 | assert_eq!(v.last(), Some(1.0)); 86 | assert_eq!(v.window_len(), WINDOW_LEN); 87 | v.update(2.0); 88 | assert_eq!(v.last(), Some(1.0)); 89 | assert_eq!(v.window_len(), WINDOW_LEN); 90 | v.update(0.5); 91 | assert_eq!(v.last(), Some(0.5)); 92 | assert_eq!(v.window_len(), WINDOW_LEN); 93 | v.update(1.1); 94 | assert_eq!(v.last(), Some(0.5)); 95 | assert_eq!(v.window_len(), WINDOW_LEN); 96 | v.update(1.2); 97 | assert_eq!(v.last(), Some(0.5)); 98 | assert_eq!(v.window_len(), WINDOW_LEN); 99 | v.update(1.3); 100 | assert_eq!(v.last(), Some(1.1)); 101 | assert_eq!(v.window_len(), WINDOW_LEN); 102 | v.update(1.4); 103 | assert_eq!(v.last(), Some(1.2)); 104 | assert_eq!(v.window_len(), WINDOW_LEN); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/sliding_windows/mod.rs: -------------------------------------------------------------------------------- 1 | //! This module contains `View` implementations that act like sliding windows, usually containinig a history of values over the window. 2 | //! Usually more memory intensive than the implementations in `rolling`. 3 | 4 | mod alma; 5 | mod binary_entropy; 6 | mod center_of_gravity; 7 | mod correlation_trend_indicator; 8 | mod cumulative; 9 | mod cyber_cycle; 10 | mod ehlers_fisher_transform; 11 | mod ema; 12 | mod hl_normalizer; 13 | mod laguerre_filter; 14 | mod laguerre_rsi; 15 | mod max; 16 | mod min; 17 | mod my_rsi; 18 | mod noise_elimination_technology; 19 | mod polarized_fractal_efficiency; 20 | mod re_flex; 21 | mod roc; 22 | mod roofing_filter; 23 | mod rsi; 24 | mod sma; 25 | mod super_smoother; 26 | mod trend_flex; 27 | mod variance_stabilizing_transformation; 28 | mod vsct; 29 | mod welford_online; 30 | 31 | pub use alma::Alma; 32 | pub use binary_entropy::BinaryEntropy; 33 | pub use center_of_gravity::CenterOfGravity; 34 | pub use correlation_trend_indicator::CorrelationTrendIndicator; 35 | pub use cumulative::Cumulative; 36 | pub use cyber_cycle::CyberCycle; 37 | pub use ehlers_fisher_transform::EhlersFisherTransform; 38 | pub use ema::Ema; 39 | pub use hl_normalizer::HLNormalizer; 40 | pub use laguerre_filter::LaguerreFilter; 41 | pub use laguerre_rsi::LaguerreRSI; 42 | pub use max::Max; 43 | pub use min::Min; 44 | pub use my_rsi::MyRSI; 45 | pub use noise_elimination_technology::NoiseEliminationTechnology; 46 | pub use polarized_fractal_efficiency::PolarizedFractalEfficiency; 47 | pub use re_flex::ReFlex; 48 | pub use roc::Roc; 49 | pub use roofing_filter::RoofingFilter; 50 | pub use rsi::Rsi; 51 | pub use sma::Sma; 52 | pub use super_smoother::SuperSmoother; 53 | pub use trend_flex::TrendFlex; 54 | pub use variance_stabilizing_transformation::Vst; 55 | pub use vsct::Vsct; 56 | pub use welford_online::WelfordOnline; 57 | -------------------------------------------------------------------------------- /src/sliding_windows/my_rsi.rs: -------------------------------------------------------------------------------- 1 | //! John Ehlers MyRSI 2 | //! from: 3 | 4 | use crate::View; 5 | use getset::CopyGetters; 6 | use num::Float; 7 | use std::{collections::VecDeque, num::NonZeroUsize}; 8 | 9 | /// John Ehlers MyRSI 10 | /// from: 11 | #[derive(Debug, Clone, CopyGetters)] 12 | pub struct MyRSI { 13 | view: V, 14 | /// The sliding window length. 15 | #[getset(get_copy = "pub")] 16 | window_len: NonZeroUsize, 17 | cu: T, 18 | cd: T, 19 | out: T, 20 | q_vals: VecDeque, 21 | last_val: T, 22 | oldest_val: T, 23 | } 24 | 25 | impl MyRSI 26 | where 27 | V: View, 28 | T: Float, 29 | { 30 | /// Create a new MyRSI indicator with a chained View and a given window length 31 | pub fn new(view: V, window_len: NonZeroUsize) -> Self { 32 | MyRSI { 33 | view, 34 | window_len, 35 | cu: T::zero(), 36 | cd: T::zero(), 37 | out: T::zero(), 38 | q_vals: VecDeque::with_capacity(window_len.get()), 39 | last_val: T::zero(), 40 | oldest_val: T::zero(), 41 | } 42 | } 43 | } 44 | 45 | impl View for MyRSI 46 | where 47 | V: View, 48 | T: Float, 49 | { 50 | fn update(&mut self, val: T) { 51 | debug_assert!(val.is_finite(), "value must be finite"); 52 | self.view.update(val); 53 | let Some(val) = self.view.last() else { return }; 54 | debug_assert!(val.is_finite(), "value must be finite"); 55 | 56 | if self.q_vals.is_empty() { 57 | self.oldest_val = val; 58 | self.last_val = val; 59 | } 60 | if self.q_vals.len() >= self.window_len.get() { 61 | let old_val = self.q_vals.pop_front().unwrap(); 62 | if old_val > self.oldest_val { 63 | self.cu = self.cu - (old_val - self.oldest_val); 64 | } else { 65 | self.cd = self.cd - (self.oldest_val - old_val); 66 | } 67 | self.oldest_val = old_val; 68 | } 69 | self.q_vals.push_back(val); 70 | 71 | // accumulate 'closes up' and 'closes down' 72 | if val > self.last_val { 73 | self.cu = self.cu + val - self.last_val; 74 | } else { 75 | self.cd = self.cd + self.last_val - val; 76 | } 77 | self.last_val = val; 78 | 79 | if self.cu + self.cd != T::zero() { 80 | self.out = (self.cu - self.cd) / (self.cu + self.cd); 81 | } 82 | } 83 | 84 | #[inline] 85 | fn last(&self) -> Option { 86 | if self.q_vals.len() < self.window_len.get() { 87 | return None; 88 | } 89 | debug_assert!(self.out.is_finite(), "value must be finite"); 90 | Some(self.out) 91 | } 92 | } 93 | 94 | #[cfg(test)] 95 | mod tests { 96 | use super::*; 97 | use crate::plot::plot_values; 98 | use crate::pure_functions::Echo; 99 | use crate::test_data::TEST_DATA; 100 | 101 | #[test] 102 | fn my_rsi() { 103 | // TODO: don't be so lazy with this test. 104 | let mut my_rsi = MyRSI::::new(Echo::new(), NonZeroUsize::new(16).unwrap()); 105 | for v in &TEST_DATA { 106 | my_rsi.update(*v); 107 | if let Some(val) = my_rsi.last() { 108 | dbg!(val); 109 | assert!(val <= 1.0); 110 | assert!(val >= -1.0); 111 | } 112 | } 113 | } 114 | 115 | #[test] 116 | fn my_rsi_plot() { 117 | let mut my_rsi = MyRSI::new(Echo::new(), NonZeroUsize::new(16).unwrap()); 118 | let mut out: Vec = Vec::new(); 119 | for v in &TEST_DATA { 120 | my_rsi.update(*v); 121 | if let Some(rsi) = my_rsi.last() { 122 | out.push(rsi); 123 | } 124 | } 125 | println!("out: {:?}", out); 126 | let filename = "img/my_rsi.png"; 127 | plot_values(out, filename).unwrap(); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/sliding_windows/noise_elimination_technology.rs: -------------------------------------------------------------------------------- 1 | //! John Ehlers Noise elimination technology using kendall correlation 2 | //! from 3 | 4 | use crate::View; 5 | use getset::CopyGetters; 6 | use num::Float; 7 | use std::{collections::VecDeque, num::NonZeroUsize}; 8 | 9 | /// John Ehlers Noise elimination technology using kendall correlation 10 | /// from 11 | #[derive(Debug, Clone, CopyGetters)] 12 | pub struct NoiseEliminationTechnology { 13 | view: V, 14 | /// The sliding window length. 15 | #[getset(get_copy = "pub")] 16 | window_len: NonZeroUsize, 17 | out: Option, 18 | q_vals: VecDeque, 19 | } 20 | 21 | impl NoiseEliminationTechnology 22 | where 23 | V: View, 24 | T: Float, 25 | { 26 | /// Create a new NET with a chained View and window length 27 | pub fn new(view: V, window_len: NonZeroUsize) -> Self { 28 | NoiseEliminationTechnology { 29 | view, 30 | window_len, 31 | out: None, 32 | q_vals: VecDeque::with_capacity(window_len.get()), 33 | } 34 | } 35 | } 36 | 37 | impl View for NoiseEliminationTechnology 38 | where 39 | V: View, 40 | T: Float, 41 | { 42 | fn update(&mut self, val: T) { 43 | debug_assert!(val.is_finite(), "value must be finite"); 44 | self.view.update(val); 45 | let Some(val) = self.view.last() else { return }; 46 | debug_assert!(val.is_finite(), "value must be finite"); 47 | 48 | if self.q_vals.len() >= self.window_len.get() { 49 | self.q_vals.pop_front(); 50 | } 51 | self.q_vals.push_back(val); 52 | 53 | if self.q_vals.len() < 2 { 54 | return; 55 | } 56 | let mut x: Vec = vec![T::zero(); self.q_vals.len()]; 57 | let mut y: Vec = vec![T::zero(); self.q_vals.len()]; 58 | for count in 1..self.q_vals.len() { 59 | x[count] = *self.q_vals.get(self.q_vals.len() - count).unwrap(); 60 | y[count] = -T::from(count).expect("can convert"); 61 | } 62 | 63 | let mut num = T::zero(); 64 | for count in 2..self.q_vals.len() { 65 | for k in 1..count - 1 { 66 | num = num - ((x[count] - x[k]).signum()); 67 | } 68 | } 69 | 70 | let n = T::from(self.q_vals.len()).expect("can convert"); 71 | let denom = T::from(0.5).expect("can convert") * n * (n - T::one()); 72 | let out = num / denom; 73 | debug_assert!(out.is_finite(), "value must be finite"); 74 | self.out = Some(out) 75 | } 76 | 77 | #[inline(always)] 78 | fn last(&self) -> Option { 79 | self.out 80 | } 81 | } 82 | 83 | #[cfg(test)] 84 | mod tests { 85 | use super::*; 86 | use crate::plot::plot_values; 87 | use crate::pure_functions::Echo; 88 | use crate::sliding_windows::MyRSI; 89 | use crate::test_data::TEST_DATA; 90 | 91 | #[test] 92 | fn net_my_rsi_plot() { 93 | let mut net = NoiseEliminationTechnology::new( 94 | MyRSI::new(Echo::new(), NonZeroUsize::new(16).unwrap()), 95 | NonZeroUsize::new(16).unwrap(), 96 | ); 97 | let mut out: Vec = Vec::new(); 98 | for v in &TEST_DATA { 99 | net.update(*v); 100 | if let Some(val) = net.last() { 101 | out.push(val); 102 | } 103 | } 104 | println!("out: {:?}", out); 105 | 106 | let filename = "img/net_my_rsi.png"; 107 | plot_values(out, filename).unwrap(); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/sliding_windows/polarized_fractal_efficiency.rs: -------------------------------------------------------------------------------- 1 | //! A PolarizedFractalEfficiency indicator with output range [-1.0 and 1.0] rather than [-100, 100] 2 | //! it is also possible to use a custom moving average instead of the default EMA in the original 3 | 4 | use crate::View; 5 | use getset::CopyGetters; 6 | use num::Float; 7 | use std::{collections::VecDeque, num::NonZeroUsize}; 8 | 9 | #[derive(Debug, Clone, CopyGetters)] 10 | /// A PolarizedFractalEfficiency indicator with output range [-1.0 and 1.0] rather than [-100, 100] 11 | /// it is also possible to use a custom moving average instead of the default EMA in the original 12 | pub struct PolarizedFractalEfficiency { 13 | view: V, 14 | moving_average: M, // defines which moving average to use, default is EMA 15 | /// The sliding window length 16 | #[getset(get_copy = "pub")] 17 | window_len: NonZeroUsize, 18 | q_vals: VecDeque, 19 | out: Option, 20 | } 21 | 22 | impl PolarizedFractalEfficiency 23 | where 24 | V: View, 25 | M: View, 26 | T: Float, 27 | { 28 | /// Create a new PolarizedFractalEfficiency indicator with a chained view, custom moving 29 | /// average and a window length 30 | pub fn new(view: V, moving_average: M, window_len: NonZeroUsize) -> Self { 31 | Self { 32 | view, 33 | moving_average, 34 | window_len, 35 | q_vals: VecDeque::with_capacity(window_len.get()), 36 | out: None, 37 | } 38 | } 39 | } 40 | 41 | impl View for PolarizedFractalEfficiency 42 | where 43 | V: View, 44 | M: View, 45 | T: Float, 46 | { 47 | fn update(&mut self, val: T) { 48 | debug_assert!(val.is_finite(), "value must be finite"); 49 | self.view.update(val); 50 | let Some(val) = self.view.last() else { return }; 51 | debug_assert!(val.is_finite(), "value must be finite"); 52 | 53 | if self.q_vals.len() >= self.window_len.get() { 54 | self.q_vals.pop_front(); 55 | } 56 | self.q_vals.push_back(val); 57 | 58 | let window_len = T::from(self.window_len.get()).expect("can convert"); 59 | if self.q_vals.len() >= self.window_len.get() { 60 | let mut s = T::zero(); 61 | let wl: usize = self.window_len.get() - 1; 62 | for i in 0..self.window_len.get() - 2 { 63 | let v_0 = *self.q_vals.get(wl - i).unwrap(); 64 | let v_1 = *self.q_vals.get(wl - i - 1).unwrap(); 65 | s = s + ((v_0 - v_1).powi(2) + T::one()).sqrt(); 66 | } 67 | let mut p = 68 | ((val - *self.q_vals.front().unwrap()).powi(2) + window_len.powi(2)).sqrt() / s; 69 | if val < *self.q_vals.get(self.window_len.get() - 2).unwrap() { 70 | p = -p; 71 | } 72 | // apply a moving average 73 | self.moving_average.update(p); 74 | self.out = self.moving_average.last(); 75 | } 76 | } 77 | 78 | #[inline(always)] 79 | fn last(&self) -> Option { 80 | self.out.map(|v| { 81 | debug_assert!(v.is_finite(), "value must be finite"); 82 | v 83 | }) 84 | } 85 | } 86 | 87 | #[cfg(test)] 88 | mod tests { 89 | use super::*; 90 | use crate::plot::plot_values; 91 | use crate::pure_functions::Echo; 92 | use crate::sliding_windows::Ema; 93 | use crate::test_data::TEST_DATA; 94 | 95 | #[test] 96 | fn polarized_fractal_efficiency() { 97 | // TODO: don't be so lazy with this test. maybe compare against a reference implementation. 98 | let mut pfe = PolarizedFractalEfficiency::new( 99 | Echo::new(), 100 | Ema::new(Echo::new(), NonZeroUsize::new(16).unwrap()), 101 | NonZeroUsize::new(16).unwrap(), 102 | ); 103 | for v in &TEST_DATA { 104 | pfe.update(*v); 105 | if let Some(val) = pfe.last() { 106 | assert!(val <= 1.0); 107 | assert!(val >= -1.0); 108 | } 109 | } 110 | } 111 | 112 | #[test] 113 | fn polarized_fractal_efficiency_plot() { 114 | let mut pfe = PolarizedFractalEfficiency::new( 115 | Echo::new(), 116 | Ema::new(Echo::new(), NonZeroUsize::new(16).unwrap()), 117 | NonZeroUsize::new(16).unwrap(), 118 | ); 119 | let mut out: Vec = Vec::new(); 120 | for v in &TEST_DATA { 121 | pfe.update(*v); 122 | if let Some(val) = pfe.last() { 123 | out.push(val); 124 | } 125 | } 126 | let filename = "img/polarized_fractal_efficiency.png"; 127 | plot_values(out, filename).unwrap(); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/sliding_windows/re_flex.rs: -------------------------------------------------------------------------------- 1 | //! John Ehlers ReFlex Indicator 2 | //! from: 3 | 4 | use getset::CopyGetters; 5 | use num::Float; 6 | use std::{collections::VecDeque, num::NonZeroUsize}; 7 | 8 | use crate::View; 9 | 10 | /// John Ehlers ReFlex Indicator 11 | /// from: 12 | #[derive(Debug, Clone, CopyGetters)] 13 | pub struct ReFlex { 14 | view: V, 15 | /// The sliding window length 16 | #[getset(get_copy = "pub")] 17 | window_len: NonZeroUsize, 18 | last_val: T, 19 | last_m: T, 20 | q_vals: VecDeque, 21 | out: Option, 22 | } 23 | 24 | impl ReFlex 25 | where 26 | V: View, 27 | T: Float, 28 | { 29 | /// Create a new ReFlex indicator with a chained View 30 | /// and a given sliding window length 31 | pub fn new(view: V, window_len: NonZeroUsize) -> Self { 32 | ReFlex { 33 | view, 34 | window_len, 35 | last_val: T::zero(), 36 | last_m: T::zero(), 37 | q_vals: VecDeque::with_capacity(window_len.get()), 38 | out: None, 39 | } 40 | } 41 | } 42 | 43 | impl View for ReFlex 44 | where 45 | V: View, 46 | T: Float, 47 | { 48 | fn update(&mut self, val: T) { 49 | debug_assert!(val.is_finite(), "value must be finite"); 50 | self.view.update(val); 51 | let Some(val) = self.view.last() else { return }; 52 | debug_assert!(val.is_finite(), "value must be finite"); 53 | 54 | if self.q_vals.is_empty() { 55 | self.last_val = val; 56 | } 57 | if self.q_vals.len() >= self.window_len.get() { 58 | self.q_vals.pop_front(); 59 | } 60 | let window_len = T::from(self.window_len.get()).expect("can convert"); 61 | let two = T::from(2.0).expect("can convert"); 62 | let a1 = T::from(8.88442402435).expect("can convert") / window_len; 63 | let b1 = two * a1 * (T::from(4.44221201218).expect("can convert") / window_len).cos(); 64 | let c3 = -a1 * a1; 65 | let c1 = T::one() - b1 - c3; 66 | 67 | let l = self.q_vals.len(); 68 | let mut filt = T::zero(); 69 | if l == 0 { 70 | filt = c1 * (val + self.last_val) / two; 71 | } else if l == 1 { 72 | let filt1 = *self.q_vals.get(l - 1).unwrap(); 73 | filt = c1 * (val + self.last_val) / two + b1 * filt1; 74 | } else if l > 1 { 75 | let filt2 = *self.q_vals.get(l - 2).unwrap(); 76 | let filt1 = *self.q_vals.get(l - 1).unwrap(); 77 | filt = c1 * (val + self.last_val) / two + b1 * filt1 + c3 * filt2; 78 | } 79 | self.last_val = val; 80 | self.q_vals.push_back(filt); 81 | 82 | let slope = (*self.q_vals.front().unwrap() - filt) / window_len; 83 | 84 | // sum the differences 85 | let mut d_sum = T::zero(); 86 | for i in 0..self.q_vals.len() { 87 | let index = self.q_vals.len() - 1 - i; 88 | d_sum = d_sum 89 | + ((filt + T::from(i).expect("can convert") * slope) 90 | - *self.q_vals.get(index).unwrap()); 91 | } 92 | d_sum = d_sum / window_len; 93 | 94 | // normalize in termsn of standard deviation 95 | let ms0 = T::from(0.04).expect("can convert") * d_sum.powi(2) 96 | + T::from(0.96).expect("can convert") * self.last_m; 97 | self.last_m = ms0; 98 | if ms0 > T::zero() { 99 | let out = d_sum / ms0.sqrt(); 100 | debug_assert!(out.is_finite(), "value must be finite"); 101 | self.out = Some(out); 102 | } 103 | } 104 | 105 | #[inline(always)] 106 | fn last(&self) -> Option { 107 | self.out 108 | } 109 | } 110 | 111 | #[cfg(test)] 112 | mod tests { 113 | use super::*; 114 | use crate::plot::plot_values; 115 | use crate::pure_functions::Echo; 116 | use crate::test_data::TEST_DATA; 117 | 118 | #[test] 119 | fn re_flex_plot() { 120 | let mut rf = ReFlex::new(Echo::new(), NonZeroUsize::new(16).unwrap()); 121 | let mut out: Vec = Vec::new(); 122 | for v in &TEST_DATA { 123 | rf.update(*v); 124 | if let Some(val) = rf.last() { 125 | out.push(val); 126 | } 127 | } 128 | let filename = "img/re_flex.png"; 129 | plot_values(out, filename).unwrap(); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/sliding_windows/roc.rs: -------------------------------------------------------------------------------- 1 | //! Rate of Change Indicator 2 | 3 | use getset::CopyGetters; 4 | use num::Float; 5 | use std::{collections::VecDeque, num::NonZeroUsize}; 6 | 7 | use crate::View; 8 | 9 | /// Rate of Change Indicator 10 | #[derive(Debug, Clone, CopyGetters)] 11 | pub struct Roc { 12 | view: V, 13 | /// The sliding window length 14 | #[getset(get_copy = "pub")] 15 | window_len: NonZeroUsize, 16 | oldest: Option, 17 | q_vals: VecDeque, 18 | out: Option, 19 | } 20 | 21 | impl Roc 22 | where 23 | V: View, 24 | T: Float, 25 | { 26 | /// Create a new Rate of Change Indicator with a chained View 27 | /// and a given sliding window length 28 | pub fn new(view: V, window_len: NonZeroUsize) -> Self { 29 | Roc { 30 | view, 31 | window_len, 32 | oldest: None, 33 | q_vals: VecDeque::with_capacity(window_len.get()), 34 | out: None, 35 | } 36 | } 37 | } 38 | 39 | impl View for Roc 40 | where 41 | V: View, 42 | T: Float, 43 | { 44 | fn update(&mut self, val: T) { 45 | debug_assert!(val.is_finite(), "value must be finite"); 46 | self.view.update(val); 47 | let Some(val) = self.view.last() else { return }; 48 | debug_assert!(val.is_finite(), "value must be finite"); 49 | 50 | if self.q_vals.is_empty() { 51 | self.oldest = Some(val); 52 | } 53 | if self.q_vals.len() >= self.window_len.get() { 54 | let old = self.q_vals.front().unwrap(); 55 | self.oldest = Some(*old); 56 | self.q_vals.pop_front(); 57 | } 58 | self.q_vals.push_back(val); 59 | 60 | let Some(oldest) = self.oldest else { return }; 61 | if oldest == T::zero() { 62 | return; 63 | } 64 | let roc = ((val - oldest) / oldest) * T::from(100.0).expect("can convert"); 65 | debug_assert!(roc.is_finite(), "`roc` must be finite"); 66 | self.out = Some(roc); 67 | } 68 | 69 | #[inline(always)] 70 | fn last(&self) -> Option { 71 | self.out 72 | } 73 | } 74 | 75 | #[cfg(test)] 76 | mod tests { 77 | use super::*; 78 | use crate::plot::plot_values; 79 | use crate::pure_functions::Echo; 80 | use crate::test_data::TEST_DATA; 81 | 82 | #[test] 83 | fn roc_plot() { 84 | let mut r = Roc::new(Echo::new(), NonZeroUsize::new(16).unwrap()); 85 | let mut out: Vec = Vec::new(); 86 | for v in &TEST_DATA { 87 | r.update(*v); 88 | if let Some(val) = r.last() { 89 | out.push(val); 90 | } 91 | } 92 | let filename = "img/roc.png"; 93 | plot_values(out, filename).unwrap(); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/sliding_windows/roofing_filter.rs: -------------------------------------------------------------------------------- 1 | use std::num::NonZeroUsize; 2 | 3 | use crate::{pure_functions::Echo, View}; 4 | use getset::CopyGetters; 5 | use num::Float; 6 | 7 | use super::SuperSmoother; 8 | 9 | /// Roofing Filter by John Ehlers 10 | /// From paper: 11 | #[derive(Debug, Clone, CopyGetters)] 12 | pub struct RoofingFilter { 13 | view: V, 14 | super_smoother: SuperSmoother>, 15 | /// The sliding window length. 16 | #[getset(get_copy = "pub")] 17 | window_len: NonZeroUsize, 18 | i: usize, 19 | alpha_1: T, 20 | // previous value 21 | val_1: T, 22 | // value from 2 steps ago 23 | val_2: T, 24 | // high pass filter value of previous step 25 | hp_1: T, 26 | // high pass filter value from two steps ago 27 | hp_2: T, 28 | } 29 | 30 | impl RoofingFilter 31 | where 32 | V: View, 33 | T: Float, 34 | { 35 | /// Create a Roofing Filter with a chained view 36 | pub fn new( 37 | view: V, 38 | window_len_low_pass: NonZeroUsize, 39 | super_smoother_len_high_pass: NonZeroUsize, 40 | ) -> Self { 41 | // NOTE: 4.4422 radians from 0.707 * 360 degrees 42 | let wl = T::from(window_len_low_pass.get()).expect("can convert"); 43 | let f = T::from(4.4422).expect("Can convert"); 44 | let alpha_1 = ((f / wl).cos() + (f / wl).sin() - T::one()) / (f / wl).cos(); 45 | 46 | RoofingFilter { 47 | view, 48 | super_smoother: SuperSmoother::new(Echo::new(), super_smoother_len_high_pass), 49 | window_len: window_len_low_pass, 50 | i: 0, 51 | alpha_1, 52 | val_1: T::zero(), 53 | val_2: T::zero(), 54 | hp_1: T::zero(), 55 | hp_2: T::zero(), 56 | } 57 | } 58 | } 59 | 60 | impl View for RoofingFilter 61 | where 62 | V: View, 63 | T: Float, 64 | { 65 | fn update(&mut self, val: T) { 66 | debug_assert!(val.is_finite(), "value must be finite"); 67 | self.view.update(val); 68 | let Some(val) = self.view.last() else { return }; 69 | debug_assert!(val.is_finite(), "value must be finite"); 70 | 71 | let two = T::from(2.0).expect("can convert"); 72 | let hp = (T::one() - self.alpha_1 / two).powi(2) * (val - two * self.val_1 + self.val_2) 73 | + two * (T::one() - self.alpha_1) * self.hp_1 74 | - (T::one() - self.alpha_1).powi(2) * self.hp_2; 75 | self.hp_2 = self.hp_1; 76 | self.hp_1 = hp; 77 | 78 | self.val_2 = self.val_1; 79 | self.val_1 = val; 80 | 81 | if self.i > self.window_len.get() { 82 | // to avoid weird output, only update, once warmup stage is done 83 | self.super_smoother.update(hp); 84 | } 85 | self.i += 1; 86 | } 87 | 88 | #[inline(always)] 89 | fn last(&self) -> Option { 90 | self.super_smoother.last().map(|v| { 91 | debug_assert!(v.is_finite(), "value must be finite"); 92 | v 93 | }) 94 | } 95 | } 96 | 97 | #[cfg(test)] 98 | mod tests { 99 | use super::*; 100 | use crate::plot::plot_values; 101 | use crate::test_data::TEST_DATA; 102 | 103 | #[test] 104 | fn roofing_filter_plot() { 105 | let mut rf = RoofingFilter::new( 106 | Echo::new(), 107 | NonZeroUsize::new(48).unwrap(), 108 | NonZeroUsize::new(10).unwrap(), 109 | ); 110 | let mut out: Vec = Vec::new(); 111 | for v in &TEST_DATA { 112 | rf.update(*v); 113 | if let Some(val) = rf.last() { 114 | out.push(val); 115 | } 116 | } 117 | let filename = "img/roofing_filter.png"; 118 | plot_values(out, filename).unwrap(); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/sliding_windows/rsi.rs: -------------------------------------------------------------------------------- 1 | //! Relative Strength Index Indicator 2 | 3 | use getset::CopyGetters; 4 | use num::Float; 5 | use std::{collections::VecDeque, num::NonZeroUsize}; 6 | 7 | use crate::View; 8 | 9 | /// Relative Strength Index Indicator 10 | #[derive(Debug, Clone, CopyGetters)] 11 | pub struct Rsi { 12 | view: V, 13 | /// The sliding window length. 14 | #[getset(get_copy = "pub")] 15 | window_len: NonZeroUsize, 16 | avg_gain: T, 17 | avg_loss: T, 18 | old_ref: T, 19 | last_val: T, 20 | q_vals: VecDeque, 21 | out: Option, 22 | } 23 | 24 | impl Rsi 25 | where 26 | V: View, 27 | T: Float, 28 | { 29 | /// Create a Relative Strength Index Indicator with a chained View 30 | /// and a given sliding window length 31 | #[inline] 32 | pub fn new(view: V, window_len: NonZeroUsize) -> Self { 33 | Rsi { 34 | view, 35 | window_len, 36 | avg_gain: T::zero(), 37 | avg_loss: T::zero(), 38 | old_ref: T::zero(), 39 | last_val: T::zero(), 40 | q_vals: VecDeque::with_capacity(window_len.get()), 41 | out: None, 42 | } 43 | } 44 | } 45 | 46 | impl View for Rsi 47 | where 48 | V: View, 49 | T: Float, 50 | { 51 | fn update(&mut self, val: T) { 52 | debug_assert!(val.is_finite(), "value must be finite"); 53 | self.view.update(val); 54 | let Some(val) = self.view.last() else { return }; 55 | debug_assert!(val.is_finite(), "value from `View` must be finite"); 56 | 57 | if self.q_vals.is_empty() { 58 | self.old_ref = val; 59 | self.last_val = val; 60 | } 61 | let window_len = T::from(self.window_len.get()).expect("can convert"); 62 | if self.q_vals.len() >= self.window_len.get() { 63 | // remove old 64 | let old_val = *self.q_vals.front().unwrap(); 65 | let change = old_val - self.old_ref; 66 | debug_assert!(change.is_finite(), "`change` must be finite"); 67 | self.old_ref = old_val; 68 | self.q_vals.pop_front(); 69 | if change > T::zero() { 70 | self.avg_gain = self.avg_gain - change / window_len; 71 | } else { 72 | self.avg_loss = self.avg_loss - change.abs() / window_len; 73 | } 74 | } 75 | self.q_vals.push_back(val); 76 | 77 | let change = val - self.last_val; 78 | debug_assert!(change.is_finite(), "`change` must be finite"); 79 | self.last_val = val; 80 | if change > T::zero() { 81 | self.avg_gain = self.avg_gain + change / window_len; 82 | } else { 83 | self.avg_loss = self.avg_loss + change.abs() / window_len; 84 | } 85 | debug_assert!(self.avg_gain.is_finite(), "`avg_gain` must be finite"); 86 | debug_assert!(self.avg_loss.is_finite(), "`avg_loss` must be finite"); 87 | 88 | if self.q_vals.len() < self.window_len.get() { 89 | return; 90 | } 91 | 92 | let hundred = T::from(100.0).expect("can convert"); 93 | if self.avg_loss == T::zero() { 94 | self.out = Some(hundred); 95 | } else { 96 | let rs = self.avg_gain / self.avg_loss; 97 | debug_assert!(rs.is_finite(), "`rs` must be finite"); 98 | let rsi = hundred - hundred / (T::one() + rs); 99 | debug_assert!(rsi.is_finite(), "value must be finite"); 100 | self.out = Some(rsi); 101 | } 102 | } 103 | 104 | #[inline(always)] 105 | fn last(&self) -> Option { 106 | self.out 107 | } 108 | } 109 | 110 | #[cfg(test)] 111 | mod tests { 112 | use super::*; 113 | use crate::plot::plot_values; 114 | use crate::pure_functions::Echo; 115 | use crate::test_data::TEST_DATA; 116 | 117 | #[test] 118 | fn rsi_plot() { 119 | let mut rsi = Rsi::new(Echo::new(), NonZeroUsize::new(16).unwrap()); 120 | let mut out: Vec = Vec::new(); 121 | for v in &TEST_DATA { 122 | rsi.update(*v); 123 | if let Some(val) = rsi.last() { 124 | out.push(val); 125 | } 126 | } 127 | let filename = "img/rsi.png"; 128 | plot_values(out, filename).unwrap(); 129 | } 130 | 131 | #[test] 132 | fn rsi_range() { 133 | let mut rsi = Rsi::new(Echo::new(), NonZeroUsize::new(16).unwrap()); 134 | for v in &TEST_DATA { 135 | rsi.update(*v); 136 | if let Some(last) = rsi.last() { 137 | assert!(last >= 0.0); 138 | assert!(last <= 100.0); 139 | } 140 | } 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/sliding_windows/sma.rs: -------------------------------------------------------------------------------- 1 | //! SMA - Simple Moving Average 2 | 3 | use num::Float; 4 | use std::{collections::VecDeque, num::NonZeroUsize}; 5 | 6 | use crate::View; 7 | 8 | #[derive(Debug, Clone)] 9 | /// SMA - Simple Moving Average 10 | pub struct Sma { 11 | view: V, 12 | window_len: NonZeroUsize, 13 | q_vals: VecDeque, 14 | sum: T, 15 | } 16 | 17 | impl Sma 18 | where 19 | V: View, 20 | T: Float, 21 | { 22 | /// Create a new simple moving average with a chained View 23 | /// and a given sliding window length 24 | #[inline] 25 | pub fn new(view: V, window_len: NonZeroUsize) -> Self { 26 | Sma { 27 | view, 28 | window_len, 29 | q_vals: VecDeque::new(), 30 | sum: T::zero(), 31 | } 32 | } 33 | } 34 | 35 | impl View for Sma 36 | where 37 | V: View, 38 | T: Float, 39 | { 40 | fn update(&mut self, val: T) { 41 | debug_assert!(val.is_finite(), "value must be finite"); 42 | self.view.update(val); 43 | let Some(val) = self.view.last() else { return }; 44 | debug_assert!(val.is_finite(), "value must be finite"); 45 | 46 | if self.q_vals.len() > self.window_len.get() { 47 | let old_val = self.q_vals.pop_front().unwrap(); 48 | self.sum = self.sum - old_val; 49 | } 50 | self.q_vals.push_back(val); 51 | 52 | self.sum = self.sum + val; 53 | } 54 | 55 | fn last(&self) -> Option { 56 | if self.q_vals.len() < self.window_len.get() { 57 | return None; 58 | } 59 | let sma = self.sum / T::from(self.q_vals.len()).expect("can convert"); 60 | debug_assert!(sma.is_finite(), "value must be finite"); 61 | Some(sma) 62 | } 63 | } 64 | 65 | #[cfg(test)] 66 | mod tests { 67 | use super::*; 68 | use crate::test_data::TEST_DATA; 69 | use crate::{plot::plot_values, pure_functions::Echo}; 70 | use rand::{rng, Rng}; 71 | 72 | #[test] 73 | fn sma() { 74 | let mut rng = rng(); 75 | 76 | let mut sma = Sma::new(Echo::new(), NonZeroUsize::new(16).unwrap()); 77 | for _ in 0..1024 { 78 | let r = rng.random::(); 79 | sma.update(r); 80 | if let Some(last) = sma.last() { 81 | assert!(last >= 0.0); 82 | assert!(last <= 1.0); 83 | } 84 | } 85 | } 86 | 87 | #[test] 88 | fn sma_plot() { 89 | let mut sma = Sma::new(Echo::new(), NonZeroUsize::new(16).unwrap()); 90 | let mut out: Vec = Vec::new(); 91 | for v in &TEST_DATA { 92 | sma.update(*v); 93 | if let Some(val) = sma.last() { 94 | out.push(val); 95 | } 96 | } 97 | let filename = "img/sma.png"; 98 | plot_values(out, filename).unwrap(); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/sliding_windows/super_smoother.rs: -------------------------------------------------------------------------------- 1 | use getset::CopyGetters; 2 | use num::Float; 3 | use std::{f64::consts::PI, num::NonZeroUsize}; 4 | 5 | use crate::View; 6 | 7 | /// John Ehlers SuperSmoother filter 8 | /// from 9 | #[derive(Debug, Clone, CopyGetters)] 10 | pub struct SuperSmoother { 11 | view: V, 12 | /// The sliding window length. 13 | #[getset(get_copy = "pub")] 14 | window_len: NonZeroUsize, 15 | i: usize, 16 | c1: T, 17 | c2: T, 18 | c3: T, 19 | /// filter value at current step 20 | filt: T, 21 | // filter one step ago 22 | filt_1: T, 23 | // filter two steps ago 24 | filt_2: T, 25 | last_val: T, 26 | } 27 | 28 | impl SuperSmoother 29 | where 30 | V: View, 31 | T: Float, 32 | { 33 | /// Create a new instance of the SuperSmoother with a chained View 34 | pub fn new(view: V, window_len: NonZeroUsize) -> Self { 35 | let wl = T::from(window_len.get()).expect("can convert"); 36 | let a1 = 37 | (-T::from(1.414).expect("can convert") * T::from(PI).expect("can convert") / wl).exp(); 38 | // NOTE: 4.4422 is radians of 1.414 * 180 degrees 39 | let b1 = T::from(2.0).expect("can convert") 40 | * a1 41 | * (T::from(4.4422).expect("can convert") / wl).cos(); 42 | let c2 = b1; 43 | let c3 = -a1 * a1; 44 | 45 | Self { 46 | view, 47 | window_len, 48 | i: 0, 49 | c1: T::one() - c2 - c3, 50 | c2, 51 | c3, 52 | filt: T::zero(), 53 | filt_1: T::zero(), 54 | filt_2: T::zero(), 55 | last_val: T::zero(), 56 | } 57 | } 58 | } 59 | 60 | impl View for SuperSmoother 61 | where 62 | V: View, 63 | T: Float, 64 | { 65 | fn update(&mut self, val: T) { 66 | debug_assert!(val.is_finite(), "value must be finite"); 67 | self.view.update(val); 68 | let Some(val) = self.view.last() else { return }; 69 | debug_assert!(val.is_finite(), "value must be finite"); 70 | 71 | self.filt = self.c1 * (val + self.last_val) / T::from(2.0).expect("can convert") 72 | + (self.c2 * self.filt_1) 73 | + (self.c3 * self.filt_2); 74 | self.filt_2 = self.filt_1; 75 | self.filt_1 = self.filt; 76 | self.last_val = val; 77 | self.i += 1; 78 | } 79 | 80 | #[inline] 81 | fn last(&self) -> Option { 82 | // NOTE: filter only kicks in after warmup steps are done 83 | if self.i < self.window_len.get() { 84 | None 85 | } else { 86 | let out = self.filt; 87 | debug_assert!(out.is_finite(), "value must be finite"); 88 | Some(self.filt) 89 | } 90 | } 91 | } 92 | 93 | #[cfg(test)] 94 | mod tests { 95 | use crate::{plot::plot_values, pure_functions::Echo, test_data::TEST_DATA}; 96 | 97 | use super::*; 98 | 99 | #[test] 100 | fn super_smoother_plot() { 101 | let mut ss = SuperSmoother::new(Echo::new(), NonZeroUsize::new(20).unwrap()); 102 | let mut out: Vec = Vec::with_capacity(TEST_DATA.len()); 103 | for v in &TEST_DATA { 104 | ss.update(*v); 105 | if let Some(val) = ss.last() { 106 | out.push(val); 107 | } 108 | } 109 | let filename = "img/super_smoother.png"; 110 | plot_values(out, filename).unwrap(); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/sliding_windows/trend_flex.rs: -------------------------------------------------------------------------------- 1 | //! John Ehlers TrendFlex Indicators 2 | //! from: 3 | 4 | use getset::CopyGetters; 5 | use num::Float; 6 | use std::{collections::VecDeque, num::NonZeroUsize}; 7 | 8 | use crate::View; 9 | 10 | /// John Ehlers TrendFlex Indicators 11 | /// from: 12 | #[derive(Debug, Clone, CopyGetters)] 13 | pub struct TrendFlex { 14 | view: V, 15 | /// The sliding window length. 16 | #[getset(get_copy = "pub")] 17 | window_len: NonZeroUsize, 18 | last_val: T, 19 | last_m: T, 20 | q_filts: VecDeque, 21 | out: Option, 22 | } 23 | 24 | impl TrendFlex 25 | where 26 | V: View, 27 | T: Float, 28 | { 29 | /// Create a new TrendFlex Indicator with a chained View 30 | /// and a given sliding window length 31 | #[inline] 32 | pub fn new(view: V, window_len: NonZeroUsize) -> Self { 33 | TrendFlex { 34 | view, 35 | window_len, 36 | last_val: T::zero(), 37 | last_m: T::zero(), 38 | q_filts: VecDeque::with_capacity(window_len.get()), 39 | out: None, 40 | } 41 | } 42 | } 43 | 44 | impl View for TrendFlex 45 | where 46 | V: View, 47 | T: Float, 48 | { 49 | fn update(&mut self, val: T) { 50 | debug_assert!(val.is_finite(), "value must be finite"); 51 | self.view.update(val); 52 | let Some(val) = self.view.last() else { return }; 53 | debug_assert!(val.is_finite(), "value must be finite"); 54 | 55 | if self.q_filts.is_empty() { 56 | self.last_val = val; 57 | } 58 | if self.q_filts.len() >= self.window_len.get() { 59 | self.q_filts.pop_front(); 60 | } 61 | let window_len = T::from(self.window_len.get()).expect("can convert"); 62 | let two = T::from(2.0).expect("can convert"); 63 | let a1 = (T::from(-8.88442402435).expect("can convert") / window_len).exp(); 64 | let b1 = two * a1 * (T::from(4.44221201218).expect("can convert") / window_len).cos(); 65 | let c3 = -a1 * a1; 66 | let c1 = T::one() - b1 - c3; 67 | 68 | let l = self.q_filts.len(); 69 | let mut filt = T::zero(); 70 | if l == 0 { 71 | filt = c1 * (val + self.last_val) / two 72 | } else if l == 1 { 73 | let filt1 = *self.q_filts.get(l - 1).unwrap(); 74 | filt = c1 * (val + self.last_val) / two + b1 * filt1 75 | } else if l > 1 { 76 | let filt2 = *self.q_filts.get(l - 2).unwrap(); 77 | let filt1 = *self.q_filts.get(l - 1).unwrap(); 78 | filt = c1 * (val + self.last_val) / two + b1 * filt1 + c3 * filt2; 79 | } 80 | self.last_val = val; 81 | self.q_filts.push_back(filt); 82 | 83 | // sum the differences 84 | let mut d_sum = T::zero(); 85 | for i in 0..self.q_filts.len() { 86 | let index: usize = self.q_filts.len() - 1 - i; 87 | d_sum = d_sum + (filt - self.q_filts[index]); 88 | } 89 | d_sum = d_sum / window_len; 90 | 91 | // normalize in terms of standard deviation; 92 | let ms0 = T::from(0.04).expect("can convert") * d_sum.powi(2) 93 | + T::from(0.96).expect("can convert") * self.last_m; 94 | self.last_m = ms0; 95 | if ms0 > T::zero() { 96 | let out = d_sum / ms0.sqrt(); 97 | debug_assert!(out.is_finite(), "value must be finite"); 98 | self.out = Some(out); 99 | } else { 100 | self.out = Some(T::zero()); 101 | } 102 | } 103 | 104 | #[inline(always)] 105 | fn last(&self) -> Option { 106 | self.out 107 | } 108 | } 109 | 110 | #[cfg(test)] 111 | mod tests { 112 | use super::*; 113 | use crate::plot::plot_values; 114 | use crate::pure_functions::Echo; 115 | use crate::test_data::TEST_DATA; 116 | 117 | #[test] 118 | fn trend_flex_plot() { 119 | let mut tf = TrendFlex::new(Echo::new(), NonZeroUsize::new(16).unwrap()); 120 | let mut out: Vec = Vec::new(); 121 | for v in &TEST_DATA { 122 | tf.update(*v); 123 | if let Some(val) = tf.last() { 124 | out.push(val); 125 | } 126 | } 127 | let filename = "img/trend_flex.png"; 128 | plot_values(out, filename).unwrap(); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/sliding_windows/variance_stabilizing_transformation.rs: -------------------------------------------------------------------------------- 1 | //! Variance Stabilizing Transform uses the standard deviation to normalize values 2 | 3 | use std::num::NonZeroUsize; 4 | 5 | use crate::{pure_functions::Echo, View}; 6 | use num::Float; 7 | 8 | use super::WelfordOnline; 9 | 10 | /// Variance Stabilizing Transform uses the standard deviation to normalize values 11 | #[derive(Debug, Clone)] 12 | pub struct Vst { 13 | view: V, 14 | last: T, 15 | welford_online: WelfordOnline>, 16 | } 17 | 18 | impl Vst 19 | where 20 | V: View, 21 | T: Float, 22 | { 23 | /// Create a new Variance Stabilizing Transform with a chained View 24 | /// and a given window length for computing standard deviation 25 | pub fn new(view: V, window_len: NonZeroUsize) -> Self { 26 | Self { 27 | view, 28 | last: T::zero(), 29 | welford_online: WelfordOnline::new(Echo::new(), window_len), 30 | } 31 | } 32 | 33 | /// The sliding window length. 34 | #[inline] 35 | pub fn window_len(&self) -> NonZeroUsize { 36 | self.welford_online.window_len() 37 | } 38 | } 39 | 40 | impl View for Vst 41 | where 42 | V: View, 43 | T: Float, 44 | { 45 | fn update(&mut self, val: T) { 46 | debug_assert!(val.is_finite(), "value must be finite"); 47 | self.view.update(val); 48 | let Some(val) = self.view.last() else { return }; 49 | debug_assert!(val.is_finite(), "value must be finite"); 50 | 51 | self.welford_online.update(val); 52 | self.last = val; 53 | } 54 | 55 | fn last(&self) -> Option { 56 | let std_dev = self.welford_online.last()?; 57 | if std_dev == T::zero() { 58 | return Some(self.last); 59 | } 60 | let out = self.last / std_dev; 61 | debug_assert!(out.is_finite(), "value must be finite"); 62 | Some(out) 63 | } 64 | } 65 | 66 | #[cfg(test)] 67 | mod tests { 68 | use super::*; 69 | use crate::plot::plot_values; 70 | use crate::test_data::TEST_DATA; 71 | 72 | #[test] 73 | fn variance_stabilizing_transform_plot() { 74 | let mut tf = Vst::new(Echo::new(), NonZeroUsize::new(16).unwrap()); 75 | let mut out: Vec = Vec::new(); 76 | for v in &TEST_DATA { 77 | tf.update(*v); 78 | if let Some(val) = tf.last() { 79 | out.push(val); 80 | } 81 | } 82 | let filename = "img/trend_flex.png"; 83 | plot_values(out, filename).unwrap(); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/sliding_windows/vsct.rs: -------------------------------------------------------------------------------- 1 | //! Variance Stabilizing Centering Transform Sliding Window 2 | 3 | use std::num::NonZeroUsize; 4 | 5 | use crate::{pure_functions::Echo, View}; 6 | use num::Float; 7 | 8 | use super::WelfordOnline; 9 | 10 | /// Variance Stabilizing Centering Transform Sliding Window 11 | #[derive(Debug, Clone)] 12 | pub struct Vsct { 13 | view: V, 14 | welford_online: WelfordOnline>, 15 | last: T, 16 | } 17 | 18 | impl Vsct 19 | where 20 | V: View, 21 | T: Float, 22 | { 23 | /// Create a new Variance Stabilizing Centering Transform with a chained View 24 | /// and a given sliding window length 25 | #[inline] 26 | pub fn new(view: V, window_len: NonZeroUsize) -> Self { 27 | Vsct { 28 | view, 29 | welford_online: WelfordOnline::new(Echo::new(), window_len), 30 | last: T::zero(), 31 | } 32 | } 33 | 34 | /// The sliding window length. 35 | #[inline(always)] 36 | pub fn window_len(&self) -> NonZeroUsize { 37 | self.welford_online.window_len() 38 | } 39 | } 40 | 41 | impl View for Vsct 42 | where 43 | V: View, 44 | T: Float, 45 | { 46 | fn update(&mut self, val: T) { 47 | debug_assert!(val.is_finite(), "value must be finite"); 48 | self.view.update(val); 49 | let Some(val) = self.view.last() else { return }; 50 | debug_assert!(val.is_finite(), "value must be finite"); 51 | 52 | self.welford_online.update(val); 53 | self.last = val; 54 | } 55 | 56 | fn last(&self) -> Option { 57 | let std_dev = self.welford_online.last()?; 58 | if std_dev == T::zero() { 59 | return Some(T::zero()); 60 | } 61 | let mean = self.welford_online.mean(); 62 | let out = (self.last - mean) / std_dev; 63 | debug_assert!(out.is_finite(), "value must be finite"); 64 | Some(out) 65 | } 66 | } 67 | 68 | #[cfg(test)] 69 | mod tests { 70 | use super::*; 71 | use crate::plot::plot_values; 72 | use crate::test_data::TEST_DATA; 73 | 74 | #[test] 75 | fn vsct_plot() { 76 | let mut vsct = Vsct::new(Echo::new(), NonZeroUsize::new(16).unwrap()); 77 | let mut out: Vec = Vec::with_capacity(TEST_DATA.len()); 78 | for v in &TEST_DATA { 79 | vsct.update(*v); 80 | if let Some(val) = vsct.last() { 81 | out.push(val); 82 | } 83 | } 84 | let filename = "img/vsct.png"; 85 | plot_values(out, filename).unwrap(); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/sliding_windows/welford_online.rs: -------------------------------------------------------------------------------- 1 | //! Welford online algorithm for computing mean and variance on-the-fly 2 | //! over a sliding window 3 | 4 | use crate::View; 5 | use getset::CopyGetters; 6 | use num::Float; 7 | use std::{collections::VecDeque, num::NonZeroUsize}; 8 | 9 | /// Welford online algorithm for computing mean and variance on-the-fly 10 | /// over a sliding window 11 | #[derive(Debug, Clone, CopyGetters)] 12 | pub struct WelfordOnline { 13 | view: V, 14 | /// The sliding window length. 15 | #[getset(get_copy = "pub")] 16 | window_len: NonZeroUsize, 17 | q_vals: VecDeque, 18 | /// The mean of the observed samples 19 | #[getset(get_copy = "pub")] 20 | mean: T, 21 | m2: T, 22 | count: usize, 23 | } 24 | 25 | impl WelfordOnline 26 | where 27 | V: View, 28 | T: Float, 29 | { 30 | /// Create a WelfordOnline struct with a chained View 31 | pub fn new(view: V, window_len: NonZeroUsize) -> Self { 32 | Self { 33 | view, 34 | window_len, 35 | q_vals: VecDeque::with_capacity(window_len.get()), 36 | mean: T::zero(), 37 | m2: T::zero(), 38 | count: 0, 39 | } 40 | } 41 | 42 | #[inline] 43 | fn update_stats_add(&mut self, x: T) { 44 | let delta = x - self.mean; 45 | self.mean = self.mean + (delta / T::from(self.count + 1).unwrap()); 46 | self.m2 = self.m2 + (delta * (x - self.mean)); 47 | self.count += 1; 48 | } 49 | 50 | #[inline] 51 | fn update_stats_remove(&mut self, old_value: T) { 52 | let delta = old_value - self.mean; 53 | self.mean = self.mean - (delta / T::from(self.count).unwrap()); 54 | self.m2 = self.m2 - (delta * (old_value - self.mean)); 55 | self.count -= 1; 56 | } 57 | 58 | /// Return the variance of the sliding window 59 | #[inline] 60 | pub fn variance(&self) -> T { 61 | if self.count > 1 { 62 | self.m2 / T::from(self.count - 1).expect("can convert") 63 | } else { 64 | T::zero() 65 | } 66 | } 67 | } 68 | 69 | impl View for WelfordOnline 70 | where 71 | V: View, 72 | T: Float, 73 | { 74 | fn update(&mut self, val: T) { 75 | debug_assert!(val.is_finite(), "value must be finite"); 76 | self.view.update(val); 77 | let Some(val) = self.view.last() else { return }; 78 | debug_assert!(val.is_finite(), "value must be finite"); 79 | 80 | self.q_vals.push_back(val); 81 | 82 | if self.q_vals.len() > self.window_len.get() { 83 | let old_val = self.q_vals.pop_front().unwrap(); 84 | self.update_stats_remove(old_val); 85 | } 86 | self.update_stats_add(val); 87 | } 88 | 89 | #[inline] 90 | fn last(&self) -> Option { 91 | if self.count < self.window_len.get() - 1 { 92 | // To ensure we don't return anything when there are not enough samples. 93 | return None; 94 | } 95 | let var = self.variance(); 96 | if var <= T::zero() { 97 | return Some(T::zero()); 98 | } 99 | debug_assert!(var >= T::zero(), "Variance must be positive"); 100 | let out = var.sqrt(); 101 | debug_assert!(out.is_finite(), "value must be finite"); 102 | Some(out) 103 | } 104 | } 105 | 106 | #[cfg(test)] 107 | mod tests { 108 | use super::*; 109 | use crate::plot::plot_values; 110 | use crate::pure_functions::Echo; 111 | use crate::test_data::TEST_DATA; 112 | use round::round; 113 | 114 | #[test] 115 | fn welford_online() { 116 | let mut wo = WelfordOnline::new(Echo::new(), NonZeroUsize::new(TEST_DATA.len()).unwrap()); 117 | for v in &TEST_DATA { 118 | wo.update(*v); 119 | if let Some(val) = wo.last() { 120 | assert!(!val.is_nan()); 121 | } 122 | } 123 | let w_std_dev = wo.last().expect("Is some"); 124 | 125 | // compute the standard deviation with the regular formula 126 | let avg: f64 = TEST_DATA.iter().sum::() / TEST_DATA.len() as f64; 127 | let std_dev: f64 = ((1.0 / (TEST_DATA.len() as f64 - 1.0)) 128 | * TEST_DATA.iter().map(|v| (v - avg).powi(2)).sum::()) 129 | .sqrt(); 130 | 131 | assert_eq!(round(w_std_dev, 4), round(std_dev, 4)); 132 | } 133 | 134 | #[test] 135 | fn welford_online_plot() { 136 | let mut wo = WelfordOnline::new(Echo::new(), NonZeroUsize::new(16).unwrap()); 137 | let mut out: Vec = Vec::new(); 138 | for v in &TEST_DATA { 139 | wo.update(*v); 140 | if let Some(val) = wo.last() { 141 | out.push(val); 142 | } 143 | } 144 | let filename = "img/welford_online_sliding.png"; 145 | plot_values(out, filename).unwrap(); 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/test_data.rs: -------------------------------------------------------------------------------- 1 | /// provide data for tests for consistent plotting, generated from test generate_test_data 2 | pub const TEST_DATA: [f64; 256] = [ 3 | 99.3443859531321, 4 | 99.01796871838589, 5 | 98.73294124423916, 6 | 97.2739698578621, 7 | 96.58956429476007, 8 | 96.17405687656834, 9 | 95.5504917649603, 10 | 95.2258896307167, 11 | 96.71669907742658, 12 | 97.68548584014461, 13 | 97.35679689790979, 14 | 97.98148906225232, 15 | 97.74744187507619, 16 | 95.82834576145414, 17 | 95.63693788813393, 18 | 96.20863302917208, 19 | 93.87439647771104, 20 | 93.34124486284382, 21 | 93.62226984322741, 22 | 93.32529230570216, 23 | 93.56324809850148, 24 | 93.43950793092951, 25 | 92.326980457101, 26 | 93.31877423830359, 27 | 92.68230194423907, 28 | 93.39179868847415, 29 | 93.12019397044874, 30 | 95.05250167555539, 31 | 96.19094034592094, 32 | 95.96958343623919, 33 | 95.28203330541099, 34 | 96.66344563833844, 35 | 97.15242184893589, 36 | 98.35607085463742, 37 | 100.16063999413541, 38 | 101.83340122396639, 39 | 102.84788098841419, 40 | 103.71940415571392, 41 | 104.62953335838891, 42 | 103.98969238953936, 43 | 103.91309409983153, 44 | 103.2851387866162, 45 | 102.17644850273467, 46 | 102.01770624080842, 47 | 103.56396149120575, 48 | 104.43856685018562, 49 | 105.8824505415504, 50 | 104.04750937370548, 51 | 104.09801102299349, 52 | 103.88711700838368, 53 | 102.45772409283933, 54 | 101.26840195757961, 55 | 101.30376081720475, 56 | 100.58265282387622, 57 | 100.63084239896286, 58 | 100.15531634387614, 59 | 99.20069957542664, 60 | 97.48983886669224, 61 | 97.72475995163315, 62 | 99.50768820319824, 63 | 100.24757563227178, 64 | 98.41326855301413, 65 | 99.34289078958523, 66 | 98.92949468088315, 67 | 98.20091997179189, 68 | 100.02499529267772, 69 | 98.14340038604517, 70 | 98.89354868550257, 71 | 100.02386256422953, 72 | 101.9676873300173, 73 | 101.13328552608908, 74 | 100.9312639545906, 75 | 101.10410640188124, 76 | 100.50887594983548, 77 | 102.98327070137287, 78 | 101.90953764601055, 79 | 101.99961079265714, 80 | 102.60399303300879, 81 | 100.74003219666174, 82 | 100.34066727318698, 83 | 100.48170502066017, 84 | 101.29396325759843, 85 | 101.68763421570961, 86 | 103.03442974009931, 87 | 103.20031682442183, 88 | 101.91054025149595, 89 | 103.63890162440866, 90 | 105.59196339950319, 91 | 105.22208375287964, 92 | 107.56161068431278, 93 | 108.67977448126737, 94 | 107.69847508418036, 95 | 107.63696511110226, 96 | 108.96674461169215, 97 | 109.87167765408688, 98 | 109.0801602030226, 99 | 107.12405681564934, 100 | 107.56557791776585, 101 | 108.03483947377255, 102 | 108.48271295210924, 103 | 109.50093731378043, 104 | 109.45608398995721, 105 | 108.50297845841158, 106 | 106.89522563380511, 107 | 105.89347772431637, 108 | 106.2563969568064, 109 | 107.32986401521974, 110 | 105.90380926315483, 111 | 106.79861579524628, 112 | 106.53393268987247, 113 | 105.89009112293725, 114 | 105.89576994319575, 115 | 106.11362601421654, 116 | 105.90851633533542, 117 | 105.54069745623849, 118 | 106.04117227747543, 119 | 107.39473362242643, 120 | 105.58500713055203, 121 | 104.3076949768713, 122 | 105.62951387151354, 123 | 105.29865865305393, 124 | 104.32385673917688, 125 | 104.6089300169441, 126 | 106.22314104991864, 127 | 106.52125916849918, 128 | 105.95650754911641, 129 | 105.97186915917985, 130 | 104.93440861375129, 131 | 105.38600916973475, 132 | 105.10483943066369, 133 | 104.01844025090355, 134 | 105.98810848434191, 135 | 107.77560060835235, 136 | 107.6596801390034, 137 | 109.47819790618092, 138 | 110.76791345927869, 139 | 111.5580651539678, 140 | 110.7264086523916, 141 | 111.03736020427613, 142 | 109.36130057380936, 143 | 108.11951660680839, 144 | 108.94542278329558, 145 | 109.8026161613033, 146 | 109.36923017919733, 147 | 110.21793943155153, 148 | 112.27064126251435, 149 | 112.74011314418512, 150 | 111.59881833861996, 151 | 113.78963822470962, 152 | 113.99135964686687, 153 | 114.86852753099278, 154 | 114.69675269261997, 155 | 114.74986044406559, 156 | 115.97723154374691, 157 | 118.1395035355269, 158 | 119.968241896465, 159 | 120.07451182569149, 160 | 119.76187307383526, 161 | 118.22757845304308, 162 | 117.55021801413554, 163 | 117.33099500993167, 164 | 116.57984737778312, 165 | 117.47135822225023, 166 | 117.05222836193262, 167 | 117.70426871806892, 168 | 118.32263871396299, 169 | 118.96813922280398, 170 | 120.26005748381039, 171 | 118.29504464181535, 172 | 118.29410294906351, 173 | 118.61298833618902, 174 | 119.20841311783735, 175 | 117.75137033619453, 176 | 117.07616496391844, 177 | 114.0268027078526, 178 | 112.10908997440627, 179 | 112.29405246504002, 180 | 111.65125719045339, 181 | 110.01869918721582, 182 | 109.73911268914264, 183 | 110.96694397553058, 184 | 112.52469951419455, 185 | 112.2678514823442, 186 | 114.06592626475438, 187 | 113.9543980204273, 188 | 113.74409768364886, 189 | 112.97995588514443, 190 | 112.3620894379739, 191 | 110.18260732273959, 192 | 108.91341835244836, 193 | 108.5394576575903, 194 | 108.97518270149718, 195 | 110.0600514595192, 196 | 109.48542519756982, 197 | 109.63374934173866, 198 | 108.36508419484343, 199 | 109.87214324107256, 200 | 112.09073221444038, 201 | 111.38430877625467, 202 | 108.92579174472556, 203 | 108.4179571306924, 204 | 106.37004008179791, 205 | 106.04054373469694, 206 | 105.38070510014745, 207 | 104.32807616433209, 208 | 104.81075235112503, 209 | 106.2340803987244, 210 | 107.81582140534051, 211 | 108.1083492556622, 212 | 106.49409983905444, 213 | 106.88426975477003, 214 | 109.00131994428816, 215 | 109.1389258976682, 216 | 107.32316730132652, 217 | 107.77398137297342, 218 | 108.98428561744404, 219 | 107.86583322680377, 220 | 108.3401230095469, 221 | 110.77081722769685, 222 | 110.47086842516178, 223 | 110.3214146405094, 224 | 109.2540456718041, 225 | 110.98001639596882, 226 | 112.18860089194166, 227 | 113.09393873472656, 228 | 113.0115186222329, 229 | 113.53829957214127, 230 | 114.1716308645449, 231 | 114.9895228633777, 232 | 113.88473868520654, 233 | 112.83495564882135, 234 | 113.2335442948531, 235 | 112.7118291646537, 236 | 111.24027986720239, 237 | 112.33218285473248, 238 | 112.12194766720192, 239 | 111.45354009181551, 240 | 111.93755652073081, 241 | 112.38140571207704, 242 | 112.57603159435519, 243 | 112.19022345238757, 244 | 112.96309756483599, 245 | 112.2185392010747, 246 | 114.52267981924768, 247 | 114.69782518517519, 248 | 114.38973705522415, 249 | 115.29051275695146, 250 | 115.2111636571004, 251 | 114.01220950064518, 252 | 113.85716523541734, 253 | 114.38517373586262, 254 | 112.99596969377843, 255 | 111.44647816263287, 256 | 110.20777061619351, 257 | 109.36788471095252, 258 | 108.74101451836995, 259 | ]; 260 | 261 | #[cfg(test)] 262 | mod tests { 263 | use rand::{rngs::SmallRng, SeedableRng}; 264 | use time_series_generator::generate_standard_normal; 265 | 266 | #[test] 267 | fn generate_test_data() { 268 | let data_len: usize = 256; 269 | let mut rng = SmallRng::seed_from_u64(0); 270 | let vals: Vec = generate_standard_normal(&mut rng, data_len, 100.0); 271 | println!("vals: {:?}", vals); 272 | } 273 | } 274 | --------------------------------------------------------------------------------