@devanshhq/indica 0.1.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.
@@ -0,0 +1,67 @@
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 ADDED
@@ -0,0 +1,74 @@
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 ADDED
@@ -0,0 +1,17 @@
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
+ }
package/src/volume.rs ADDED
@@ -0,0 +1,64 @@
1
+ /// Classify volume trend by comparing recent 5-day avg to 20-day avg.
2
+ /// Returns a static string: "surging", "increasing", "stable", "declining", "drying up",
3
+ /// or "insufficient data".
4
+ pub fn volume_trend(volumes: &[f64]) -> &'static str {
5
+ if volumes.len() < 20 {
6
+ return "insufficient data";
7
+ }
8
+
9
+ let avg5: f64 = volumes[volumes.len() - 5..].iter().sum::<f64>() / 5.0;
10
+ let avg20: f64 = volumes[volumes.len() - 20..].iter().sum::<f64>() / 20.0;
11
+
12
+ if avg20 == 0.0 {
13
+ return "insufficient data";
14
+ }
15
+
16
+ let ratio = avg5 / avg20;
17
+
18
+ if ratio > 1.5 {
19
+ "surging"
20
+ } else if ratio > 1.1 {
21
+ "increasing"
22
+ } else if ratio > 0.9 {
23
+ "stable"
24
+ } else if ratio > 0.5 {
25
+ "declining"
26
+ } else {
27
+ "drying up"
28
+ }
29
+ }
30
+
31
+ #[cfg(test)]
32
+ mod tests {
33
+ use super::*;
34
+
35
+ #[test]
36
+ fn volume_stable() {
37
+ let volumes = vec![1000.0; 25];
38
+ assert_eq!(volume_trend(&volumes), "stable");
39
+ }
40
+
41
+ #[test]
42
+ fn volume_surging() {
43
+ let mut volumes = vec![100.0; 20];
44
+ // Last 5 are much higher
45
+ for _ in 0..5 {
46
+ volumes.push(500.0);
47
+ }
48
+ assert_eq!(volume_trend(&volumes), "surging");
49
+ }
50
+
51
+ #[test]
52
+ fn volume_drying_up() {
53
+ let mut volumes = vec![1000.0; 20];
54
+ for _ in 0..5 {
55
+ volumes.push(10.0);
56
+ }
57
+ assert_eq!(volume_trend(&volumes), "drying up");
58
+ }
59
+
60
+ #[test]
61
+ fn volume_insufficient() {
62
+ assert_eq!(volume_trend(&[100.0; 10]), "insufficient data");
63
+ }
64
+ }