@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
package/src/macd.rs
DELETED
|
@@ -1,132 +0,0 @@
|
|
|
1
|
-
use crate::utils::round;
|
|
2
|
-
|
|
3
|
-
/// MACD crossover direction.
|
|
4
|
-
#[derive(Debug, Clone, PartialEq)]
|
|
5
|
-
pub enum Crossover {
|
|
6
|
-
Bullish,
|
|
7
|
-
Bearish,
|
|
8
|
-
None,
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
/// MACD computation result.
|
|
12
|
-
#[derive(Debug, Clone)]
|
|
13
|
-
pub struct MacdResult {
|
|
14
|
-
pub value: f64,
|
|
15
|
-
pub signal: f64,
|
|
16
|
-
pub histogram: f64,
|
|
17
|
-
pub crossover: Crossover,
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/// MACD (Moving Average Convergence Divergence).
|
|
21
|
-
/// Default parameters: fast=12, slow=26, signal=9.
|
|
22
|
-
/// Returns `None` if insufficient data.
|
|
23
|
-
pub fn macd(
|
|
24
|
-
closes: &[f64],
|
|
25
|
-
fast_period: usize,
|
|
26
|
-
slow_period: usize,
|
|
27
|
-
signal_period: usize,
|
|
28
|
-
) -> Option<MacdResult> {
|
|
29
|
-
if closes.len() < slow_period + signal_period || slow_period == 0 {
|
|
30
|
-
return None;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
let k_fast = 2.0 / (fast_period as f64 + 1.0);
|
|
34
|
-
let k_slow = 2.0 / (slow_period as f64 + 1.0);
|
|
35
|
-
|
|
36
|
-
let mut ema_slow: f64 = closes[..slow_period].iter().sum::<f64>() / slow_period as f64;
|
|
37
|
-
|
|
38
|
-
// Seed fast EMA and advance it to slow_period point
|
|
39
|
-
let mut ema_fast: f64 = closes[..fast_period].iter().sum::<f64>() / fast_period as f64;
|
|
40
|
-
for i in fast_period..slow_period {
|
|
41
|
-
ema_fast = closes[i] * k_fast + ema_fast * (1.0 - k_fast);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
let mut macd_line = Vec::new();
|
|
45
|
-
for i in slow_period..closes.len() {
|
|
46
|
-
ema_fast = closes[i] * k_fast + ema_fast * (1.0 - k_fast);
|
|
47
|
-
ema_slow = closes[i] * k_slow + ema_slow * (1.0 - k_slow);
|
|
48
|
-
macd_line.push(ema_fast - ema_slow);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
if macd_line.len() < signal_period {
|
|
52
|
-
return None;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// Signal line = EMA of MACD line
|
|
56
|
-
let k_signal = 2.0 / (signal_period as f64 + 1.0);
|
|
57
|
-
let mut signal_line: f64 =
|
|
58
|
-
macd_line[..signal_period].iter().sum::<f64>() / signal_period as f64;
|
|
59
|
-
|
|
60
|
-
let mut prev_signal = signal_line;
|
|
61
|
-
for i in signal_period..macd_line.len() {
|
|
62
|
-
prev_signal = signal_line;
|
|
63
|
-
signal_line = macd_line[i] * k_signal + signal_line * (1.0 - k_signal);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
let current_macd = *macd_line.last().unwrap();
|
|
67
|
-
let histogram = current_macd - signal_line;
|
|
68
|
-
|
|
69
|
-
let prev_macd = if macd_line.len() >= 2 {
|
|
70
|
-
macd_line[macd_line.len() - 2]
|
|
71
|
-
} else {
|
|
72
|
-
current_macd
|
|
73
|
-
};
|
|
74
|
-
let prev_histogram = prev_macd - prev_signal;
|
|
75
|
-
|
|
76
|
-
let crossover = if prev_histogram <= 0.0 && histogram > 0.0 {
|
|
77
|
-
Crossover::Bullish
|
|
78
|
-
} else if prev_histogram >= 0.0 && histogram < 0.0 {
|
|
79
|
-
Crossover::Bearish
|
|
80
|
-
} else {
|
|
81
|
-
Crossover::None
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
Some(MacdResult {
|
|
85
|
-
value: round(current_macd, 2),
|
|
86
|
-
signal: round(signal_line, 2),
|
|
87
|
-
histogram: round(histogram, 2),
|
|
88
|
-
crossover,
|
|
89
|
-
})
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
#[cfg(test)]
|
|
93
|
-
mod tests {
|
|
94
|
-
use super::*;
|
|
95
|
-
|
|
96
|
-
fn trending_up() -> Vec<f64> {
|
|
97
|
-
(0..50).map(|i| 100.0 + i as f64 * 0.5).collect()
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
fn trending_down() -> Vec<f64> {
|
|
101
|
-
(0..50).map(|i| 150.0 - i as f64 * 0.5).collect()
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
#[test]
|
|
105
|
-
fn macd_trending_up() {
|
|
106
|
-
let result = macd(&trending_up(), 12, 26, 9).unwrap();
|
|
107
|
-
assert!(result.value > 0.0, "MACD should be positive in uptrend");
|
|
108
|
-
assert!(result.histogram > 0.0 || result.histogram.abs() < 0.5);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
#[test]
|
|
112
|
-
fn macd_trending_down() {
|
|
113
|
-
let result = macd(&trending_down(), 12, 26, 9).unwrap();
|
|
114
|
-
assert!(result.value < 0.0, "MACD should be negative in downtrend");
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
#[test]
|
|
118
|
-
fn macd_insufficient_data() {
|
|
119
|
-
assert!(macd(&[1.0; 20], 12, 26, 9).is_none());
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
#[test]
|
|
123
|
-
fn macd_crossover_detection() {
|
|
124
|
-
// Flat then up — should eventually get bullish crossover
|
|
125
|
-
let mut data: Vec<f64> = vec![100.0; 35];
|
|
126
|
-
for i in 0..20 {
|
|
127
|
-
data.push(100.0 + i as f64 * 2.0);
|
|
128
|
-
}
|
|
129
|
-
let result = macd(&data, 12, 26, 9);
|
|
130
|
-
assert!(result.is_some());
|
|
131
|
-
}
|
|
132
|
-
}
|
package/src/moving_avg.rs
DELETED
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
/// Simple Moving Average of the last `period` values.
|
|
2
|
-
/// Returns `None` if there are fewer values than `period`.
|
|
3
|
-
pub fn sma(values: &[f64], period: usize) -> Option<f64> {
|
|
4
|
-
if values.len() < period || period == 0 {
|
|
5
|
-
return None;
|
|
6
|
-
}
|
|
7
|
-
let slice = &values[values.len() - period..];
|
|
8
|
-
let sum: f64 = slice.iter().sum();
|
|
9
|
-
Some(sum / period as f64)
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
/// Exponential Moving Average.
|
|
13
|
-
/// Seeded with SMA of the first `period` values, then smoothed forward.
|
|
14
|
-
/// Returns `None` if there are fewer values than `period`.
|
|
15
|
-
pub fn ema(values: &[f64], period: usize) -> Option<f64> {
|
|
16
|
-
if values.len() < period || period == 0 {
|
|
17
|
-
return None;
|
|
18
|
-
}
|
|
19
|
-
let k = 2.0 / (period as f64 + 1.0);
|
|
20
|
-
let seed: f64 = values[..period].iter().sum::<f64>() / period as f64;
|
|
21
|
-
let result = values[period..].iter().fold(seed, |prev, &val| {
|
|
22
|
-
val * k + prev * (1.0 - k)
|
|
23
|
-
});
|
|
24
|
-
Some(result)
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
#[cfg(test)]
|
|
28
|
-
mod tests {
|
|
29
|
-
use super::*;
|
|
30
|
-
|
|
31
|
-
#[test]
|
|
32
|
-
fn sma_basic() {
|
|
33
|
-
assert_eq!(sma(&[1.0, 2.0, 3.0, 4.0, 5.0], 3), Some(4.0));
|
|
34
|
-
assert_eq!(sma(&[10.0, 20.0, 30.0], 3), Some(20.0));
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
#[test]
|
|
38
|
-
fn sma_insufficient_data() {
|
|
39
|
-
assert_eq!(sma(&[1.0, 2.0], 5), None);
|
|
40
|
-
assert_eq!(sma(&[], 1), None);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
#[test]
|
|
44
|
-
fn sma_period_one() {
|
|
45
|
-
assert_eq!(sma(&[42.0, 99.0], 1), Some(99.0));
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
#[test]
|
|
49
|
-
fn ema_basic() {
|
|
50
|
-
let data = vec![10.0, 11.0, 12.0, 13.0, 14.0, 15.0];
|
|
51
|
-
let result = ema(&data, 3).unwrap();
|
|
52
|
-
// EMA(3) of [10,11,12,13,14,15]:
|
|
53
|
-
// Seed = avg(10,11,12) = 11.0, k = 0.5
|
|
54
|
-
// 13*0.5 + 11*0.5 = 12.0
|
|
55
|
-
// 14*0.5 + 12*0.5 = 13.0
|
|
56
|
-
// 15*0.5 + 13*0.5 = 14.0
|
|
57
|
-
assert!((result - 14.0).abs() < 0.01);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
#[test]
|
|
61
|
-
fn ema_insufficient_data() {
|
|
62
|
-
assert_eq!(ema(&[1.0], 5), None);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
#[test]
|
|
66
|
-
fn ema_equals_sma_at_period_length() {
|
|
67
|
-
let data = vec![10.0, 20.0, 30.0];
|
|
68
|
-
// With exactly `period` values, EMA seed = SMA and no further smoothing
|
|
69
|
-
assert_eq!(ema(&data, 3), sma(&data, 3));
|
|
70
|
-
}
|
|
71
|
-
}
|
package/src/napi_bindings.rs
DELETED
|
@@ -1,166 +0,0 @@
|
|
|
1
|
-
use napi_derive::napi;
|
|
2
|
-
use crate::batch::{StockData, batch_compute_parallel};
|
|
3
|
-
|
|
4
|
-
// ── Simple indicator functions (for single stock use) ──
|
|
5
|
-
|
|
6
|
-
#[napi]
|
|
7
|
-
pub fn calc_sma(values: Vec<f64>, period: u32) -> Option<f64> {
|
|
8
|
-
crate::sma(&values, period as usize)
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
#[napi]
|
|
12
|
-
pub fn calc_ema(values: Vec<f64>, period: u32) -> Option<f64> {
|
|
13
|
-
crate::ema(&values, period as usize)
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
#[napi]
|
|
17
|
-
pub fn calc_rsi(closes: Vec<f64>, period: u32) -> Option<f64> {
|
|
18
|
-
crate::rsi(&closes, period as usize)
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
#[napi]
|
|
22
|
-
pub fn calc_atr(highs: Vec<f64>, lows: Vec<f64>, closes: Vec<f64>, period: u32) -> Option<f64> {
|
|
23
|
-
crate::atr(&highs, &lows, &closes, period as usize)
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
#[napi]
|
|
27
|
-
pub fn calc_volume_trend(volumes: Vec<f64>) -> String {
|
|
28
|
-
crate::volume_trend(&volumes).to_string()
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
#[napi]
|
|
32
|
-
pub fn calc_relative_strength(stock: Vec<f64>, benchmark: Vec<f64>, period: u32) -> Option<f64> {
|
|
33
|
-
crate::relative_strength(&stock, &benchmark, period as usize)
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// ── Struct results (returned as JS objects) ──
|
|
37
|
-
|
|
38
|
-
#[napi(object)]
|
|
39
|
-
pub struct JsMacdResult {
|
|
40
|
-
pub value: f64,
|
|
41
|
-
pub signal: f64,
|
|
42
|
-
pub histogram: f64,
|
|
43
|
-
pub crossover: String,
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
#[napi]
|
|
47
|
-
pub fn calc_macd(closes: Vec<f64>, fast: u32, slow: u32, signal: u32) -> Option<JsMacdResult> {
|
|
48
|
-
crate::macd(&closes, fast as usize, slow as usize, signal as usize).map(|r| JsMacdResult {
|
|
49
|
-
value: r.value,
|
|
50
|
-
signal: r.signal,
|
|
51
|
-
histogram: r.histogram,
|
|
52
|
-
crossover: match r.crossover {
|
|
53
|
-
crate::Crossover::Bullish => "bullish".to_string(),
|
|
54
|
-
crate::Crossover::Bearish => "bearish".to_string(),
|
|
55
|
-
crate::Crossover::None => "none".to_string(),
|
|
56
|
-
},
|
|
57
|
-
})
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
#[napi(object)]
|
|
61
|
-
pub struct JsBollingerBands {
|
|
62
|
-
pub upper: f64,
|
|
63
|
-
pub middle: f64,
|
|
64
|
-
pub lower: f64,
|
|
65
|
-
pub percent_b: f64,
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
#[napi]
|
|
69
|
-
pub fn calc_bollinger_bands(closes: Vec<f64>, period: u32, std_dev: f64) -> Option<JsBollingerBands> {
|
|
70
|
-
crate::bollinger_bands(&closes, period as usize, std_dev).map(|r| JsBollingerBands {
|
|
71
|
-
upper: r.upper,
|
|
72
|
-
middle: r.middle,
|
|
73
|
-
lower: r.lower,
|
|
74
|
-
percent_b: r.percent_b,
|
|
75
|
-
})
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
#[napi(object)]
|
|
79
|
-
pub struct JsPivotPoints {
|
|
80
|
-
pub r3: f64,
|
|
81
|
-
pub r2: f64,
|
|
82
|
-
pub r1: f64,
|
|
83
|
-
pub pivot: f64,
|
|
84
|
-
pub s1: f64,
|
|
85
|
-
pub s2: f64,
|
|
86
|
-
pub s3: f64,
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
#[napi]
|
|
90
|
-
pub fn calc_pivot_points(high: f64, low: f64, close: f64) -> JsPivotPoints {
|
|
91
|
-
let r = crate::pivot_points(high, low, close);
|
|
92
|
-
JsPivotPoints {
|
|
93
|
-
r3: r.r3, r2: r.r2, r1: r.r1,
|
|
94
|
-
pivot: r.pivot,
|
|
95
|
-
s1: r.s1, s2: r.s2, s3: r.s3,
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// ── Batch processing (the main payoff) ──
|
|
100
|
-
|
|
101
|
-
#[napi(object)]
|
|
102
|
-
pub struct JsStockData {
|
|
103
|
-
pub symbol: String,
|
|
104
|
-
pub closes: Vec<f64>,
|
|
105
|
-
pub highs: Vec<f64>,
|
|
106
|
-
pub lows: Vec<f64>,
|
|
107
|
-
pub volumes: Vec<f64>,
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
#[napi(object)]
|
|
111
|
-
pub struct JsIndicatorSnapshot {
|
|
112
|
-
pub symbol: String,
|
|
113
|
-
pub sma_20: Option<f64>,
|
|
114
|
-
pub sma_50: Option<f64>,
|
|
115
|
-
pub sma_200: Option<f64>,
|
|
116
|
-
pub ema_20: Option<f64>,
|
|
117
|
-
pub rsi_14: Option<f64>,
|
|
118
|
-
pub macd: Option<JsMacdResult>,
|
|
119
|
-
pub bollinger: Option<JsBollingerBands>,
|
|
120
|
-
pub atr_14: Option<f64>,
|
|
121
|
-
pub volume_trend: String,
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
#[napi]
|
|
125
|
-
pub fn batch_compute_indicators(stocks: Vec<JsStockData>) -> Vec<JsIndicatorSnapshot> {
|
|
126
|
-
let stock_data: Vec<StockData> = stocks
|
|
127
|
-
.into_iter()
|
|
128
|
-
.map(|s| StockData {
|
|
129
|
-
symbol: s.symbol,
|
|
130
|
-
closes: s.closes,
|
|
131
|
-
highs: s.highs,
|
|
132
|
-
lows: s.lows,
|
|
133
|
-
volumes: s.volumes,
|
|
134
|
-
})
|
|
135
|
-
.collect();
|
|
136
|
-
|
|
137
|
-
batch_compute_parallel(&stock_data)
|
|
138
|
-
.into_iter()
|
|
139
|
-
.map(|snap| JsIndicatorSnapshot {
|
|
140
|
-
symbol: snap.symbol,
|
|
141
|
-
sma_20: snap.sma_20,
|
|
142
|
-
sma_50: snap.sma_50,
|
|
143
|
-
sma_200: snap.sma_200,
|
|
144
|
-
ema_20: snap.ema_20,
|
|
145
|
-
rsi_14: snap.rsi_14,
|
|
146
|
-
macd: snap.macd_result.map(|m| JsMacdResult {
|
|
147
|
-
value: m.value,
|
|
148
|
-
signal: m.signal,
|
|
149
|
-
histogram: m.histogram,
|
|
150
|
-
crossover: match m.crossover {
|
|
151
|
-
crate::Crossover::Bullish => "bullish".to_string(),
|
|
152
|
-
crate::Crossover::Bearish => "bearish".to_string(),
|
|
153
|
-
crate::Crossover::None => "none".to_string(),
|
|
154
|
-
},
|
|
155
|
-
}),
|
|
156
|
-
bollinger: snap.bollinger.map(|b| JsBollingerBands {
|
|
157
|
-
upper: b.upper,
|
|
158
|
-
middle: b.middle,
|
|
159
|
-
lower: b.lower,
|
|
160
|
-
percent_b: b.percent_b,
|
|
161
|
-
}),
|
|
162
|
-
atr_14: snap.atr_14,
|
|
163
|
-
volume_trend: snap.volume_trend,
|
|
164
|
-
})
|
|
165
|
-
.collect()
|
|
166
|
-
}
|
package/src/pivot.rs
DELETED
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
use crate::utils::round;
|
|
2
|
-
|
|
3
|
-
/// Classic Pivot Points result.
|
|
4
|
-
#[derive(Debug, Clone)]
|
|
5
|
-
pub struct PivotPointsResult {
|
|
6
|
-
pub r3: f64,
|
|
7
|
-
pub r2: f64,
|
|
8
|
-
pub r1: f64,
|
|
9
|
-
pub pivot: f64,
|
|
10
|
-
pub s1: f64,
|
|
11
|
-
pub s2: f64,
|
|
12
|
-
pub s3: f64,
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/// Classic Pivot Points from a single candle (typically previous day).
|
|
16
|
-
pub fn pivot_points(high: f64, low: f64, close: f64) -> PivotPointsResult {
|
|
17
|
-
let pivot = (high + low + close) / 3.0;
|
|
18
|
-
let r1 = 2.0 * pivot - low;
|
|
19
|
-
let s1 = 2.0 * pivot - high;
|
|
20
|
-
let r2 = pivot + (high - low);
|
|
21
|
-
let s2 = pivot - (high - low);
|
|
22
|
-
let r3 = high + 2.0 * (pivot - low);
|
|
23
|
-
let s3 = low - 2.0 * (high - pivot);
|
|
24
|
-
|
|
25
|
-
PivotPointsResult {
|
|
26
|
-
r3: round(r3, 2),
|
|
27
|
-
r2: round(r2, 2),
|
|
28
|
-
r1: round(r1, 2),
|
|
29
|
-
pivot: round(pivot, 2),
|
|
30
|
-
s1: round(s1, 2),
|
|
31
|
-
s2: round(s2, 2),
|
|
32
|
-
s3: round(s3, 2),
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
#[cfg(test)]
|
|
37
|
-
mod tests {
|
|
38
|
-
use super::*;
|
|
39
|
-
|
|
40
|
-
#[test]
|
|
41
|
-
fn pivot_basic() {
|
|
42
|
-
let result = pivot_points(110.0, 90.0, 100.0);
|
|
43
|
-
assert_eq!(result.pivot, 100.0);
|
|
44
|
-
assert!(result.r1 > result.pivot);
|
|
45
|
-
assert!(result.r2 > result.r1);
|
|
46
|
-
assert!(result.r3 > result.r2);
|
|
47
|
-
assert!(result.s1 < result.pivot);
|
|
48
|
-
assert!(result.s2 < result.s1);
|
|
49
|
-
assert!(result.s3 < result.s2);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
#[test]
|
|
53
|
-
fn pivot_symmetry() {
|
|
54
|
-
// When close == midpoint of range, pivot = close
|
|
55
|
-
let result = pivot_points(110.0, 90.0, 100.0);
|
|
56
|
-
assert_eq!(result.pivot, 100.0);
|
|
57
|
-
}
|
|
58
|
-
}
|
package/src/relative_strength.rs
DELETED
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
use crate::utils::round;
|
|
2
|
-
|
|
3
|
-
/// Relative Strength vs a benchmark (e.g., Nifty 50).
|
|
4
|
-
/// RS = 1 + (stock_return - benchmark_return).
|
|
5
|
-
/// Returns >1 if outperforming, <1 if underperforming.
|
|
6
|
-
/// Returns `None` if insufficient data.
|
|
7
|
-
pub fn relative_strength(
|
|
8
|
-
stock_closes: &[f64],
|
|
9
|
-
benchmark_closes: &[f64],
|
|
10
|
-
period: usize,
|
|
11
|
-
) -> Option<f64> {
|
|
12
|
-
if stock_closes.len() < period || benchmark_closes.len() < period || period == 0 {
|
|
13
|
-
return None;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
let stock_start = stock_closes[stock_closes.len() - period];
|
|
17
|
-
let stock_end = *stock_closes.last().unwrap();
|
|
18
|
-
let bench_start = benchmark_closes[benchmark_closes.len() - period];
|
|
19
|
-
let bench_end = *benchmark_closes.last().unwrap();
|
|
20
|
-
|
|
21
|
-
if stock_start == 0.0 || bench_start == 0.0 {
|
|
22
|
-
return None;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
let stock_return = (stock_end - stock_start) / stock_start;
|
|
26
|
-
let bench_return = (bench_end - bench_start) / bench_start;
|
|
27
|
-
|
|
28
|
-
if bench_return == 0.0 {
|
|
29
|
-
return Some(if stock_return > 0.0 { 2.0 } else { 0.0 });
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
Some(round(1.0 + (stock_return - bench_return), 2))
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
#[cfg(test)]
|
|
36
|
-
mod tests {
|
|
37
|
-
use super::*;
|
|
38
|
-
|
|
39
|
-
#[test]
|
|
40
|
-
fn rs_outperforming() {
|
|
41
|
-
let stock = vec![100.0, 110.0, 120.0]; // +20%
|
|
42
|
-
let bench = vec![100.0, 105.0, 110.0]; // +10%
|
|
43
|
-
let result = relative_strength(&stock, &bench, 3).unwrap();
|
|
44
|
-
assert!(result > 1.0); // Stock beat benchmark
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
#[test]
|
|
48
|
-
fn rs_underperforming() {
|
|
49
|
-
let stock = vec![100.0, 95.0, 90.0]; // -10%
|
|
50
|
-
let bench = vec![100.0, 105.0, 110.0]; // +10%
|
|
51
|
-
let result = relative_strength(&stock, &bench, 3).unwrap();
|
|
52
|
-
assert!(result < 1.0); // Stock lagged benchmark
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
#[test]
|
|
56
|
-
fn rs_equal_performance() {
|
|
57
|
-
let stock = vec![100.0, 110.0]; // +10%
|
|
58
|
-
let bench = vec![100.0, 110.0]; // +10%
|
|
59
|
-
let result = relative_strength(&stock, &bench, 2).unwrap();
|
|
60
|
-
assert_eq!(result, 1.0);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
#[test]
|
|
64
|
-
fn rs_insufficient_data() {
|
|
65
|
-
assert!(relative_strength(&[100.0], &[100.0], 5).is_none());
|
|
66
|
-
}
|
|
67
|
-
}
|
package/src/rsi.rs
DELETED
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
/// Relative Strength Index using Wilder's smoothing.
|
|
2
|
-
/// Default period is typically 14.
|
|
3
|
-
/// Returns `None` if insufficient data (need at least `period + 1` values).
|
|
4
|
-
pub fn rsi(closes: &[f64], period: usize) -> Option<f64> {
|
|
5
|
-
if closes.len() < period + 1 || period == 0 {
|
|
6
|
-
return None;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
let mut avg_gain = 0.0;
|
|
10
|
-
let mut avg_loss = 0.0;
|
|
11
|
-
|
|
12
|
-
// Initial average gain/loss from first `period` changes
|
|
13
|
-
for i in 1..=period {
|
|
14
|
-
let change = closes[i] - closes[i - 1];
|
|
15
|
-
if change > 0.0 {
|
|
16
|
-
avg_gain += change;
|
|
17
|
-
} else {
|
|
18
|
-
avg_loss += change.abs();
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
avg_gain /= period as f64;
|
|
22
|
-
avg_loss /= period as f64;
|
|
23
|
-
|
|
24
|
-
// Wilder's smoothing for remaining values
|
|
25
|
-
for i in (period + 1)..closes.len() {
|
|
26
|
-
let change = closes[i] - closes[i - 1];
|
|
27
|
-
let gain = if change > 0.0 { change } else { 0.0 };
|
|
28
|
-
let loss = if change < 0.0 { change.abs() } else { 0.0 };
|
|
29
|
-
avg_gain = (avg_gain * (period as f64 - 1.0) + gain) / period as f64;
|
|
30
|
-
avg_loss = (avg_loss * (period as f64 - 1.0) + loss) / period as f64;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
if avg_loss == 0.0 {
|
|
34
|
-
return Some(100.0);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
let rs = avg_gain / avg_loss;
|
|
38
|
-
Some(100.0 - 100.0 / (1.0 + rs))
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
#[cfg(test)]
|
|
42
|
-
mod tests {
|
|
43
|
-
use super::*;
|
|
44
|
-
|
|
45
|
-
#[test]
|
|
46
|
-
fn rsi_all_gains() {
|
|
47
|
-
let closes: Vec<f64> = (0..20).map(|i| 100.0 + i as f64).collect();
|
|
48
|
-
assert_eq!(rsi(&closes, 14), Some(100.0));
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
#[test]
|
|
52
|
-
fn rsi_all_losses() {
|
|
53
|
-
let closes: Vec<f64> = (0..20).map(|i| 100.0 - i as f64).collect();
|
|
54
|
-
let result = rsi(&closes, 14).unwrap();
|
|
55
|
-
assert!(result < 1.0); // Near 0
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
#[test]
|
|
59
|
-
fn rsi_midrange() {
|
|
60
|
-
// Alternating up/down should be near 50
|
|
61
|
-
let closes = vec![
|
|
62
|
-
100.0, 102.0, 100.0, 102.0, 100.0, 102.0, 100.0, 102.0,
|
|
63
|
-
100.0, 102.0, 100.0, 102.0, 100.0, 102.0, 100.0, 102.0,
|
|
64
|
-
];
|
|
65
|
-
let result = rsi(&closes, 14).unwrap();
|
|
66
|
-
assert!((result - 50.0).abs() < 5.0);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
#[test]
|
|
70
|
-
fn rsi_insufficient_data() {
|
|
71
|
-
assert_eq!(rsi(&[100.0, 101.0], 14), None);
|
|
72
|
-
assert_eq!(rsi(&[], 14), None);
|
|
73
|
-
}
|
|
74
|
-
}
|
package/src/utils.rs
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
/// Round to N decimal places.
|
|
2
|
-
pub fn round(value: f64, decimals: u32) -> f64 {
|
|
3
|
-
let factor = 10_f64.powi(decimals as i32);
|
|
4
|
-
(value * factor).round() / factor
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
#[cfg(test)]
|
|
8
|
-
mod tests {
|
|
9
|
-
use super::*;
|
|
10
|
-
|
|
11
|
-
#[test]
|
|
12
|
-
fn test_round() {
|
|
13
|
-
assert_eq!(round(1.23456, 2), 1.23);
|
|
14
|
-
assert_eq!(round(1.235, 2), 1.24);
|
|
15
|
-
assert_eq!(round(100.0, 2), 100.0);
|
|
16
|
-
}
|
|
17
|
-
}
|