velora_ta/
types.rs

1//! Common types used by technical indicators.
2
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5
6/// Value returned by a single-value indicator at a specific point in time.
7#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
8pub struct IndicatorValue {
9    /// Timestamp of this indicator value
10    pub timestamp: DateTime<Utc>,
11    /// The indicator value
12    pub value: f64,
13}
14
15impl IndicatorValue {
16    /// Create a new indicator value.
17    pub fn new(timestamp: DateTime<Utc>, value: f64) -> Self {
18        Self { timestamp, value }
19    }
20}
21
22/// Multi-value indicator output (e.g., Bollinger Bands returns upper/middle/lower).
23#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
24pub struct MultiIndicatorValue {
25    /// Timestamp of this indicator value
26    pub timestamp: DateTime<Utc>,
27    /// The indicator values (order depends on specific indicator)
28    pub values: Vec<f64>,
29}
30
31impl MultiIndicatorValue {
32    /// Create a new multi-indicator value.
33    pub fn new(timestamp: DateTime<Utc>, values: Vec<f64>) -> Self {
34        Self { timestamp, values }
35    }
36
37    /// Get a specific value by index.
38    pub fn get(&self, index: usize) -> Option<f64> {
39        self.values.get(index).copied()
40    }
41
42    /// Get the number of values.
43    pub fn len(&self) -> usize {
44        self.values.len()
45    }
46
47    /// Check if there are no values.
48    pub fn is_empty(&self) -> bool {
49        self.values.is_empty()
50    }
51}
52
53impl From<Vec<f64>> for MultiIndicatorValue {
54    fn from(values: Vec<f64>) -> Self {
55        Self {
56            timestamp: Utc::now(),
57            values,
58        }
59    }
60}
61
62/// Price type to extract from OHLCV data.
63#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
64pub enum PriceType {
65    /// Open price
66    Open,
67    /// High price
68    High,
69    /// Low price
70    Low,
71    /// Close price
72    #[default]
73    Close,
74    /// Typical price: (High + Low + Close) / 3
75    Typical,
76    /// Weighted close: (High + Low + Close + Close) / 4
77    Weighted,
78    /// Average price: (High + Low) / 2
79    Average,
80    /// Median price: (High + Low) / 2 (same as Average)
81    Median,
82}
83
84impl PriceType {
85    /// Extract price from OHLC values.
86    ///
87    /// # Arguments
88    ///
89    /// * `open` - Open price
90    /// * `high` - High price
91    /// * `low` - Low price
92    /// * `close` - Close price
93    pub fn extract(&self, open: f64, high: f64, low: f64, close: f64) -> f64 {
94        match self {
95            PriceType::Open => open,
96            PriceType::High => high,
97            PriceType::Low => low,
98            PriceType::Close => close,
99            PriceType::Typical => (high + low + close) / 3.0,
100            PriceType::Weighted => (high + low + close + close) / 4.0,
101            PriceType::Average | PriceType::Median => (high + low) / 2.0,
102        }
103    }
104}
105
106/// Simple OHLC bar for indicators that need high/low data.
107///
108/// This is a lightweight structure used internally by indicators like
109/// Stochastic, Williams %R, and ATR. It's separate from velora-core's
110/// Candle type to keep this library standalone.
111#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
112pub struct OhlcBar {
113    /// Open price
114    pub open: f64,
115    /// High price
116    pub high: f64,
117    /// Low price
118    pub low: f64,
119    /// Close price
120    pub close: f64,
121}
122
123impl OhlcBar {
124    /// Create a new OHLC bar.
125    pub fn new(open: f64, high: f64, low: f64, close: f64) -> Self {
126        Self {
127            open,
128            high,
129            low,
130            close,
131        }
132    }
133
134    /// Extract a specific price type from this bar.
135    pub fn price(&self, price_type: PriceType) -> f64 {
136        price_type.extract(self.open, self.high, self.low, self.close)
137    }
138
139    /// Get typical price: (High + Low + Close) / 3
140    pub fn typical_price(&self) -> f64 {
141        (self.high + self.low + self.close) / 3.0
142    }
143
144    /// Get range: High - Low
145    pub fn range(&self) -> f64 {
146        self.high - self.low
147    }
148}
149
150#[cfg(test)]
151mod tests {
152    use super::*;
153
154    #[test]
155    fn test_price_type_extraction() {
156        let open = 100.0;
157        let high = 105.0;
158        let low = 95.0;
159        let close = 102.0;
160
161        assert_eq!(PriceType::Open.extract(open, high, low, close), 100.0);
162        assert_eq!(PriceType::High.extract(open, high, low, close), 105.0);
163        assert_eq!(PriceType::Low.extract(open, high, low, close), 95.0);
164        assert_eq!(PriceType::Close.extract(open, high, low, close), 102.0);
165
166        // Typical: (105 + 95 + 102) / 3 = 100.666...
167        assert!((PriceType::Typical.extract(open, high, low, close) - 100.666666).abs() < 0.001);
168
169        // Weighted: (105 + 95 + 102 + 102) / 4 = 101.0
170        assert_eq!(PriceType::Weighted.extract(open, high, low, close), 101.0);
171
172        // Average/Median: (105 + 95) / 2 = 100.0
173        assert_eq!(PriceType::Average.extract(open, high, low, close), 100.0);
174        assert_eq!(PriceType::Median.extract(open, high, low, close), 100.0);
175    }
176
177    #[test]
178    fn test_indicator_value() {
179        let timestamp = Utc::now();
180        let value = IndicatorValue::new(timestamp, 42.0);
181
182        assert_eq!(value.timestamp, timestamp);
183        assert_eq!(value.value, 42.0);
184    }
185
186    #[test]
187    fn test_multi_indicator_value() {
188        let timestamp = Utc::now();
189        let values = vec![1.0, 2.0, 3.0];
190        let multi = MultiIndicatorValue::new(timestamp, values.clone());
191
192        assert_eq!(multi.timestamp, timestamp);
193        assert_eq!(multi.len(), 3);
194        assert!(!multi.is_empty());
195        assert_eq!(multi.get(0), Some(1.0));
196        assert_eq!(multi.get(1), Some(2.0));
197        assert_eq!(multi.get(2), Some(3.0));
198        assert_eq!(multi.get(3), None);
199    }
200}