ta_statistics/
single_statistics.rs

1use num_traits::Float;
2
3use core::iter::Sum;
4
5use crate::{
6    PairedStatistics,
7    rolling_moments::RollingMoments,
8    utils::{
9        RingBuffer,
10        helper::{median_from_sorted_slice, quantile_from_sorted_slice},
11    },
12};
13
14/// A structure that computes various statistics over a fixed-size window of values.
15/// A specialized statistics implementation for single time-series data analysis.
16///
17/// This structure provides comprehensive statistical calculations for financial
18/// time-series data, optimized for algorithmic trading applications. It maintains
19/// a fixed-size window of values and efficiently updates statistics as new data
20/// points arrive in a streaming fashion.
21///
22/// The structure is particularly useful for technical analysis, risk management,
23/// and alpha generation in quantitative trading strategies.
24#[derive(Debug, Clone)]
25pub struct SingleStatistics<T> {
26    /// Rolling moments
27    moments: RollingMoments<T>,
28    /// Fixed buffer for sorting on demand
29    sorted_buf: RingBuffer<T>,
30    /// Current minimum value
31    min: Option<T>,
32    /// Current maximum value
33    max: Option<T>,
34    /// Maximum drawdown
35    max_drawdown: Option<T>,
36}
37
38impl<T> SingleStatistics<T>
39where
40    T: Default + Clone + Float,
41{
42    /// Creates a new `SingleStatistics` instance with the specified period.
43    ///
44    /// # Arguments
45    ///
46    /// * `period` - The period of the statistics
47    ///
48    /// # Returns
49    ///
50    /// * `Self` - The statistics object
51    pub fn new(period: usize) -> Self {
52        Self {
53            moments: RollingMoments::new(period),
54            sorted_buf: RingBuffer::new(period),
55            min: None,
56            max: None,
57            max_drawdown: None,
58        }
59    }
60
61    /// Returns the period of the statistics
62    ///
63    /// # Returns
64    ///
65    /// * `usize` - The period of the statistics
66    pub fn period(&self) -> usize {
67        self.moments.period()
68    }
69
70    /// Resets the statistics
71    ///
72    /// # Returns
73    ///
74    /// * `&mut Self` - The statistics object
75    pub fn reset(&mut self) -> &mut Self {
76        self.moments.reset();
77        self.sorted_buf.reset();
78        self.min = None;
79        self.max = None;
80        self.max_drawdown = None;
81        self
82    }
83
84    /// Recomputes the single statistics, could be called to avoid
85    /// prolonged compounding of floating rounding errors
86    ///
87    /// # Returns
88    ///
89    /// * `&mut Self` - The rolling moments object
90    pub fn recompute(&mut self) -> &mut Self {
91        self.moments.recompute();
92        self
93    }
94
95    fn period_t(&self) -> Option<T>
96    where
97        T: Float,
98    {
99        T::from(self.period())
100    }
101
102    // Copies and sorts the buf
103    fn sorted_buf(&mut self) -> &[T]
104    where
105        T: Copy + Default + PartialOrd,
106    {
107        self.sorted_buf.copy_from_slice(self.moments.as_slice());
108        self.sorted_buf.sort()
109    }
110
111    /// Returns the Delta Degrees of Freedom
112    ///
113    /// # Returns
114    ///
115    /// * `bool` - The Delta Degrees of Freedom
116    pub const fn ddof(&self) -> bool {
117        self.moments.ddof()
118    }
119
120    /// Sets the Delta Degrees of Freedom
121    ///
122    /// # Arguments
123    ///
124    /// * `ddof` - The Delta Degrees of Freedom
125    ///
126    /// # Returns
127    ///
128    /// * `&mut Self` - The statistics object
129    pub const fn set_ddof(&mut self, ddof: bool) -> &mut Self {
130        self.moments.set_ddof(ddof);
131        self
132    }
133
134    /// Updates the statistical calculations with a new value in the time series
135    ///
136    /// Incorporates a new data point into the rolling window, maintaining the specified
137    /// window size by removing the oldest value when necessary. This is the core method
138    /// that should be called whenever new data is available for processing.
139    ///
140    /// The statistics are calculated using the Kahan-Babuška-Neumaier (Kbn) algorithm
141    /// for numerically stable summation. This compensated summation technique minimizes
142    /// floating-point errors that would otherwise accumulate in long-running calculations,
143    /// particularly important for financial time-series analysis where precision is critical.
144    ///
145    /// # Arguments
146    ///
147    /// * `value` - The new value to be added to the time series
148    ///
149    /// # Returns
150    ///
151    /// * `&mut Self` - The statistics object
152    pub fn next(&mut self, value: T) -> &mut Self {
153        self.moments.next(value);
154        self
155    }
156
157    /// Returns the sum of all values in the rolling window
158    ///
159    /// This fundamental calculation serves as the basis for numerous higher-order statistics
160    /// and provides key insights for:
161    ///
162    /// - Aggregating transaction volumes to identify participant interest levels
163    /// - Constructing accumulation/distribution profiles across price action
164    /// - Measuring net directional pressure in time series data
165    /// - Quantifying capital flows between market segments
166    ///
167    /// # Returns
168    ///
169    /// * `Option<T>` - The sum of all values in the window, or `None` if the window is not full
170    ///
171    /// # Examples
172    ///
173    /// ```
174    /// # use ta_statistics::SingleStatistics;
175    /// let mut stats = SingleStatistics::new(3);
176    /// let inputs = [1_000_000.1, 1_000_000.2, 1_000_000.3, 1_000_000.4, 1_000_000.5, 1_000_000.6, 1_000_000.7];
177    /// let mut results = vec![];
178    ///
179    /// inputs.iter().for_each(|i| {
180    ///     stats.next(*i).sum().map(|v| results.push(v));
181    /// });
182    ///
183    /// let expected = [3000000.6, 3000000.9, 3000001.2, 3000001.5, 3000001.8];
184    /// assert_eq!(&results, &expected);
185    /// ```
186    pub fn sum(&self) -> Option<T> {
187        self.moments.sum()
188    }
189
190    /// Returns the sum of squares of all values in the rolling window
191    ///
192    /// This calculation provides the sum of squared values in the series, offering insights into
193    /// the magnitude of values regardless of sign:
194    ///
195    /// - Serves as a component for calculating variance and other dispersion measures
196    /// - Emphasizes larger values due to the squaring operation
197    /// - Provides power estimation in signal processing applications
198    /// - Helps detect significant deviations from expected behavior
199    ///
200    /// # Returns
201    ///
202    /// * `Option<T>` - The sum of squares in the window, or `None` if the window is not full
203    ///
204    /// # Examples
205    ///
206    /// ```
207    /// # use ta_statistics::SingleStatistics;
208    /// let mut stats = SingleStatistics::new(3);
209    /// let inputs = [1_000_000.1, 1_000_000.2, 1_000_000.3, 1_000_000.4, 1_000_000.5, 1_000_000.6, 1_000_000.7];
210    /// let mut results = vec![];
211    ///
212    /// inputs.iter().for_each(|i| {
213    ///     stats.next(*i).sum_sq().map(|v| results.push(v));
214    /// });
215    /// let expected = [3000001200000.14, 3000001800000.29, 3000002400000.5, 3000003000000.77, 3000003600001.0996];
216    /// assert_eq!(&results, &expected);
217    /// ```
218    pub fn sum_sq(&self) -> Option<T> {
219        self.moments.sum_sq()
220    }
221
222    /// Returns the arithmetic mean of all values in the rolling window
223    ///
224    /// This central tendency measure forms the foundation for numerous statistical calculations
225    /// and serves as a reference point for analyzing data distributions:
226    ///
227    /// - Establishes equilibrium levels for reversion-based analytical models
228    /// - Provides baseline reference for filtering noisy sequential data
229    /// - Creates statistical foundation for pattern detection in time series
230    /// - Enables feature normalization in advanced predictive modeling
231    ///
232    /// # Returns
233    ///
234    /// * `Option<T>` - The arithmetic mean of values in the window, or `None` if the window is not full
235    ///
236    /// # Examples
237    ///
238    /// ```
239    /// # use ta_statistics::SingleStatistics;
240    /// # use assert_approx_eq::assert_approx_eq;
241    /// let mut stats = SingleStatistics::new(3);
242    /// let inputs = [1_000_000.1, 1_000_000.2, 1_000_000.3, 1_000_000.4, 1_000_000.5, 1_000_000.6, 1_000_000.7];
243    /// let mut results = vec![];
244    ///
245    /// inputs.iter().for_each(|i| {
246    ///     stats.next(*i).mean().map(|v| results.push(v));
247    /// });
248    ///
249    /// let expected: [f64; 5] = [1000000.2, 1000000.3, 1000000.4, 1000000.5, 1000000.6];
250    /// for (i, e) in expected.iter().enumerate() {
251    ///     assert_approx_eq!(e, results[i], 0.0001);
252    /// }
253    /// ```
254    pub fn mean(&self) -> Option<T> {
255        self.moments.mean()
256    }
257
258    /// Returns the mean of squares of all values in the rolling window
259    ///
260    /// This calculation provides the average of squared values in the series, offering
261    /// insights into the magnitude of values regardless of sign:
262    ///
263    /// - Serves as a component for calculating variance and other dispersion measures
264    /// - Emphasizes larger values due to the squaring operation
265    /// - Provides power estimation in signal processing applications
266    /// - Helps detect significant deviations from expected behavior
267    ///
268    /// # Returns
269    ///
270    /// * `Option<T>` - The mean of squared values in the window, or `None` if the window is not full
271    ///
272    /// # Examples
273    ///
274    /// ```
275    /// # use ta_statistics::SingleStatistics;
276    /// # use assert_approx_eq::assert_approx_eq;
277    /// let mut stats = SingleStatistics::new(3);
278    /// let inputs = [1_000_000.1, 1_000_000.2, 1_000_000.3, 1_000_000.4, 1_000_000.5, 1_000_000.6, 1_000_000.7];
279    /// let mut results = vec![];
280    ///
281    /// inputs.iter().for_each(|i| {
282    ///     stats.next(*i).mean_sq().map(|v| results.push(v));
283    /// });
284    ///
285    /// let expected: [f64; 5] = [1000000400000.05,1000000600000.1,1000000800000.17,1000001000000.26,1000001200000.37];
286    /// for (i, e) in expected.iter().enumerate() {
287    ///     assert_approx_eq!(e, results[i], 0.01);
288    /// }
289    /// ```
290    pub fn mean_sq(&self) -> Option<T> {
291        self.moments.mean_sq()
292    }
293
294    /// Returns the mode (most frequently occurring value) in the rolling window
295    ///
296    /// The mode identifies the most common value within a distribution, providing
297    /// insight into clustering behavior and prevalent conditions:
298    ///
299    /// - Identifies common price points that may act as magnets or barriers
300    /// - Detects clustering in volume or activity patterns
301    /// - Provides non-parametric central tendency alternative to mean/median
302    /// - Highlights dominant price levels where transactions concentrate
303    ///
304    /// # Returns
305    ///
306    /// * `Option<T>` - The mode of values in the window, or `None` if the window is not full
307    ///
308    /// # Examples
309    ///
310    /// ```
311    /// # use ta_statistics::SingleStatistics;
312    /// # use assert_approx_eq::assert_approx_eq;
313    /// let mut stats = SingleStatistics::new(3);
314    /// let inputs = [1.0, 2.0, 1.0, 2.0, 3.0, 3.0, 3.0, 2.0, 2.0, 1.0];
315    /// let mut results = vec![];
316    ///
317    /// inputs.iter().for_each(|i| {
318    ///     stats.next(*i).mode().map(|v| results.push(v));
319    /// });
320    ///
321    /// let expected: [f64; 8] = [1.0, 2.0, 1.0, 3.0, 3.0, 3.0, 2.0, 2.0];
322    /// assert_eq!(&results, &expected);
323    /// ```
324    pub fn mode(&mut self) -> Option<T> {
325        if !self.moments.is_ready() {
326            return None;
327        }
328
329        let slice = self.sorted_buf();
330
331        let mut mode = slice[0];
332        let mut mode_count = 1;
333
334        let mut current = slice[0];
335        let mut current_count = 1;
336
337        for &value in &slice[1..] {
338            if value == current {
339                current_count += 1;
340            } else {
341                if current_count > mode_count || (current_count == mode_count && current < mode) {
342                    mode = current;
343                    mode_count = current_count;
344                }
345                current = value;
346                current_count = 1;
347            }
348        }
349
350        if current_count > mode_count || (current_count == mode_count && current < mode) {
351            mode = current;
352        }
353
354        Some(mode)
355    }
356
357    /// Returns the median (middle value) of the rolling window
358    ///
359    /// The median represents the central value when data is sorted, providing a robust
360    /// measure of central tendency that's less affected by outliers than the mean:
361    ///
362    /// - Offers resilient central price estimation in volatile conditions
363    /// - Establishes more stable reference points during extreme events
364    /// - Provides core input for non-parametric statistical models
365    /// - Creates baseline for interquartile range and other robust dispersion measures
366    ///
367    /// # Returns
368    ///
369    /// * `Option<T>` - The median of values in the window, or `None` if the window is not full
370    ///
371    /// # Examples
372    ///
373    /// ```
374    /// # use ta_statistics::SingleStatistics;
375    /// # use assert_approx_eq::assert_approx_eq;
376    /// let mut stats = SingleStatistics::new(3);
377    /// let inputs = [5.0, 2.0, 8.0, 1.0, 7.0, 3.0, 9.0];
378    /// let mut results = vec![];
379    ///
380    /// inputs.iter().for_each(|i| {
381    ///     stats.next(*i).median().map(|v| results.push(v));
382    /// });
383    ///
384    /// let expected: [f64; 5] = [5.0, 2.0, 7.0, 3.0, 7.0];
385    /// assert_eq!(&results, &expected);
386    /// ```
387    pub fn median(&mut self) -> Option<T> {
388        self.moments
389            .is_ready()
390            .then_some(median_from_sorted_slice(self.sorted_buf()))
391    }
392
393    /// Returns the minimum value in the rolling window
394    ///
395    /// The minimum value represents the lower bound of a data series over the observation
396    /// period, providing key reference points for analysis and decision-making:
397    ///
398    /// - Establishes potential support levels in price-based analysis
399    /// - Identifies optimal entry thresholds for mean-reverting sequences
400    /// - Sets critical risk boundaries for position management systems
401    /// - Provides baseline scenarios for stress-testing and risk modeling
402    ///
403    /// Uses a monotonic queue data structure to efficiently track the minimum value,
404    /// offering O(1) time complexity for lookups and amortized O(1) time complexity for
405    /// insertions and deletions, making it highly efficient for rolling window calculations.
406    ///
407    /// # Returns
408    ///
409    /// * `Option<T>` - The minimum value in the window, or `None` if the window is not full
410    ///
411    /// # Examples
412    ///
413    /// ```
414    /// # use ta_statistics::SingleStatistics;
415    /// # use assert_approx_eq::assert_approx_eq;
416    /// let mut stats = SingleStatistics::new(3);
417    /// let inputs = [25.4, 26.2, 26.0, 26.1, 25.8, 25.9, 26.3, 26.2, 26.5];
418    /// let mut results = vec![];
419    ///
420    /// inputs.iter().for_each(|i| {
421    ///     stats.next(*i).min().map(|v| results.push(v));
422    /// });
423    ///
424    /// let expected: [f64; 7] = [25.4, 26.0, 25.8, 25.8, 25.8, 25.9, 26.2];
425    /// assert_eq!(&results, &expected);
426    /// ```
427    pub fn min(&mut self) -> Option<T> {
428        if !self.moments.is_ready() {
429            return None;
430        }
431        self.moments.min()
432    }
433
434    /// Returns the maximum value in the rolling window
435    ///
436    /// The maximum value identifies the upper bound of a data series over the observation
437    /// period, establishing crucial reference points for analytical frameworks:
438    ///
439    /// - Identifies potential resistance zones in technical analysis
440    /// - Optimizes profit-taking thresholds based on historical precedent
441    /// - Confirms genuine breakouts from established trading ranges
442    /// - Defines upper boundaries for range-bound trading approaches
443    ///
444    /// Uses a monotonic queue data structure to efficiently track the maximum value, offering
445    /// O(1) time complexity for lookups and amortized O(1) time complexity for insertions and
446    /// deletions, making it highly efficient for rolling window calculations.
447    ///
448    /// # Returns
449    ///
450    /// * `Option<T>` - The maximum value in the window, or `None` if the window is not full
451    ///
452    /// # Examples
453    ///
454    /// ```
455    /// # use ta_statistics::SingleStatistics;
456    /// # use assert_approx_eq::assert_approx_eq;
457    /// let mut stats = SingleStatistics::new(3);
458    /// let inputs = [25.4, 26.2, 26.0, 26.1, 25.8, 25.9, 26.3, 26.2, 26.5];
459    /// let mut results = vec![];
460    ///
461    /// inputs.iter().for_each(|i| {
462    ///     stats.next(*i).max().map(|v| results.push(v));
463    /// });
464    ///
465    /// let expected: [f64; 7] = [26.2, 26.2, 26.1, 26.1, 26.3, 26.3, 26.5];
466    /// assert_eq!(&results, &expected);
467    /// ```
468    pub fn max(&mut self) -> Option<T> {
469        if !self.moments.is_ready() {
470            return None;
471        }
472
473        self.moments.max()
474    }
475
476    /// Returns the mean absolute deviation of values in the rolling window
477    ///
478    /// This robust dispersion measure calculates the average absolute difference from the mean,
479    /// offering advantages over variance in certain analytical contexts:
480    ///
481    /// - Provides volatility measurement that's less sensitive to extreme outliers
482    /// - Quantifies data noise levels to enhance signal processing accuracy
483    /// - Complements standard risk models with an alternative dispersion metric
484    /// - Establishes more stable thresholds for adaptive signal generation
485    ///
486    /// # Returns
487    ///
488    /// * `Option<T>` - The mean absolute deviation of values, or `None` if the window is not full
489    ///
490    /// # Examples
491    ///
492    /// ```
493    /// # use ta_statistics::SingleStatistics;
494    /// # use assert_approx_eq::assert_approx_eq;
495    /// let mut stats = SingleStatistics::new(3);
496    /// let mut results = vec![];
497    /// let inputs = [2.0, 4.0, 6.0, 8.0, 10.0];
498    /// inputs.iter().for_each(|i| {
499    ///     stats.next(*i).mean_absolute_deviation().map(|v| results.push(v));
500    /// });
501    ///
502    /// let expected: [f64; 3] = [1.3333, 1.3333, 1.3333];
503    /// for (i, e) in expected.iter().enumerate() {
504    ///     assert_approx_eq!(e, results[i], 0.1);
505    /// }
506    ///
507    /// ```
508    pub fn mean_absolute_deviation(&self) -> Option<T>
509    where
510        T: Sum,
511    {
512        let mean = self.mean()?;
513        let abs_sum = self.moments.iter().map(|&x| (x - mean).abs()).sum::<T>();
514        self.period_t().map(|n| abs_sum / n)
515    }
516
517    /// Returns the median absolute deviation of values in the rolling window
518    ///
519    /// This exceptionally robust dispersion measure calculates the median of absolute differences
520    /// from the median, offering superior resistance to outliers:
521    ///
522    /// - Provides reliable volatility assessment in erratic or noisy environments
523    /// - Serves as a foundation for robust anomaly detection systems
524    /// - Enables stable threshold calibration for adaptive decision systems
525    /// - Forms basis for robust statistical estimators in non-normal distributions
526    ///
527    /// # Returns
528    ///
529    /// * `Option<T>` - The median absolute deviation of values, or `None` if the window is not full
530    ///
531    /// # Examples
532    ///
533    /// ```
534    /// # use ta_statistics::SingleStatistics;
535    /// # use assert_approx_eq::assert_approx_eq;
536    /// let mut stats = SingleStatistics::new(3);
537    /// let mut results = vec![];
538    /// let inputs = [5.0, 2.0, 8.0, 1.0, 7.0, 3.0, 9.0];
539    /// inputs.iter().for_each(|i| {
540    ///     stats.next(*i).median_absolute_deviation().map(|v| results.push(v));
541    /// });
542    ///
543    /// let expected: [f64; 5] = [3.0, 1.0, 1.0, 2.0, 2.0];
544    /// for (i, e) in expected.iter().enumerate() {
545    ///     assert_approx_eq!(e, results[i], 0.1);
546    /// }
547    ///
548    /// ```
549    pub fn median_absolute_deviation(&mut self) -> Option<T> {
550        let median = self.median()?;
551        self.sorted_buf
552            .iter_mut()
553            .zip(self.moments.as_slice())
554            .for_each(|(dev, &x)| *dev = (x - median).abs());
555
556        Some(median_from_sorted_slice(self.sorted_buf.sort()))
557    }
558
559    /// Returns the variance of values in the rolling window
560    ///
561    /// This second-moment statistical measure quantifies dispersion around the mean
562    /// and serves multiple analytical purposes:
563    ///
564    /// - Providing core risk assessment metrics for position sizing decisions
565    /// - Enabling volatility regime detection to adapt methodologies appropriately
566    /// - Filtering signal noise to improve discriminatory power
567    /// - Identifying dispersion-based opportunities in related instrument groups
568    ///
569    /// # Returns
570    ///
571    /// * `Option<T>` - The variance of values in the window, or `None` if the window is not full
572    ///
573    /// # Examples
574    ///
575    /// ```
576    /// # use ta_statistics::SingleStatistics;
577    /// # use assert_approx_eq::assert_approx_eq;
578    /// let mut stats = SingleStatistics::new(3);
579    /// let mut results = vec![];
580    /// let inputs = [25.4, 26.2, 26.0, 26.1, 25.8, 25.9, 26.3, 26.2, 26.5];
581    /// inputs.iter().for_each(|i| {
582    ///     stats.next(*i).variance().map(|v| results.push(v));
583    /// });
584    ///
585    /// let expected: [f64; 7] = [0.1156, 0.0067, 0.0156, 0.0156, 0.0467, 0.0289, 0.0156];
586    /// for (i, e) in expected.iter().enumerate() {
587    ///     assert_approx_eq!(e, results[i], 0.0001);
588    /// }
589    ///
590    /// stats.reset().set_ddof(true);
591    /// results = vec![];
592    /// inputs.iter().for_each(|i| {
593    ///     stats.next(*i).variance().map(|v| results.push(v));
594    /// });
595    ///
596    /// let expected: [f64; 7] = [0.1733, 0.01, 0.0233, 0.0233, 0.07, 0.0433, 0.0233];
597    /// for (i, e) in expected.iter().enumerate() {
598    ///     assert_approx_eq!(e, results[i], 0.0001);
599    /// }
600    /// ```
601    pub fn variance(&self) -> Option<T> {
602        self.moments.variance()
603    }
604
605    /// Returns the standard deviation of values in the rolling window
606    ///
607    /// As the square root of variance, this statistic provides an intuitive measure
608    /// of data dispersion in the original units and enables:
609    ///
610    /// - Setting dynamic volatility thresholds for risk boundaries
611    /// - Detecting potential mean-reversion opportunities when values deviate significantly
612    /// - Normalizing position sizing across different volatility environments
613    /// - Identifying market regime changes to adapt strategic approaches
614    ///
615    /// # Returns
616    ///
617    /// * `Option<T>` - The standard deviation of values in the window, or `None` if the window is not full
618    ///
619    /// # Examples
620    ///
621    /// ```
622    /// # use ta_statistics::SingleStatistics;
623    /// # use assert_approx_eq::assert_approx_eq;
624    /// let mut stats = SingleStatistics::new(3);
625    /// let mut results = vec![];
626    /// let inputs = [25.4, 26.2, 26.0, 26.1, 25.8, 25.9, 26.3, 26.2, 26.5];
627    /// inputs.iter().for_each(|i| {
628    ///     stats.next(*i).stddev().map(|v| results.push(v));
629    /// });
630    ///
631    /// let expected: [f64; 7] = [0.3399, 0.0816, 0.1247, 0.1247, 0.216, 0.17, 0.1247];
632    /// for (i, e) in expected.iter().enumerate() {
633    ///     assert_approx_eq!(e, results[i], 0.0001);
634    /// }
635    ///
636    /// stats.reset().set_ddof(true);
637    /// results = vec![];
638    /// inputs.iter().for_each(|i| {
639    ///     stats.next(*i).stddev().map(|v| results.push(v));
640    /// });
641    ///
642    /// let expected: [f64; 7] = [0.4163, 0.1, 0.1528, 0.1528, 0.2646, 0.2082, 0.1528];
643    /// for (i, e) in expected.iter().enumerate() {
644    ///     assert_approx_eq!(e, results[i], 0.0001);
645    /// }
646    /// ```
647    pub fn stddev(&self) -> Option<T> {
648        self.moments.stddev()
649    }
650
651    /// Returns the z-score of the most recent value relative to the rolling window
652    ///
653    /// Z-scores express how many standard deviations a value deviates from the mean,
654    /// providing a normalized measure that facilitates:
655    ///
656    /// - Statistical arbitrage through relative valuation in correlated series
657    /// - Robust outlier detection across varying market conditions
658    /// - Cross-instrument comparisons on a standardized scale
659    /// - Setting consistent thresholds that remain valid across changing volatility regimes
660    ///
661    /// # Returns
662    ///
663    /// * `Option<T>` - The z-score of the most recent value, or `None` if the window is not full
664    ///
665    /// # Examples
666    ///
667    /// ```
668    /// # use ta_statistics::SingleStatistics;
669    /// # use assert_approx_eq::assert_approx_eq;
670    /// let mut stats = SingleStatistics::new(3);
671    /// let mut results = vec![];
672    /// let inputs = [1.2, -0.7, 3.4, 2.1, -1.5, 0.0, 2.2, -0.3, 1.5, -2.0];
673    /// inputs.iter().for_each(|i| {
674    ///     stats.next(*i).zscore().map(|v| results.push(v));
675    /// });
676    ///
677    /// let expected: [f64; 8] = [1.2535, 0.2923, -1.3671, -0.1355, 1.2943, -0.8374, 0.3482, -1.2129];
678    /// for (i, e) in expected.iter().enumerate() {
679    ///     assert_approx_eq!(e, results[i], 0.0001);
680    /// }
681    ///
682    /// stats.reset().set_ddof(true);
683    /// results = vec![];
684    /// inputs.iter().for_each(|i| {
685    ///     stats.next(*i).zscore().map(|v| results.push(v));
686    /// });
687    ///
688    /// let expected: [f64; 8] = [1.0235, 0.2386, -1.1162, -0.1106, 1.0568, -0.6837, 0.2843, -0.9903];
689    /// for (i, e) in expected.iter().enumerate() {
690    ///     assert_approx_eq!(e, results[i], 0.0001);
691    /// }
692    /// ```
693    pub fn zscore(&self) -> Option<T> {
694        self.moments.zscore()
695    }
696
697    /// Returns the skewness of values in the rolling window
698    ///
699    /// This third-moment statistic measures distribution asymmetry, revealing whether
700    /// extreme values tend toward one direction. A comprehensive analysis of skewness:
701    ///
702    /// - Detects asymmetric return distributions critical for accurate risk modeling
703    /// - Reveals directional biases in market microstructure that may predict future movements
704    /// - Provides early signals of potential regime transitions in volatile environments
705    /// - Enables refined models for derivative pricing beyond simple volatility assumptions
706    ///
707    /// # Returns
708    ///
709    /// * `Option<T>` - The skewness of values in the window, or `None` if the window is not full
710    ///
711    /// # Examples
712    ///
713    /// ```
714    /// # use ta_statistics::SingleStatistics;
715    /// # use assert_approx_eq::assert_approx_eq;
716    /// let mut stats = SingleStatistics::new(4);
717    /// let mut results = vec![];
718    /// let inputs = [25.4, 26.2, 26.0, 26.1, 25.8, 25.9, 26.3, 26.2, 26.5];
719    /// inputs.iter().for_each(|i| {
720    ///     stats.next(*i).skew().map(|v| results.push(v));
721    /// });
722    ///
723    /// let expected: [f64; 6] =  [-0.97941, -0.43465, 0.0, 0.27803, 0.0, -0.32332];
724    /// for (i, e) in expected.iter().enumerate() {
725    ///     assert_approx_eq!(e, results[i], 0.0001);
726    /// }
727    ///
728    /// stats.reset().set_ddof(true);
729    /// results = vec![];
730    /// inputs.iter().for_each(|i| {
731    ///     stats.next(*i).skew().map(|v| results.push(v));
732    /// });
733    ///
734    /// let expected: [f64; 6] = [-1.69639, -0.75284, 0.0, 0.48156, 0.0, -0.56];
735    /// for (i, e) in expected.iter().enumerate() {
736    ///     assert_approx_eq!(e, results[i], 0.0001);
737    /// }
738    /// ```
739    pub fn skew(&self) -> Option<T> {
740        self.moments.skew()
741    }
742
743    /// Returns the kurtosis of values in the rolling window
744    ///
745    /// This fourth-moment statistic measures the 'tailedness' of a distribution, describing
746    /// the frequency of extreme values compared to a normal distribution:
747    ///
748    /// - Quantifies fat-tail risk exposure essential for anticipating extreme market movements
749    /// - Signals potentially exploitable market inefficiencies through distribution analysis
750    /// - Provides critical parameters for selecting appropriate derivatives strategies
751    /// - Enhances Value-at-Risk models by incorporating more realistic tail behavior
752    ///
753    /// # Returns
754    ///
755    /// * `Option<T>` - The kurtosis of values in the window, or `None` if the window is not full
756    ///
757    /// # Examples
758    ///
759    /// ```
760    /// # use ta_statistics::SingleStatistics;
761    /// # use assert_approx_eq::assert_approx_eq;
762    /// let mut stats = SingleStatistics::new(4);
763    /// let mut results = vec![];
764    /// let inputs = [25.4, 26.2, 26.0, 26.1, 25.8, 25.9, 26.3, 26.2, 26.5];
765    /// inputs.iter().for_each(|i| {
766    ///     stats.next(*i).kurt().map(|v| results.push(v));
767    /// });
768    ///
769    /// let expected: [f64; 6] = [-0.7981, -1.1543, -1.3600, -1.4266, -1.7785, -1.0763];
770    /// for (i, e) in expected.iter().enumerate() {
771    ///     assert_approx_eq!(e, results[i], 0.0001);
772    /// }
773    ///
774    /// stats.reset().set_ddof(true);
775    /// results = vec![];
776    /// inputs.iter().for_each(|i| {
777    ///     stats.next(*i).kurt().map(|v| results.push(v));
778    /// });
779    ///
780    /// let expected: [f64; 6] = [3.0144, 0.3429, -1.2, -1.6995, -4.3391, 0.928];
781    /// for (i, e) in expected.iter().enumerate() {
782    ///   assert_approx_eq!(e, results[i], 0.0001);
783    /// }
784    /// ```
785    pub fn kurt(&self) -> Option<T> {
786        self.moments.kurt()
787    }
788
789    /// Returns the slope of the linear regression line
790    ///
791    /// The regression slope represents the rate of change in the best-fit linear model,
792    /// quantifying the directional movement and its magnitude within the data:
793    ///
794    /// - Provides precise measurement of trend strength and conviction
795    /// - Quantifies velocity of change for optimal timing decisions
796    /// - Signals potential reversion points when diverging from historical patterns
797    /// - Measures the relative imbalance between supply and demand forces
798    ///
799    /// # Returns
800    ///
801    /// * `Option<T>` - The slope of the linear regression line, or `None` if the window is not full
802    ///
803    /// # Examples
804    ///
805    /// ```
806    /// # use ta_statistics::SingleStatistics;
807    /// # use assert_approx_eq::assert_approx_eq;
808    /// let mut stats = SingleStatistics::new(5);
809    /// let mut results = vec![];
810    /// let inputs = [10.0, 10.5, 11.2, 10.9, 11.5, 11.9, 12.3, 12.1, 11.8, 12.5];
811    /// inputs.iter().for_each(|i| {
812    ///     stats.next(*i).linreg_slope().map(|v| results.push(v));
813    /// });
814    ///
815    /// let expected: [f64; 6] = [0.34, 0.31, 0.32, 0.32, 0.08, 0.07];
816    /// for (i, e) in expected.iter().enumerate() {
817    ///     assert_approx_eq!(e, results[i], 0.1);
818    /// }
819    /// ```
820    pub fn linreg_slope(&self) -> Option<T> {
821        if !self.moments.is_ready() {
822            return None;
823        }
824
825        let mut s = PairedStatistics::new(self.period());
826        for (i, &x) in self.moments.iter().enumerate() {
827            s.next((x, T::from(i)?));
828        }
829
830        s.beta()
831    }
832
833    /// Returns both slope and intercept of the linear regression line
834    ///
835    /// This comprehensive regression analysis provides the complete linear model,
836    /// enabling more sophisticated trend-based calculations:
837    ///
838    /// - Constructs complete linear models of price or indicator evolution
839    /// - Determines both direction and reference level in a single calculation
840    /// - Enables advanced divergence analysis against actual values
841    /// - Provides foundation for channel-based analytical frameworks
842    ///
843    /// # Returns
844    ///
845    /// * `Option<(T, T)>` - A tuple containing (slope, intercept), or `None` if the window is not full
846    ///
847    /// # Examples
848    ///
849    /// ```
850    /// # use ta_statistics::SingleStatistics;
851    /// # use assert_approx_eq::assert_approx_eq;
852    /// let mut stats = SingleStatistics::new(5);
853    /// let mut ddof = false;
854    /// let mut results = vec![];
855    /// let inputs = [10.0, 10.5, 11.2, 10.9, 11.5, 11.9, 12.3, 12.1, 11.8, 12.5];
856    /// inputs.iter().for_each(|i| {
857    ///     stats.next(*i).linreg_slope_intercept().map(|v| results.push(v));
858    /// });
859    ///
860    /// let expected: [(f64, f64); 6] = [
861    ///     (0.34, 10.14),
862    ///     (0.31, 10.58),
863    ///     (0.32, 10.92),
864    ///     (0.32, 11.1),
865    ///     (0.08, 11.76),
866    ///     (0.07, 11.98),
867    /// ];
868    /// for (i, e) in expected.iter().enumerate() {
869    ///     assert_approx_eq!(e.0, results[i].0, 0.1);
870    ///     assert_approx_eq!(e.1, results[i].1, 0.1);
871    /// }
872    /// ```
873    pub fn linreg_slope_intercept(&self) -> Option<(T, T)> {
874        let (mean, slope) = self.mean().zip(self.linreg_slope())?;
875        let _1 = T::one();
876        self.period_t()
877            .zip(T::from(2))
878            .map(|(p, _2)| (p - _1) / _2)
879            .map(|mt| (slope, mean - slope * mt))
880    }
881
882    /// Returns the y-intercept of the linear regression line
883    ///
884    /// The regression intercept represents the base level or starting point of the
885    /// best-fit linear model, providing key reference information:
886    ///
887    /// - Establishes the theoretical zero-point reference level
888    /// - Complements slope calculations to complete linear projections
889    /// - Assists in fair value determination for mean-reversion models
890    /// - Provides a fixed component for decomposing price into trend and oscillation
891    ///
892    /// # Returns
893    ///
894    /// * `Option<T>` - The y-intercept of the regression line, or `None` if the window is not full
895    ///
896    /// # Examples
897    ///
898    /// ```
899    /// # use ta_statistics::SingleStatistics;
900    /// # use assert_approx_eq::assert_approx_eq;
901    /// let mut stats = SingleStatistics::new(5);
902    /// let mut results = vec![];
903    /// let inputs = [10.0, 10.5, 11.2, 10.9, 11.5, 11.9, 12.3, 12.1, 11.8, 12.5];
904    /// inputs.iter().for_each(|i| {
905    ///     stats.next(*i).linreg_intercept().map(|v| results.push(v));
906    /// });
907    ///
908    /// let expected: [f64; 6] = [10.14, 10.58, 10.92, 11.1, 11.76, 11.98];
909    /// for (i, e) in expected.iter().enumerate() {
910    ///     assert_approx_eq!(e, results[i], 0.1);
911    /// }
912    ///
913    /// ```
914    pub fn linreg_intercept(&self) -> Option<T> {
915        self.linreg_slope_intercept()
916            .map(|(_, intercept)| intercept)
917    }
918
919    /// Returns the angle (in degrees) of the linear regression line
920    ///
921    /// The regression angle converts the slope into degrees, providing a more intuitive
922    /// measure of trend inclination that's bounded between -90 and 90 degrees:
923    ///
924    /// - Offers an easily interpretable measure of trend strength
925    /// - Provides normalized measurement across different scaling contexts
926    /// - Enables clear categorization of trend intensity
927    /// - Simplifies visual representation of directional movement
928    ///
929    /// # Returns
930    ///
931    /// * `Option<T>` - The angle of the regression line in degrees, or `None` if the window is not full
932    ///
933    /// # Examples
934    ///
935    /// ```
936    /// # use ta_statistics::SingleStatistics;
937    /// # use assert_approx_eq::assert_approx_eq;
938    /// let mut stats = SingleStatistics::new(5);
939    /// let mut ddof = false;
940    /// let mut results = vec![];
941    /// let inputs = [10.0, 10.5, 11.2, 10.9, 11.5, 11.9, 12.3, 12.1, 11.8, 12.5];
942    /// inputs.iter().for_each(|i| {
943    ///     stats.next(*i).linreg_angle().map(|v| results.push(v));
944    /// });
945    ///
946    /// let expected: [f64; 6] = [0.3396, 0.3100, 0.3199, 0.3199, 0.0799, 0.0699];
947    /// for (i, e) in expected.iter().enumerate() {
948    ///     assert_approx_eq!(e, results[i], 0.1);
949    /// }
950    ///
951    /// ```
952    pub fn linreg_angle(&self) -> Option<T> {
953        self.linreg_slope().map(|slope| slope.atan())
954    }
955
956    /// Returns the linear regression value (predicted y) for the last position
957    ///
958    /// This calculation provides the expected value at the current position according to
959    /// the best-fit linear model across the window period:
960    ///
961    /// - Establishes theoretical fair value targets for mean-reversion analysis
962    /// - Projects trend trajectory for momentum-based methodologies
963    /// - Filters cyclical noise to extract underlying directional bias
964    /// - Provides basis for divergence analysis between actual and expected values
965    ///
966    /// # Returns
967    ///
968    /// * `Option<T>` - The predicted value at the current position, or `None` if the window is not full
969    ///
970    /// # Examples
971    ///
972    /// ```
973    /// # use ta_statistics::SingleStatistics;
974    /// # use assert_approx_eq::assert_approx_eq;
975    /// let mut stats = SingleStatistics::new(5);
976    /// let mut ddof = false;
977    /// let mut results = vec![];
978    /// let inputs = [10.0, 10.5, 11.2, 10.9, 11.5, 11.9, 12.3, 12.1, 11.8, 12.5];
979    /// inputs.iter().for_each(|i| {
980    ///     stats.next(*i).linreg().map(|v| results.push(v));
981    /// });
982    ///
983    /// let expected: [f64; 6] = [11.5, 11.82, 12.2, 12.38, 12.08, 12.26];
984    /// for (i, e) in expected.iter().enumerate() {
985    ///     assert_approx_eq!(e, results[i], 0.1);
986    /// }
987    ///
988    /// ```
989    pub fn linreg(&self) -> Option<T> {
990        let _1 = T::one();
991        self.linreg_slope_intercept()
992            .zip(self.period_t())
993            .map(|((slope, intercept), period)| slope * (period - _1) + intercept)
994    }
995
996    /// Returns the current drawdown from peak
997    ///
998    /// Measures the percentage decline from the highest observed value to the current value,
999    /// providing crucial insights for risk management and performance evaluation:
1000    ///
1001    /// - Enables dynamic adjustment of risk exposure during challenging conditions
1002    /// - Facilitates strategy rotation based on relative performance metrics
1003    /// - Forms the foundation of capital preservation systems during market stress
1004    /// - Identifies potential opportunities for strategic positioning during dislocations
1005    ///
1006    /// # Returns
1007    ///
1008    /// * `Option<T>` - The current drawdown from peak, or `None` if the window is not full
1009    ///
1010    /// # Examples
1011    ///
1012    /// ```
1013    /// # use ta_statistics::SingleStatistics;
1014    /// # use assert_approx_eq::assert_approx_eq;
1015    /// let mut stats = SingleStatistics::new(3);
1016    /// let mut results = vec![];
1017    /// let inputs = [100.0, 110.0, 105.0, 115.0, 100.0, 95.0, 105.0, 110.0, 100.0];
1018    /// inputs.iter().for_each(|i| {
1019    ///     stats.next(*i).drawdown().map(|v| results.push(v));
1020    /// });
1021    ///
1022    /// let expected: [f64; 7] = [0.045, 0.0, 0.13, 0.174, 0.0, 0.0, 0.091];
1023    /// for (i, e) in expected.iter().enumerate() {
1024    ///     assert_approx_eq!(e, results[i], 0.1);
1025    /// }
1026    /// ```
1027    pub fn drawdown(&mut self) -> Option<T> {
1028        self.max().zip(self.moments.value()).map(|(max, input)| {
1029            if max <= T::zero() || input <= T::zero() {
1030                T::zero()
1031            } else {
1032                ((max - input) / max).max(T::zero())
1033            }
1034        })
1035    }
1036
1037    /// Returns the maximum drawdown in the window
1038    ///
1039    /// Maximum drawdown measures the largest peak-to-trough decline within a time series,
1040    /// serving as a foundational metric for risk assessment and strategy evaluation:
1041    ///
1042    /// - Establishes critical constraints for comprehensive risk management frameworks
1043    /// - Provides an objective metric for evaluating strategy viability under stress
1044    /// - Informs position sizing parameters to maintain proportional risk exposure
1045    /// - Contributes valuable input to market regime classification models
1046    ///
1047    /// # Returns
1048    ///
1049    /// * `Option<T>` - The maximum drawdown in the window, or `None` if the window is not full
1050    ///
1051    /// # Examples
1052    ///
1053    /// ```
1054    /// # use ta_statistics::SingleStatistics;
1055    /// # use assert_approx_eq::assert_approx_eq;
1056    /// let mut stats = SingleStatistics::new(3);
1057    /// let mut results = vec![];
1058    /// let inputs = [100.0, 110.0, 105.0, 115.0, 100.0, 95.0, 105.0, 110.0, 100.0];
1059    /// inputs.iter().for_each(|i| {
1060    ///     stats.next(*i).max_drawdown().map(|v| results.push(v));
1061    /// });
1062    ///
1063    /// let expected: [f64; 7] = [0.045, 0.045, 0.13, 0.174, 0.174, 0.174, 0.174];
1064    /// for (i, e) in expected.iter().enumerate() {
1065    ///     assert_approx_eq!(e, results[i], 0.1);
1066    /// }
1067    ///
1068    /// ```
1069    pub fn max_drawdown(&mut self) -> Option<T> {
1070        let drawdown = self.drawdown()?;
1071        self.max_drawdown = match self.max_drawdown {
1072            Some(md) => Some(md.max(drawdown)),
1073            None => Some(drawdown),
1074        };
1075        self.max_drawdown
1076    }
1077
1078    /// Returns the difference between the last and first values
1079    ///
1080    /// This fundamental calculation of absolute change between two points provides
1081    /// essential directional and magnitude information for time series analysis:
1082    ///
1083    /// - Enables momentum measurement for trend strength evaluation
1084    /// - Quantifies rate-of-change to optimize timing decisions
1085    /// - Serves as a building block for pattern recognition in sequential data
1086    /// - Provides critical inputs for calculating hedge ratios and exposure management
1087    ///
1088    /// # Returns
1089    ///
1090    /// * `Option<T>` - The difference between values, or `None` if the window is not full
1091    ///
1092    /// # Examples
1093    ///
1094    /// ```
1095    /// # use ta_statistics::SingleStatistics;
1096    /// # use assert_approx_eq::assert_approx_eq;
1097    /// let mut stats = SingleStatistics::new(3);
1098    /// let mut results = vec![];
1099    /// let inputs = [100.0, 102.0, 105.0, 101.0, 98.0];
1100    /// inputs.iter().for_each(|i| {
1101    ///     stats.next(*i).diff().map(|v| results.push(v));
1102    /// });
1103    ///
1104    /// let expected: [f64; 2] = [1.0, -4.0];
1105    /// for (i, e) in expected.iter().enumerate() {
1106    ///     assert_approx_eq!(e, results[i], 0.1);
1107    /// }
1108    ///
1109    /// ```
1110    pub fn diff(&self) -> Option<T> {
1111        self.moments
1112            .value()
1113            .zip(self.moments.popped())
1114            .map(|(input, popped)| input - popped)
1115    }
1116
1117    /// Returns the percentage change between the first and last values
1118    ///
1119    /// Percentage change normalizes absolute changes by the starting value, enabling
1120    /// meaningful comparisons across different scales and measurement contexts:
1121    ///
1122    /// - Facilitates cross-asset performance comparison for relative strength analysis
1123    /// - Provides risk-normalized return metrics that account for initial exposure
1124    /// - Enables position sizing that properly adjusts for varying volatility environments
1125    /// - Serves as a key input for comparative performance evaluation across related groups
1126    ///
1127    /// # Returns
1128    ///
1129    /// * `Option<T>` - The percentage change, or `None` if the window is not full
1130    ///
1131    /// # Examples
1132    ///
1133    /// ```
1134    /// # use ta_statistics::SingleStatistics;
1135    /// # use assert_approx_eq::assert_approx_eq;
1136    /// let mut stats = SingleStatistics::new(3);
1137    /// let mut results = vec![];
1138    /// let inputs = [100.0, 105.0, 103.0, 106.0, 110.0, 108.0];
1139    /// inputs.iter().for_each(|i| {
1140    ///     stats.next(*i).pct_change().map(|v| results.push(v));
1141    /// });
1142    ///
1143    /// let expected: [f64; 3] = [0.06, 0.04761905, 0.04854369];
1144    /// for (i, e) in expected.iter().enumerate() {
1145    ///     assert_approx_eq!(e, results[i], 0.1);
1146    /// }
1147    ///
1148    /// ```
1149    pub fn pct_change(&self) -> Option<T> {
1150        self.diff()
1151            .zip(self.moments.popped())
1152            .and_then(|(diff, popped)| {
1153                if popped.is_zero() {
1154                    None
1155                } else {
1156                    Some(diff / popped)
1157                }
1158            })
1159    }
1160
1161    /// Returns the logarithmic return between the first and last values
1162    ///
1163    /// Logarithmic returns (continuous returns) offer mathematical advantages over simple
1164    /// returns, particularly for time series analysis:
1165    ///
1166    /// - Provides time-additive metrics that can be properly aggregated across periods
1167    /// - Normalizes signals in relative-value analysis of related securities
1168    /// - Creates a more consistent volatility scale regardless of price levels
1169    /// - Improves accuracy in long-horizon analyses through proper handling of compounding
1170    ///
1171    /// # Returns
1172    ///
1173    /// * `Option<T>` - The logarithmic return, or `None` if the window is not full
1174    ///
1175    /// # Examples
1176    ///
1177    /// ```
1178    /// # use ta_statistics::SingleStatistics;
1179    /// # use assert_approx_eq::assert_approx_eq;
1180    /// let mut stats = SingleStatistics::new(3);
1181    /// let mut results = vec![];
1182    /// let inputs = [100.0, 105.0, 103.0, 106.0, 110.0, 108.0];
1183    /// inputs.iter().for_each(|i| {
1184    ///     stats.next(*i).log_return().map(|v| results.push(v));
1185    /// });
1186    ///
1187    /// let expected: [f64; 3] = [0.05827, 0.04652, 0.04727];
1188    /// for (i, e) in expected.iter().enumerate() {
1189    ///     assert_approx_eq!(e, results[i], 0.1);
1190    /// }
1191    ///
1192    /// ```
1193    pub fn log_return(&self) -> Option<T> {
1194        self.moments
1195            .value()
1196            .zip(self.moments.popped())
1197            .and_then(|(current, popped)| {
1198                if popped <= T::zero() || current <= T::zero() {
1199                    None
1200                } else {
1201                    Some(current.ln() - popped.ln())
1202                }
1203            })
1204    }
1205
1206    /// Returns the quantile of the values in the window
1207    ///
1208    /// # Arguments
1209    ///
1210    /// * `q` - The quantile to calculate
1211    ///
1212    /// # Returns
1213    ///
1214    /// * `Option<T>` - The quantile, or `None` if the window is not full
1215    ///
1216    /// # Examples
1217    ///
1218    /// ```
1219    /// # use ta_statistics::SingleStatistics;
1220    /// # use assert_approx_eq::assert_approx_eq;
1221    /// let mut stats = SingleStatistics::new(3);
1222    /// let mut results = vec![];
1223    /// let inputs = [10.0, 20.0, 30.0, 40.0, 50.0];
1224    /// inputs.iter().for_each(|i| {
1225    ///     stats.next(*i).quantile(0.5).map(|v| results.push(v));
1226    /// });
1227    ///
1228    /// let expected: [f64; 3] = [20.0, 30.0, 40.0];
1229    /// for (i, e) in expected.iter().enumerate() {
1230    ///     assert_approx_eq!(e, results[i], 0.1);
1231    /// }
1232    /// ```
1233    pub fn quantile(&mut self, q: f64) -> Option<T> {
1234        if !self.moments.is_ready() || !(0.0..=1.0).contains(&q) {
1235            return None;
1236        }
1237        let period = self.period();
1238        let sorted = self.sorted_buf();
1239        quantile_from_sorted_slice(sorted, q, period)
1240    }
1241
1242    /// Returns the interquartile range of the values in the window
1243    ///
1244    /// # Returns
1245    ///
1246    /// * `Option<T>` - The interquartile range, or `None` if the window is not full
1247    ///
1248    /// # Examples
1249    ///
1250    /// ```
1251    /// # use ta_statistics::SingleStatistics;
1252    /// # use assert_approx_eq::assert_approx_eq;
1253    /// let mut stats = SingleStatistics::new(3);
1254    /// let mut results = vec![];
1255    /// let inputs = [10.0, 20.0, 30.0, 40.0, 50.0];
1256    /// inputs.iter().for_each(|i| {
1257    ///     stats.next(*i).iqr().map(|v| results.push(v));
1258    /// });
1259    ///
1260    /// let expected: [f64; 3] = [10.0, 10.0, 10.0];
1261    /// for (i, e) in expected.iter().enumerate() {
1262    ///     assert_approx_eq!(e, results[i], 0.1);
1263    /// }
1264    ///
1265    /// ```
1266    pub fn iqr(&mut self) -> Option<T> {
1267        if !self.moments.is_ready() {
1268            return None;
1269        }
1270
1271        let period = self.period();
1272        let sorted = self.sorted_buf();
1273
1274        let q1 = quantile_from_sorted_slice(sorted, 0.25, period);
1275        let q3 = quantile_from_sorted_slice(sorted, 0.75, period);
1276
1277        q1.zip(q3).map(|(q1, q3)| q3 - q1)
1278    }
1279}