@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.
- package/.pm/indica-research-report.md +390 -0
- package/ARCHITECTURE.md +470 -0
- package/Cargo.lock +1 -1
- package/Cargo.toml +2 -2
- package/README.md +98 -109
- package/package.json +2 -2
- package/src/batch/compute.rs +165 -0
- package/src/batch/mod.rs +2 -0
- package/src/batch/screen.rs +176 -0
- package/src/core/mod.rs +3 -0
- package/src/core/traits.rs +37 -0
- package/src/core/types.rs +22 -0
- package/src/core/utils.rs +43 -0
- package/src/indicators/india/circuit.rs +101 -0
- package/src/indicators/india/delivery.rs +116 -0
- package/src/indicators/india/mod.rs +2 -0
- package/src/indicators/mod.rs +6 -0
- package/src/indicators/momentum/macd.rs +123 -0
- package/src/indicators/momentum/mod.rs +3 -0
- package/src/indicators/momentum/rsi.rs +130 -0
- package/src/indicators/momentum/stochastic.rs +99 -0
- package/src/indicators/support_resistance/mod.rs +1 -0
- package/src/indicators/support_resistance/pivot.rs +40 -0
- package/src/indicators/trend/adx.rs +168 -0
- package/src/indicators/trend/ema.rs +110 -0
- package/src/indicators/trend/mod.rs +4 -0
- package/src/indicators/trend/sma.rs +75 -0
- package/src/indicators/trend/supertrend.rs +193 -0
- package/src/indicators/volatility/atr.rs +51 -0
- package/src/{bollinger.rs → indicators/volatility/bollinger.rs} +6 -22
- package/src/indicators/volatility/mod.rs +2 -0
- package/src/indicators/volume/mod.rs +3 -0
- package/src/indicators/volume/obv.rs +44 -0
- package/src/{volume.rs → indicators/volume/volume_trend.rs} +9 -27
- package/src/indicators/volume/vwap.rs +53 -0
- package/src/lib.rs +71 -21
- package/src/signals/engine.rs +139 -0
- package/src/signals/mod.rs +4 -0
- package/src/signals/presets.rs +109 -0
- package/src/signals/rules.rs +333 -0
- package/src/signals/types.rs +50 -0
- package/index.d.ts +0 -66
- package/indica.node +0 -0
- package/src/atr.rs +0 -66
- package/src/batch.rs +0 -139
- package/src/macd.rs +0 -132
- package/src/moving_avg.rs +0 -71
- package/src/napi_bindings.rs +0 -166
- package/src/pivot.rs +0 -58
- package/src/relative_strength.rs +0 -67
- package/src/rsi.rs +0 -74
- 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,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 (
|
|
13
|
-
|
|
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
|
|
54
|
+
fn bb_insufficient() {
|
|
71
55
|
assert!(bollinger_bands(&[1.0; 5], 20, 2.0).is_none());
|
|
72
56
|
}
|
|
73
57
|
}
|