Skip to main content

finance_solution/stocks/ta/
common.rs

1//! Shared TA helpers (validation, warm-up cells, rolling stats).
2
3use crate::util::error::{require_finite, FinanceError, FinanceResult};
4
5pub(crate) fn opt_cell(v: Option<f64>) -> String {
6    match v {
7        Some(x) => x.to_string(),
8        None => "n/a".to_string(),
9    }
10}
11
12pub(crate) fn validate_series(name: &'static str, xs: &[f64]) -> FinanceResult<()> {
13    if xs.is_empty() {
14        return Err(FinanceError::EmptyInput { what: name });
15    }
16    for &x in xs {
17        require_finite(name, x)?;
18    }
19    Ok(())
20}
21
22pub(crate) fn validate_positive_volume(volume: &[f64]) -> FinanceResult<()> {
23    validate_series("volume", volume)?;
24    for &v in volume {
25        if v < 0.0 {
26            return Err(FinanceError::InvalidCashflow {
27                message: "volume must be non-negative",
28            });
29        }
30    }
31    Ok(())
32}
33
34pub(crate) fn require_same_len(a: &[f64], b: &[f64], context: &'static str) -> FinanceResult<()> {
35    if a.len() != b.len() {
36        return Err(FinanceError::LengthMismatch {
37            left: a.len(),
38            right: b.len(),
39            context,
40        });
41    }
42    Ok(())
43}
44
45pub(crate) fn require_hlc(high: &[f64], low: &[f64], close: &[f64]) -> FinanceResult<()> {
46    validate_series("high", high)?;
47    validate_series("low", low)?;
48    validate_series("close", close)?;
49    require_same_len(high, low, "high/low")?;
50    require_same_len(high, close, "high/close")?;
51    for i in 0..high.len() {
52        if high[i] < low[i] {
53            return Err(FinanceError::InvalidCashflow {
54                message: "high must be >= low for each bar",
55            });
56        }
57    }
58    Ok(())
59}
60
61/// Which denominator to use for window standard deviation (Bollinger, etc.).
62#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
63pub enum StdevKind {
64    /// Unbiased sample stdev: divide by `n - 1`. **Default for Bollinger** (common in finance).
65    #[default]
66    Sample,
67    /// Population stdev: divide by `n`.
68    Population,
69}
70
71/// Standard deviation of a full window. `None` if the kind cannot be computed
72/// (`Sample` needs `n ≥ 2`; `Population` needs `n ≥ 1`).
73pub(crate) fn window_stdev(window: &[f64], kind: StdevKind) -> Option<f64> {
74    let n = window.len();
75    match kind {
76        StdevKind::Sample if n < 2 => return None,
77        StdevKind::Population if n < 1 => return None,
78        _ => {}
79    }
80    let mean = window.iter().sum::<f64>() / n as f64;
81    let denom = match kind {
82        StdevKind::Sample => n as f64 - 1.0,
83        StdevKind::Population => n as f64,
84    };
85    let var = window.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / denom;
86    Some(var.sqrt())
87}
88
89/// Sample standard deviation (`n - 1`). `None` if `n < 2`.
90pub(crate) fn sample_stdev(window: &[f64]) -> Option<f64> {
91    window_stdev(window, StdevKind::Sample)
92}
93
94/// True range for bar `i` (needs previous close when `i > 0`).
95pub(crate) fn true_range(high: f64, low: f64, prev_close: Option<f64>) -> f64 {
96    let hl = high - low;
97    match prev_close {
98        None => hl,
99        Some(pc) => hl.max((high - pc).abs()).max((low - pc).abs()),
100    }
101}