sliding_features 9.1.0

Modular sliding window with various signal processing functions and technical indicators
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
//! A sliding Min - Max Normalizer

use std::{
    collections::VecDeque,
    num::NonZeroUsize,
};

use getset::CopyGetters;
use num::Float;

use crate::View;

/// A sliding Min - Max Normalizer
///
/// Normalizes values to the [-1, 1] range using the min and max of a sliding
/// window of *past* values.  The current value is intentionally excluded from
/// the normalization window to avoid lookahead / data-leakage bias.
///
/// No output is emitted until the sliding window is completely filled.
/// During warm-up `last()` returns `None`.
#[derive(Clone, Debug, CopyGetters)]
pub struct MinMaxNormalizer<T, V> {
    view: V,
    /// The sliding window length
    #[getset(get_copy = "pub")]
    window_len: NonZeroUsize,
    q_vals: VecDeque<T>,
    min: T,
    max: T,
    out: Option<T>,
    init: bool,
}

impl<T, V> MinMaxNormalizer<T, V>
where
    V: View<T>,
    T: Float,
{
    /// Create a new instance with a chained View
    /// and a given sliding window length.
    pub fn new(view: V, window_len: NonZeroUsize) -> Self {
        MinMaxNormalizer {
            view,
            window_len,
            q_vals: VecDeque::with_capacity(window_len.get()),
            min: T::zero(),
            max: T::zero(),
            out: None,
            init: true,
        }
    }
}

fn extent_queue<T: Float>(q: &VecDeque<T>) -> (T, T) {
    let mut min = *q.front().unwrap();
    let mut max = *q.front().unwrap();

    for i in 1..q.len() {
        let val = *q.get(i).unwrap();
        if val > max {
            max = val;
        }
        if val < min {
            min = val;
        }
    }

    (min, max)
}

impl<T, V> View<T> for MinMaxNormalizer<T, V>
where
    V: View<T>,
    T: Float,
{
    fn update(&mut self, val: T) {
        debug_assert!(val.is_finite(), "value must be finite");
        self.view.update(val);
        let Some(view_last) = self.view.last() else {
            return;
        };
        debug_assert!(view_last.is_finite(), "value must be finite");

        if self.init {
            self.init = false;
            self.min = view_last;
            self.max = view_last;
            self.q_vals.push_back(view_last);
            // Warm-up: first value goes into the queue but no output yet.
            // We need window_len values before min/max are meaningful.
            return;
        }

        // Only emit output once the window is full.
        // Up to this point `out` remains None.
        if self.q_vals.len() >= self.window_len.get() {
            // Normalize the incoming value against the *previous* window's
            // min/max. This avoids lookahead bias.
            if self.min == self.max {
                self.out = Some(T::zero());
            } else {
                self.out = Some(
                    -T::one()
                        + ((view_last - self.min) * T::from(2.0).expect("can convert"))
                            / (self.max - self.min),
                );
            }
            debug_assert!(self.out.unwrap().is_finite(), "output must be finite");

            // Slide the window.
            let old = self.q_vals.pop_front().expect("Its checked above that the length is >= the non-zero window length, therefore this must be `Some`");
            self.q_vals.push_back(view_last);

            if old <= self.min || old >= self.max {
                let (min, max) = extent_queue(&self.q_vals);
                self.min = min;
                self.max = max;
            } else {
                if view_last > self.max {
                    self.max = view_last;
                }
                if view_last < self.min {
                    self.min = view_last;
                }
            }
        } else {
            // Still filling the window — no output yet.
            self.q_vals.push_back(view_last);
            if view_last > self.max {
                self.max = view_last;
            }
            if view_last < self.min {
                self.min = view_last;
            }
        }
    }

    fn last(&self) -> Option<T> {
        self.out
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::{
        plot::plot_values,
        pure_functions::Echo,
        test_data::TEST_DATA,
    };

    #[test]
    fn normalizer() {
        let mut n = MinMaxNormalizer::new(Echo::new(), NonZeroUsize::new(16).unwrap());
        for v in &TEST_DATA {
            n.update(*v);
            if let Some(last) = n.last() {
                assert!(last.is_finite());
            }
        }
    }

    #[test]
    fn min_max_normalizer_plot() {
        let mut n = MinMaxNormalizer::new(Echo::new(), NonZeroUsize::new(16).unwrap());
        let mut out: Vec<f64> = Vec::new();
        for v in &TEST_DATA {
            n.update(*v);
            if let Some(val) = n.last() {
                out.push(val);
            }
        }
        let filename = "img/min_max_normalizer.png";
        plot_values(out, filename).unwrap();
    }

    // ── Warm-up tests ──

    /// Outputs must be suppressed until the sliding window is fully filled.
    #[test]
    fn min_max_normalizer_warmup() {
        let mut n = MinMaxNormalizer::new(Echo::new(), NonZeroUsize::new(5).unwrap());
        // First 5 updates: window not yet full → None.
        // (The 1st goes through init, 2nd–5th have <N in queue.)
        for i in 0..5 {
            n.update(i as f64);
            assert!(n.last().is_none(), "warmup step {i}: expected None");
        }
        // 6th update has a full window → first output.
        n.update(5.0);
        assert!(
            n.last().is_some(),
            "first output after warmup should be Some"
        );
    }

    // ── Data-leakage / lookahead-bias tests ──

    /// The normalization of the current value must only use *past* values.
    /// When a spike arrives, the spike itself must not widen the min/max range
    /// used to normalize it — that would be lookahead bias.
    #[test]
    fn min_max_normalizer_no_lookahead_on_spike() {
        // Window = 3.
        // After warmup (3 values), the history is [10, 10, 10].
        // The 4th value (spike of 100) is normalized against [10,10,10]
        // (min=10, max=10 → output 0).
        let mut n = MinMaxNormalizer::new(Echo::new(), NonZeroUsize::new(3).unwrap());

        // Warm up: 3 values fill the window, output is None for all.
        for _ in 0..3 {
            n.update(10.0);
            assert!(n.last().is_none(), "warmup should suppress output");
        }

        // 4th value: first real normalization.
        n.update(100.0);
        let got = n.last().unwrap();
        // Leakage would give ≈ 1.0.  Correct causal output is 0.0.
        assert!(
            got.abs() < 1e-12,
            "lookahead bias detected: spike widened its own range, got {got}, expected 0.0"
        );
    }

    /// After the spike leaves the window, the normalizer should recover.
    #[test]
    fn min_max_normalizer_recovers_after_spike_leaves_window() {
        let mut n = MinMaxNormalizer::new(Echo::new(), NonZeroUsize::new(3).unwrap());

        // Warm up (3 updates, all output None).
        for _ in 0..3 {
            n.update(10.0);
        }

        // Spike (4th update — first real output, normalized against [10,10,10] → 0).
        n.update(100.0);
        let _ = n.last();

        // Push spike through the window: three more 10s are needed
        // (100 moves to index 2→1→0→popped).
        n.update(10.0);
        let _ = n.last();
        n.update(10.0);
        let _ = n.last();
        n.update(10.0);
        let _ = n.last();

        // Now the window is [10, 10, 10], min=max=10, output=0.
        n.update(10.0);
        let got = n.last().unwrap();
        assert!(
            got.abs() < 1e-12,
            "after spike left window, expected 0.0, got {got}"
        );
    }

    /// A steadily rising sequence should produce consistent normalization.
    #[test]
    fn min_max_normalizer_rising_sequence() {
        let window = 4;
        let mut n = MinMaxNormalizer::new(Echo::new(), NonZeroUsize::new(window).unwrap());
        let values: Vec<f64> = (1..=10).map(|i| i as f64 * 10.0).collect();
        // [10, 20, 30, 40, 50, 60, 70, 80, 90, 100]

        for v in &values {
            n.update(*v);
        }

        // After all updates, the last value (100) was normalized against
        // the previous window [60, 70, 80, 90] (min=60, max=90).
        // normalized(100) = -1 + 2*(100-60)/(90-60) = -1 + 2*40/30 = 5/3
        let got = n.last().unwrap();
        let expected = 5.0 / 3.0;
        let diff = (got - expected).abs();
        assert!(
            diff < 1e-12,
            "rising sequence last: expected {expected}, got {got}, diff {diff}"
        );
    }

    // ── Stale min/max bug tests ──

    /// When the singular minimum leaves the window, the min must be recalculated.
    #[test]
    fn min_max_normalizer_min_recalculated_when_singular_min_leaves() {
        let mut n = MinMaxNormalizer::new(Echo::new(), NonZeroUsize::new(4).unwrap());

        // Fill the window to warm up.
        n.update(1.0);
        n.update(100.0);
        n.update(100.0);
        n.update(100.0);
        // Queue: [1, 100, 100, 100], min=1, max=100.

        // Next 100: pops 1. This is normalized against [1,100,100,100] → 1.0.
        n.update(100.0);
        let _ = n.last();
        // Queue now: [100, 100, 100, 100].

        // Next 100: normalized against [100,100,100,100] → min=max=100 → 0.
        n.update(100.0);
        let got = n.last().unwrap();
        assert!(
            got.abs() < 1e-12,
            "after singular min left + one more update, expected 0.0, got {got}"
        );
    }

    /// Symmetric test: when the singular maximum leaves, max must be recalculated.
    #[test]
    fn min_max_normalizer_max_recalculated_when_singular_max_leaves() {
        let mut n = MinMaxNormalizer::new(Echo::new(), NonZeroUsize::new(4).unwrap());

        // Fill to warm up.
        n.update(100.0);
        n.update(1.0);
        n.update(1.0);
        n.update(1.0);
        // Queue: [100, 1, 1, 1], min=1, max=100.

        // Next 1: pops 100. Normalized against [100,1,1,1] → -1.0.
        n.update(1.0);
        let _ = n.last();
        // Queue now: [1, 1, 1, 1].

        // Next 1: normalized against [1,1,1,1] → min=max=1 → 0.
        n.update(1.0);
        let got = n.last().unwrap();
        assert!(
            got.abs() < 1e-12,
            "after singular max left + one more update, expected 0.0, got {got}"
        );
    }

    // ── General correctness tests ──

    /// When the window contains identical values, output is always 0.
    #[test]
    fn min_max_normalizer_identical_values_yield_zero() {
        let mut n = MinMaxNormalizer::new(Echo::new(), NonZeroUsize::new(5).unwrap());
        // Warm up: first 5 values produce None.
        for _ in 0..5 {
            n.update(42.0);
            assert!(n.last().is_none(), "warmup should suppress output");
        }
        // From the 6th onward, every output should be 0.
        for _ in 0..20 {
            n.update(42.0);
            assert!(
                n.last().unwrap().abs() < 1e-12,
                "identical values should yield normalized output 0"
            );
        }
    }

    /// Values at exactly the min normalize to -1.0; at exactly the max to +1.0.
    #[test]
    fn min_max_normalizer_bounds() {
        let mut n = MinMaxNormalizer::new(Echo::new(), NonZeroUsize::new(3).unwrap());

        // Warm up the window.
        n.update(0.0);
        n.update(100.0);
        n.update(100.0);
        // Queue: [0, 100, 100], min=0, max=100.
        // After 3rd update we now have a full window, first output coming next.

        // Update with a value at the min.
        n.update(0.0);
        let got_min = n.last().unwrap();
        assert!(
            (got_min - (-1.0)).abs() < 1e-12,
            "expected -1.0, got {got_min}"
        );

        // Queue is now [100, 100, 0], min=0, max=100.
        // Update with a value at the max.
        n.update(100.0);
        let got_max = n.last().unwrap();
        assert!((got_max - 1.0).abs() < 1e-12, "expected 1.0, got {got_max}");
    }

    /// Ensure MinMaxNormalizer works when chained after another View.
    #[test]
    fn min_max_normalizer_chained() {
        use crate::sliding_windows::Ema;
        let mut n = MinMaxNormalizer::new(
            Ema::new(Echo::new(), NonZeroUsize::new(5).unwrap()),
            NonZeroUsize::new(8).unwrap(),
        );
        for v in &TEST_DATA {
            n.update(*v);
            if let Some(val) = n.last() {
                assert!(
                    val.is_finite(),
                    "chained output should be finite, got {val}"
                );
            }
        }
    }

    /// The normalizer should handle the case where window_len == 1.
    #[test]
    fn min_max_normalizer_window_len_one() {
        let mut n = MinMaxNormalizer::new(Echo::new(), NonZeroUsize::new(1).unwrap());

        // First update goes through init, no output yet.
        n.update(5.0);
        assert!(n.last().is_none(), "first update: init, no output");

        // Second update: queue has 1 value, window full → output.
        n.update(10.0);
        // Normalize 10 against [5] → min=max=5 → 0.0
        assert_eq!(n.last().unwrap(), 0.0);
    }

    /// Verify the normalizer produces finite outputs for all test data.
    #[test]
    fn min_max_normalizer_all_outputs_finite() {
        let mut n = MinMaxNormalizer::new(Echo::new(), NonZeroUsize::new(16).unwrap());
        for v in &TEST_DATA {
            n.update(*v);
            if let Some(out) = n.last() {
                assert!(out.is_finite(), "output should be finite, got {out}");
            }
        }
    }

    /// Regression: the normalizer should not panic and should produce
    /// valid output when fed many values.
    #[test]
    fn min_max_normalizer_stress_test() {
        let mut n = MinMaxNormalizer::new(Echo::new(), NonZeroUsize::new(64).unwrap());
        for i in 0..1000 {
            let val = (i as f64).sin();
            n.update(val);
            if let Some(out) = n.last() {
                assert!(out.is_finite(), "output should be finite, got {out}");
            }
        }
    }
}