@devanshhq/indica 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/.pm/indica-research-report.md +390 -0
  2. package/ARCHITECTURE.md +470 -0
  3. package/Cargo.lock +1 -1
  4. package/Cargo.toml +2 -2
  5. package/README.md +98 -109
  6. package/package.json +2 -2
  7. package/src/batch/compute.rs +165 -0
  8. package/src/batch/mod.rs +2 -0
  9. package/src/batch/screen.rs +176 -0
  10. package/src/core/mod.rs +3 -0
  11. package/src/core/traits.rs +37 -0
  12. package/src/core/types.rs +22 -0
  13. package/src/core/utils.rs +43 -0
  14. package/src/indicators/india/circuit.rs +101 -0
  15. package/src/indicators/india/delivery.rs +116 -0
  16. package/src/indicators/india/mod.rs +2 -0
  17. package/src/indicators/mod.rs +6 -0
  18. package/src/indicators/momentum/macd.rs +123 -0
  19. package/src/indicators/momentum/mod.rs +3 -0
  20. package/src/indicators/momentum/rsi.rs +130 -0
  21. package/src/indicators/momentum/stochastic.rs +99 -0
  22. package/src/indicators/support_resistance/mod.rs +1 -0
  23. package/src/indicators/support_resistance/pivot.rs +40 -0
  24. package/src/indicators/trend/adx.rs +168 -0
  25. package/src/indicators/trend/ema.rs +110 -0
  26. package/src/indicators/trend/mod.rs +4 -0
  27. package/src/indicators/trend/sma.rs +75 -0
  28. package/src/indicators/trend/supertrend.rs +193 -0
  29. package/src/indicators/volatility/atr.rs +51 -0
  30. package/src/{bollinger.rs → indicators/volatility/bollinger.rs} +6 -22
  31. package/src/indicators/volatility/mod.rs +2 -0
  32. package/src/indicators/volume/mod.rs +3 -0
  33. package/src/indicators/volume/obv.rs +44 -0
  34. package/src/{volume.rs → indicators/volume/volume_trend.rs} +9 -27
  35. package/src/indicators/volume/vwap.rs +53 -0
  36. package/src/lib.rs +71 -21
  37. package/src/signals/engine.rs +139 -0
  38. package/src/signals/mod.rs +4 -0
  39. package/src/signals/presets.rs +109 -0
  40. package/src/signals/rules.rs +333 -0
  41. package/src/signals/types.rs +50 -0
  42. package/index.d.ts +0 -66
  43. package/indica.node +0 -0
  44. package/src/atr.rs +0 -66
  45. package/src/batch.rs +0 -139
  46. package/src/macd.rs +0 -132
  47. package/src/moving_avg.rs +0 -71
  48. package/src/napi_bindings.rs +0 -166
  49. package/src/pivot.rs +0 -58
  50. package/src/relative_strength.rs +0 -67
  51. package/src/rsi.rs +0 -74
  52. package/src/utils.rs +0 -17
@@ -0,0 +1,168 @@
1
+ use crate::core::traits::Indicator;
2
+ use crate::core::types::Candle;
3
+ use crate::core::utils::{round, wilders_step};
4
+
5
+ /// Streaming ADX (Average Directional Index).
6
+ /// Measures trend strength regardless of direction.
7
+ pub struct Adx {
8
+ period: usize,
9
+ count: usize,
10
+ prev_high: f64,
11
+ prev_low: f64,
12
+ prev_close: f64,
13
+ plus_dm_sum: f64,
14
+ minus_dm_sum: f64,
15
+ tr_sum: f64,
16
+ smooth_plus_dm: f64,
17
+ smooth_minus_dm: f64,
18
+ smooth_tr: f64,
19
+ dx_sum: f64,
20
+ dx_count: usize,
21
+ adx: f64,
22
+ }
23
+
24
+ impl Adx {
25
+ pub fn new(period: usize) -> Self {
26
+ Self {
27
+ period,
28
+ count: 0,
29
+ prev_high: 0.0,
30
+ prev_low: 0.0,
31
+ prev_close: 0.0,
32
+ plus_dm_sum: 0.0,
33
+ minus_dm_sum: 0.0,
34
+ tr_sum: 0.0,
35
+ smooth_plus_dm: 0.0,
36
+ smooth_minus_dm: 0.0,
37
+ smooth_tr: 0.0,
38
+ dx_sum: 0.0,
39
+ dx_count: 0,
40
+ adx: 0.0,
41
+ }
42
+ }
43
+ }
44
+
45
+ impl Indicator for Adx {
46
+ type Output = f64;
47
+
48
+ fn update(&mut self, candle: &Candle) -> Option<f64> {
49
+ self.count += 1;
50
+
51
+ if self.count == 1 {
52
+ self.prev_high = candle.high;
53
+ self.prev_low = candle.low;
54
+ self.prev_close = candle.close;
55
+ return None;
56
+ }
57
+
58
+ let plus_dm = (candle.high - self.prev_high).max(0.0);
59
+ let minus_dm = (self.prev_low - candle.low).max(0.0);
60
+ let (plus_dm, minus_dm) = if plus_dm > minus_dm {
61
+ (plus_dm, 0.0)
62
+ } else {
63
+ (0.0, minus_dm)
64
+ };
65
+
66
+ let tr = (candle.high - candle.low)
67
+ .max((candle.high - self.prev_close).abs())
68
+ .max((candle.low - self.prev_close).abs());
69
+
70
+ self.prev_high = candle.high;
71
+ self.prev_low = candle.low;
72
+ self.prev_close = candle.close;
73
+
74
+ let idx = self.count - 1; // 1-based data index
75
+
76
+ if idx <= self.period {
77
+ self.plus_dm_sum += plus_dm;
78
+ self.minus_dm_sum += minus_dm;
79
+ self.tr_sum += tr;
80
+
81
+ if idx == self.period {
82
+ self.smooth_plus_dm = self.plus_dm_sum;
83
+ self.smooth_minus_dm = self.minus_dm_sum;
84
+ self.smooth_tr = self.tr_sum;
85
+ } else {
86
+ return None;
87
+ }
88
+ } else {
89
+ self.smooth_plus_dm = wilders_step(self.smooth_plus_dm, plus_dm, self.period);
90
+ self.smooth_minus_dm = wilders_step(self.smooth_minus_dm, minus_dm, self.period);
91
+ self.smooth_tr = wilders_step(self.smooth_tr, tr, self.period);
92
+ }
93
+
94
+ if self.smooth_tr < f64::EPSILON {
95
+ return None;
96
+ }
97
+
98
+ let plus_di = 100.0 * self.smooth_plus_dm / self.smooth_tr;
99
+ let minus_di = 100.0 * self.smooth_minus_dm / self.smooth_tr;
100
+ let di_sum = plus_di + minus_di;
101
+
102
+ if di_sum < f64::EPSILON {
103
+ return None;
104
+ }
105
+
106
+ let dx = 100.0 * (plus_di - minus_di).abs() / di_sum;
107
+
108
+ self.dx_count += 1;
109
+ if self.dx_count < self.period {
110
+ self.dx_sum += dx;
111
+ None
112
+ } else if self.dx_count == self.period {
113
+ self.dx_sum += dx;
114
+ self.adx = self.dx_sum / self.period as f64;
115
+ Some(round(self.adx, 2))
116
+ } else {
117
+ self.adx = wilders_step(self.adx, dx, self.period);
118
+ Some(round(self.adx, 2))
119
+ }
120
+ }
121
+
122
+ fn reset(&mut self) {
123
+ *self = Self::new(self.period);
124
+ }
125
+ }
126
+
127
+ /// Convenience function: ADX from slices.
128
+ #[must_use]
129
+ pub fn adx(highs: &[f64], lows: &[f64], closes: &[f64], period: usize) -> Option<f64> {
130
+ let len = closes.len();
131
+ if len < 2 * period + 1 || highs.len() < len || lows.len() < len || period == 0 {
132
+ return None;
133
+ }
134
+ let candles: Vec<Candle> = (0..len)
135
+ .map(|i| Candle {
136
+ open: closes[i],
137
+ high: highs[i],
138
+ low: lows[i],
139
+ close: closes[i],
140
+ volume: 0.0,
141
+ })
142
+ .collect();
143
+ let mut ind = Adx::new(period);
144
+ ind.compute_last(&candles)
145
+ }
146
+
147
+ #[cfg(test)]
148
+ mod tests {
149
+ use super::*;
150
+
151
+ #[test]
152
+ fn adx_strong_trend() {
153
+ let closes: Vec<f64> = (0..50).map(|i| 100.0 + i as f64 * 2.0).collect();
154
+ let highs: Vec<f64> = closes.iter().map(|c| c + 1.0).collect();
155
+ let lows: Vec<f64> = closes.iter().map(|c| c - 1.0).collect();
156
+ let result = adx(&highs, &lows, &closes, 14).unwrap();
157
+ assert!(
158
+ result > 20.0,
159
+ "ADX should be high in strong trend: {}",
160
+ result
161
+ );
162
+ }
163
+
164
+ #[test]
165
+ fn adx_insufficient() {
166
+ assert!(adx(&[1.0; 10], &[1.0; 10], &[1.0; 10], 14).is_none());
167
+ }
168
+ }
@@ -0,0 +1,110 @@
1
+ use crate::core::traits::Indicator;
2
+ use crate::core::types::Candle;
3
+ use crate::core::utils::{ema_k, ema_step};
4
+
5
+ /// Streaming Exponential Moving Average.
6
+ pub struct Ema {
7
+ period: usize,
8
+ k: f64,
9
+ count: usize,
10
+ sum: f64,
11
+ value: f64,
12
+ }
13
+
14
+ impl Ema {
15
+ pub fn new(period: usize) -> Self {
16
+ Self {
17
+ period,
18
+ k: ema_k(period),
19
+ count: 0,
20
+ sum: 0.0,
21
+ value: 0.0,
22
+ }
23
+ }
24
+ }
25
+
26
+ impl Indicator for Ema {
27
+ type Output = f64;
28
+
29
+ fn update(&mut self, candle: &Candle) -> Option<f64> {
30
+ self.count += 1;
31
+ if self.count < self.period {
32
+ self.sum += candle.close;
33
+ None
34
+ } else if self.count == self.period {
35
+ self.sum += candle.close;
36
+ self.value = self.sum / self.period as f64; // Seed with SMA
37
+ Some(self.value)
38
+ } else {
39
+ self.value = ema_step(self.value, candle.close, self.k);
40
+ Some(self.value)
41
+ }
42
+ }
43
+
44
+ fn reset(&mut self) {
45
+ self.count = 0;
46
+ self.sum = 0.0;
47
+ self.value = 0.0;
48
+ }
49
+ }
50
+
51
+ /// Convenience function: EMA of close values.
52
+ #[must_use]
53
+ pub fn ema(values: &[f64], period: usize) -> Option<f64> {
54
+ if values.len() < period || period == 0 {
55
+ return None;
56
+ }
57
+ let candles: Vec<Candle> = values.iter().map(|&c| Candle::from_close(c)).collect();
58
+ let mut ind = Ema::new(period);
59
+ ind.compute_last(&candles)
60
+ }
61
+
62
+ /// Full EMA series (used internally by MACD).
63
+ pub(crate) fn ema_series(values: &[f64], period: usize) -> Option<Vec<f64>> {
64
+ if values.len() < period || period == 0 {
65
+ return None;
66
+ }
67
+ let candles: Vec<Candle> = values.iter().map(|&c| Candle::from_close(c)).collect();
68
+ let mut ind = Ema::new(period);
69
+ let results: Vec<f64> = ind.compute(&candles).into_iter().flatten().collect();
70
+ if results.is_empty() {
71
+ None
72
+ } else {
73
+ Some(results)
74
+ }
75
+ }
76
+
77
+ #[cfg(test)]
78
+ mod tests {
79
+ use super::*;
80
+
81
+ #[test]
82
+ fn ema_fn_basic() {
83
+ let data = vec![10.0, 11.0, 12.0, 13.0, 14.0, 15.0];
84
+ let result = ema(&data, 3).unwrap();
85
+ assert!((result - 14.0).abs() < 0.01);
86
+ }
87
+
88
+ #[test]
89
+ fn ema_fn_insufficient() {
90
+ assert_eq!(ema(&[1.0], 5), None);
91
+ }
92
+
93
+ #[test]
94
+ fn ema_streaming() {
95
+ let mut ind = Ema::new(3);
96
+ assert_eq!(ind.update(&Candle::from_close(10.0)), None);
97
+ assert_eq!(ind.update(&Candle::from_close(11.0)), None);
98
+ let seed = ind.update(&Candle::from_close(12.0)).unwrap();
99
+ assert!((seed - 11.0).abs() < 0.01); // SMA seed
100
+ let next = ind.update(&Candle::from_close(13.0)).unwrap();
101
+ assert!((next - 12.0).abs() < 0.01); // 13*0.5 + 11*0.5
102
+ }
103
+
104
+ #[test]
105
+ fn ema_series_works() {
106
+ let data = vec![10.0, 11.0, 12.0, 13.0, 14.0, 15.0];
107
+ let series = ema_series(&data, 3).unwrap();
108
+ assert_eq!(series.len(), 4);
109
+ }
110
+ }
@@ -0,0 +1,4 @@
1
+ pub mod adx;
2
+ pub mod ema;
3
+ pub mod sma;
4
+ pub mod supertrend;
@@ -0,0 +1,75 @@
1
+ use crate::core::traits::Indicator;
2
+ use crate::core::types::Candle;
3
+ use std::collections::VecDeque;
4
+
5
+ /// Streaming Simple Moving Average.
6
+ pub struct Sma {
7
+ period: usize,
8
+ window: VecDeque<f64>,
9
+ }
10
+
11
+ impl Sma {
12
+ pub fn new(period: usize) -> Self {
13
+ Self {
14
+ period,
15
+ window: VecDeque::with_capacity(period),
16
+ }
17
+ }
18
+ }
19
+
20
+ impl Indicator for Sma {
21
+ type Output = f64;
22
+
23
+ fn update(&mut self, candle: &Candle) -> Option<f64> {
24
+ self.window.push_back(candle.close);
25
+ if self.window.len() > self.period {
26
+ self.window.pop_front();
27
+ }
28
+ if self.window.len() == self.period {
29
+ Some(self.window.iter().sum::<f64>() / self.period as f64)
30
+ } else {
31
+ None
32
+ }
33
+ }
34
+
35
+ fn reset(&mut self) {
36
+ self.window.clear();
37
+ }
38
+ }
39
+
40
+ /// Convenience function: SMA of the last `period` close values.
41
+ #[must_use]
42
+ pub fn sma(closes: &[f64], period: usize) -> Option<f64> {
43
+ if closes.len() < period || period == 0 {
44
+ return None;
45
+ }
46
+ let slice = &closes[closes.len() - period..];
47
+ Some(slice.iter().sum::<f64>() / period as f64)
48
+ }
49
+
50
+ #[cfg(test)]
51
+ mod tests {
52
+ use super::*;
53
+
54
+ #[test]
55
+ fn sma_fn_basic() {
56
+ assert_eq!(sma(&[1.0, 2.0, 3.0, 4.0, 5.0], 3), Some(4.0));
57
+ assert_eq!(sma(&[10.0, 20.0, 30.0], 3), Some(20.0));
58
+ }
59
+
60
+ #[test]
61
+ fn sma_fn_insufficient() {
62
+ assert_eq!(sma(&[1.0, 2.0], 5), None);
63
+ assert_eq!(sma(&[], 1), None);
64
+ }
65
+
66
+ #[test]
67
+ fn sma_streaming() {
68
+ let mut ind = Sma::new(3);
69
+ assert_eq!(ind.update(&Candle::from_close(1.0)), None);
70
+ assert_eq!(ind.update(&Candle::from_close(2.0)), None);
71
+ assert_eq!(ind.update(&Candle::from_close(3.0)), Some(2.0));
72
+ assert_eq!(ind.update(&Candle::from_close(4.0)), Some(3.0));
73
+ assert_eq!(ind.update(&Candle::from_close(5.0)), Some(4.0));
74
+ }
75
+ }
@@ -0,0 +1,193 @@
1
+ use crate::core::traits::Indicator;
2
+ use crate::core::types::Candle;
3
+ use crate::core::utils::{round, wilders_step};
4
+
5
+ /// Supertrend result.
6
+ #[derive(Debug, Clone, Copy)]
7
+ pub struct SupertrendResult {
8
+ pub value: f64,
9
+ pub direction: SupertrendDirection,
10
+ }
11
+
12
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
13
+ pub enum SupertrendDirection {
14
+ Up,
15
+ Down,
16
+ }
17
+
18
+ impl std::fmt::Display for SupertrendDirection {
19
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
20
+ match self {
21
+ Self::Up => write!(f, "up"),
22
+ Self::Down => write!(f, "down"),
23
+ }
24
+ }
25
+ }
26
+
27
+ /// Streaming Supertrend indicator.
28
+ pub struct Supertrend {
29
+ atr_period: usize,
30
+ multiplier: f64,
31
+ count: usize,
32
+ // ATR state
33
+ prev_close: f64,
34
+ tr_sum: f64,
35
+ atr: f64,
36
+ // Supertrend state
37
+ prev_upper: f64,
38
+ prev_lower: f64,
39
+ prev_st: f64,
40
+ prev_direction: SupertrendDirection,
41
+ }
42
+
43
+ impl Supertrend {
44
+ pub fn new(atr_period: usize, multiplier: f64) -> Self {
45
+ Self {
46
+ atr_period,
47
+ multiplier,
48
+ count: 0,
49
+ prev_close: 0.0,
50
+ tr_sum: 0.0,
51
+ atr: 0.0,
52
+ prev_upper: 0.0,
53
+ prev_lower: 0.0,
54
+ prev_st: 0.0,
55
+ prev_direction: SupertrendDirection::Up,
56
+ }
57
+ }
58
+ }
59
+
60
+ impl Indicator for Supertrend {
61
+ type Output = SupertrendResult;
62
+
63
+ fn update(&mut self, candle: &Candle) -> Option<SupertrendResult> {
64
+ self.count += 1;
65
+
66
+ if self.count == 1 {
67
+ self.prev_close = candle.close;
68
+ return None;
69
+ }
70
+
71
+ // True Range
72
+ let tr = (candle.high - candle.low)
73
+ .max((candle.high - self.prev_close).abs())
74
+ .max((candle.low - self.prev_close).abs());
75
+
76
+ if self.count <= self.atr_period + 1 {
77
+ self.tr_sum += tr;
78
+ if self.count == self.atr_period + 1 {
79
+ self.atr = self.tr_sum / self.atr_period as f64;
80
+ } else {
81
+ self.prev_close = candle.close;
82
+ return None;
83
+ }
84
+ } else {
85
+ self.atr = wilders_step(self.atr, tr, self.atr_period);
86
+ }
87
+
88
+ let hl2 = (candle.high + candle.low) / 2.0;
89
+ let basic_upper = hl2 + self.multiplier * self.atr;
90
+ let basic_lower = hl2 - self.multiplier * self.atr;
91
+
92
+ let upper = if basic_upper < self.prev_upper || self.prev_close > self.prev_upper {
93
+ basic_upper
94
+ } else {
95
+ self.prev_upper
96
+ };
97
+
98
+ let lower = if basic_lower > self.prev_lower || self.prev_close < self.prev_lower {
99
+ basic_lower
100
+ } else {
101
+ self.prev_lower
102
+ };
103
+
104
+ let (st, direction) = if self.prev_st == self.prev_upper {
105
+ if candle.close <= upper {
106
+ (upper, SupertrendDirection::Down)
107
+ } else {
108
+ (lower, SupertrendDirection::Up)
109
+ }
110
+ } else if candle.close >= lower {
111
+ (lower, SupertrendDirection::Up)
112
+ } else {
113
+ (upper, SupertrendDirection::Down)
114
+ };
115
+
116
+ self.prev_upper = upper;
117
+ self.prev_lower = lower;
118
+ self.prev_st = st;
119
+ self.prev_direction = direction;
120
+ self.prev_close = candle.close;
121
+
122
+ Some(SupertrendResult {
123
+ value: round(st, 2),
124
+ direction,
125
+ })
126
+ }
127
+
128
+ fn reset(&mut self) {
129
+ self.count = 0;
130
+ self.prev_close = 0.0;
131
+ self.tr_sum = 0.0;
132
+ self.atr = 0.0;
133
+ self.prev_upper = 0.0;
134
+ self.prev_lower = 0.0;
135
+ self.prev_st = 0.0;
136
+ self.prev_direction = SupertrendDirection::Up;
137
+ }
138
+ }
139
+
140
+ /// Convenience function: Supertrend from slices.
141
+ #[must_use]
142
+ pub fn supertrend(
143
+ highs: &[f64],
144
+ lows: &[f64],
145
+ closes: &[f64],
146
+ atr_period: usize,
147
+ multiplier: f64,
148
+ ) -> Option<SupertrendResult> {
149
+ let len = closes.len();
150
+ if len < atr_period + 1 || highs.len() < len || lows.len() < len || atr_period == 0 {
151
+ return None;
152
+ }
153
+ let candles: Vec<Candle> = (0..len)
154
+ .map(|i| Candle {
155
+ open: closes[i],
156
+ high: highs[i],
157
+ low: lows[i],
158
+ close: closes[i],
159
+ volume: 0.0,
160
+ })
161
+ .collect();
162
+ let mut ind = Supertrend::new(atr_period, multiplier);
163
+ ind.compute_last(&candles)
164
+ }
165
+
166
+ #[cfg(test)]
167
+ mod tests {
168
+ use super::*;
169
+
170
+ #[test]
171
+ fn supertrend_uptrend() {
172
+ let closes: Vec<f64> = (0..30).map(|i| 100.0 + i as f64).collect();
173
+ let highs: Vec<f64> = closes.iter().map(|c| c + 1.0).collect();
174
+ let lows: Vec<f64> = closes.iter().map(|c| c - 1.0).collect();
175
+ let result = supertrend(&highs, &lows, &closes, 10, 3.0).unwrap();
176
+ assert_eq!(result.direction, SupertrendDirection::Up);
177
+ assert!(result.value < *closes.last().unwrap());
178
+ }
179
+
180
+ #[test]
181
+ fn supertrend_downtrend() {
182
+ let closes: Vec<f64> = (0..30).map(|i| 200.0 - i as f64).collect();
183
+ let highs: Vec<f64> = closes.iter().map(|c| c + 1.0).collect();
184
+ let lows: Vec<f64> = closes.iter().map(|c| c - 1.0).collect();
185
+ let result = supertrend(&highs, &lows, &closes, 10, 3.0).unwrap();
186
+ assert_eq!(result.direction, SupertrendDirection::Down);
187
+ }
188
+
189
+ #[test]
190
+ fn supertrend_insufficient() {
191
+ assert!(supertrend(&[1.0; 5], &[1.0; 5], &[1.0; 5], 10, 3.0).is_none());
192
+ }
193
+ }
@@ -0,0 +1,51 @@
1
+ use crate::core::utils::{round, wilders_step};
2
+
3
+ /// Average True Range using Wilder's smoothing.
4
+ #[must_use]
5
+ pub fn atr(highs: &[f64], lows: &[f64], closes: &[f64], period: usize) -> Option<f64> {
6
+ let len = closes.len();
7
+ if len < period + 1 || highs.len() < len || lows.len() < len || period == 0 {
8
+ return None;
9
+ }
10
+ let true_ranges: Vec<f64> = (1..len)
11
+ .map(|i| {
12
+ (highs[i] - lows[i])
13
+ .max((highs[i] - closes[i - 1]).abs())
14
+ .max((lows[i] - closes[i - 1]).abs())
15
+ })
16
+ .collect();
17
+ if true_ranges.len() < period {
18
+ return None;
19
+ }
20
+ let seed: f64 = true_ranges[..period].iter().sum::<f64>() / period as f64;
21
+ let mut value = seed;
22
+ for &tr in &true_ranges[period..] {
23
+ value = wilders_step(value, tr, period);
24
+ }
25
+ Some(round(value, 2))
26
+ }
27
+
28
+ #[cfg(test)]
29
+ mod tests {
30
+ use super::*;
31
+
32
+ #[test]
33
+ fn atr_basic() {
34
+ let closes: Vec<f64> = (0..20).map(|i| 100.0 + (i as f64 * 0.5)).collect();
35
+ let highs: Vec<f64> = closes.iter().map(|c| c + 2.0).collect();
36
+ let lows: Vec<f64> = closes.iter().map(|c| c - 2.0).collect();
37
+ let result = atr(&highs, &lows, &closes, 14).unwrap();
38
+ assert!(result > 0.0);
39
+ }
40
+
41
+ #[test]
42
+ fn atr_flat() {
43
+ let data = vec![100.0; 20];
44
+ assert_eq!(atr(&data, &data, &data, 14), Some(0.0));
45
+ }
46
+
47
+ #[test]
48
+ fn atr_insufficient() {
49
+ assert!(atr(&[1.0; 5], &[1.0; 5], &[1.0; 5], 14).is_none());
50
+ }
51
+ }
@@ -1,6 +1,5 @@
1
- use crate::utils::round;
1
+ use crate::core::utils::round;
2
2
 
3
- /// Bollinger Bands result.
4
3
  #[derive(Debug, Clone)]
5
4
  pub struct BollingerBandsResult {
6
5
  pub upper: f64,
@@ -9,8 +8,8 @@ pub struct BollingerBandsResult {
9
8
  pub percent_b: f64,
10
9
  }
11
10
 
12
- /// Bollinger Bands (default: period=20, std_dev_multiplier=2.0).
13
- /// Returns `None` if insufficient data.
11
+ /// Bollinger Bands (uses population standard deviation).
12
+ #[must_use]
14
13
  pub fn bollinger_bands(
15
14
  closes: &[f64],
16
15
  period: usize,
@@ -19,24 +18,18 @@ pub fn bollinger_bands(
19
18
  if closes.len() < period || period == 0 {
20
19
  return None;
21
20
  }
22
-
23
21
  let slice = &closes[closes.len() - period..];
24
22
  let middle: f64 = slice.iter().sum::<f64>() / period as f64;
25
-
26
- let variance: f64 =
27
- slice.iter().map(|&v| (v - middle).powi(2)).sum::<f64>() / period as f64;
23
+ let variance: f64 = slice.iter().map(|&v| (v - middle).powi(2)).sum::<f64>() / period as f64;
28
24
  let std_dev = variance.sqrt();
29
-
30
25
  let upper = middle + std_dev_multiplier * std_dev;
31
26
  let lower = middle - std_dev_multiplier * std_dev;
32
-
33
- let current_price = *closes.last().unwrap();
27
+ let current_price = *closes.last()?;
34
28
  let percent_b = if (upper - lower).abs() < f64::EPSILON {
35
29
  0.5
36
30
  } else {
37
31
  (current_price - lower) / (upper - lower)
38
32
  };
39
-
40
33
  Some(BollingerBandsResult {
41
34
  upper: round(upper, 2),
42
35
  middle: round(middle, 2),
@@ -55,19 +48,10 @@ mod tests {
55
48
  let result = bollinger_bands(&closes, 20, 2.0).unwrap();
56
49
  assert!(result.upper > result.middle);
57
50
  assert!(result.middle > result.lower);
58
- assert_eq!(result.middle, 10.5); // avg of 1..=20
59
- }
60
-
61
- #[test]
62
- fn bb_percent_b() {
63
- // Price at middle should give ~0.5
64
- let closes = vec![10.0; 20];
65
- let result = bollinger_bands(&closes, 20, 2.0).unwrap();
66
- assert_eq!(result.percent_b, 0.5); // all same = flat bands
67
51
  }
68
52
 
69
53
  #[test]
70
- fn bb_insufficient_data() {
54
+ fn bb_insufficient() {
71
55
  assert!(bollinger_bands(&[1.0; 5], 20, 2.0).is_none());
72
56
  }
73
57
  }
@@ -0,0 +1,2 @@
1
+ pub mod atr;
2
+ pub mod bollinger;
@@ -0,0 +1,3 @@
1
+ pub mod obv;
2
+ pub mod volume_trend;
3
+ pub mod vwap;