@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,43 @@
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
+ /// Wilder's smoothing step: (prev * (period-1) + val) / period.
8
+ /// Used by RSI and ATR.
9
+ pub fn wilders_step(prev: f64, val: f64, period: usize) -> f64 {
10
+ (prev * (period as f64 - 1.0) + val) / period as f64
11
+ }
12
+
13
+ /// EMA smoothing step: val * k + prev * (1 - k), where k = 2/(period+1).
14
+ pub fn ema_step(prev: f64, val: f64, k: f64) -> f64 {
15
+ val * k + prev * (1.0 - k)
16
+ }
17
+
18
+ /// Compute EMA smoothing factor for a given period.
19
+ pub fn ema_k(period: usize) -> f64 {
20
+ 2.0 / (period as f64 + 1.0)
21
+ }
22
+
23
+ #[cfg(test)]
24
+ mod tests {
25
+ use super::*;
26
+
27
+ #[test]
28
+ fn test_round() {
29
+ assert_eq!(round(1.23456, 2), 1.23);
30
+ assert_eq!(round(1.235, 2), 1.24);
31
+ }
32
+
33
+ #[test]
34
+ fn test_wilders_step() {
35
+ assert_eq!(wilders_step(10.0, 12.0, 2), 11.0);
36
+ }
37
+
38
+ #[test]
39
+ fn test_ema_step() {
40
+ let k = ema_k(3); // 0.5
41
+ assert_eq!(ema_step(10.0, 12.0, k), 11.0);
42
+ }
43
+ }
@@ -0,0 +1,101 @@
1
+ use crate::core::utils::round;
2
+
3
+ /// Circuit limit bands for Indian stocks.
4
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
5
+ pub enum CircuitLimit {
6
+ Percent2,
7
+ Percent5,
8
+ Percent10,
9
+ Percent20,
10
+ }
11
+
12
+ impl CircuitLimit {
13
+ fn multiplier(&self) -> f64 {
14
+ match self {
15
+ Self::Percent2 => 0.02,
16
+ Self::Percent5 => 0.05,
17
+ Self::Percent10 => 0.10,
18
+ Self::Percent20 => 0.20,
19
+ }
20
+ }
21
+ }
22
+
23
+ /// Circuit proximity status.
24
+ #[derive(Debug, Clone)]
25
+ pub struct CircuitStatus {
26
+ pub upper_limit: f64,
27
+ pub lower_limit: f64,
28
+ pub upper_distance_pct: f64, // How far from upper circuit (0 = at limit)
29
+ pub lower_distance_pct: f64, // How far from lower circuit
30
+ pub near_upper: bool, // Within 1% of upper circuit
31
+ pub near_lower: bool, // Within 1% of lower circuit
32
+ }
33
+
34
+ /// Check how close a stock is to its circuit limits.
35
+ /// Indian stocks have daily price movement limits (2/5/10/20%).
36
+ #[must_use]
37
+ pub fn circuit_proximity(
38
+ current_price: f64,
39
+ prev_close: f64,
40
+ limit: CircuitLimit,
41
+ ) -> CircuitStatus {
42
+ let mult = limit.multiplier();
43
+ let upper_limit = round(prev_close * (1.0 + mult), 2);
44
+ let lower_limit = round(prev_close * (1.0 - mult), 2);
45
+
46
+ let upper_distance_pct = if current_price < upper_limit {
47
+ round((upper_limit - current_price) / upper_limit * 100.0, 2)
48
+ } else {
49
+ 0.0
50
+ };
51
+
52
+ let lower_distance_pct = if current_price > lower_limit {
53
+ round((current_price - lower_limit) / lower_limit * 100.0, 2)
54
+ } else {
55
+ 0.0
56
+ };
57
+
58
+ CircuitStatus {
59
+ upper_limit,
60
+ lower_limit,
61
+ upper_distance_pct,
62
+ lower_distance_pct,
63
+ near_upper: upper_distance_pct < 1.0,
64
+ near_lower: lower_distance_pct < 1.0,
65
+ }
66
+ }
67
+
68
+ #[cfg(test)]
69
+ mod tests {
70
+ use super::*;
71
+
72
+ #[test]
73
+ fn circuit_basic() {
74
+ let status = circuit_proximity(108.0, 100.0, CircuitLimit::Percent10);
75
+ assert_eq!(status.upper_limit, 110.0);
76
+ assert_eq!(status.lower_limit, 90.0);
77
+ assert!(!status.near_upper);
78
+ assert!(!status.near_lower);
79
+ }
80
+
81
+ #[test]
82
+ fn circuit_near_upper() {
83
+ let status = circuit_proximity(109.5, 100.0, CircuitLimit::Percent10);
84
+ assert!(status.near_upper);
85
+ assert!(!status.near_lower);
86
+ }
87
+
88
+ #[test]
89
+ fn circuit_at_lower() {
90
+ let status = circuit_proximity(90.0, 100.0, CircuitLimit::Percent10);
91
+ assert_eq!(status.lower_distance_pct, 0.0);
92
+ assert!(status.near_lower);
93
+ }
94
+
95
+ #[test]
96
+ fn circuit_2_pct() {
97
+ let status = circuit_proximity(100.0, 100.0, CircuitLimit::Percent2);
98
+ assert_eq!(status.upper_limit, 102.0);
99
+ assert_eq!(status.lower_limit, 98.0);
100
+ }
101
+ }
@@ -0,0 +1,116 @@
1
+ use crate::core::utils::round;
2
+
3
+ /// Delivery trend classification.
4
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
5
+ pub enum DeliveryTrend {
6
+ StrongAccumulation, // High delivery % + price up
7
+ Accumulation, // Above avg delivery % + price up
8
+ Distribution, // Above avg delivery % + price down
9
+ StrongDistribution, // High delivery % + price down
10
+ Neutral,
11
+ }
12
+
13
+ impl std::fmt::Display for DeliveryTrend {
14
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
15
+ match self {
16
+ Self::StrongAccumulation => write!(f, "strong_accumulation"),
17
+ Self::Accumulation => write!(f, "accumulation"),
18
+ Self::Distribution => write!(f, "distribution"),
19
+ Self::StrongDistribution => write!(f, "strong_distribution"),
20
+ Self::Neutral => write!(f, "neutral"),
21
+ }
22
+ }
23
+ }
24
+
25
+ /// Delivery percentage: delivery_volume / total_volume * 100.
26
+ /// Unique to NSE/BSE — not available in US markets.
27
+ #[must_use]
28
+ pub fn delivery_pct(delivery_volume: f64, total_volume: f64) -> f64 {
29
+ if total_volume < f64::EPSILON {
30
+ return 0.0;
31
+ }
32
+ round(delivery_volume / total_volume * 100.0, 2)
33
+ }
34
+
35
+ /// Delivery trend analysis: compares recent delivery % with price movement.
36
+ /// High delivery + price up = genuine buying (accumulation).
37
+ /// High delivery + price down = genuine selling (distribution).
38
+ #[must_use]
39
+ pub fn delivery_trend(
40
+ delivery_pcts: &[f64],
41
+ closes: &[f64],
42
+ short_period: usize,
43
+ long_period: usize,
44
+ ) -> Option<DeliveryTrend> {
45
+ if delivery_pcts.len() < long_period
46
+ || closes.len() < long_period
47
+ || short_period == 0
48
+ || long_period == 0
49
+ || short_period >= long_period
50
+ {
51
+ return None;
52
+ }
53
+
54
+ let len = delivery_pcts.len();
55
+ let short_avg: f64 =
56
+ delivery_pcts[len - short_period..].iter().sum::<f64>() / short_period as f64;
57
+ let long_avg: f64 = delivery_pcts[len - long_period..].iter().sum::<f64>() / long_period as f64;
58
+
59
+ let delivery_ratio = if long_avg < f64::EPSILON {
60
+ 1.0
61
+ } else {
62
+ short_avg / long_avg
63
+ };
64
+
65
+ let clen = closes.len();
66
+ let price_change = if closes[clen - short_period] > f64::EPSILON {
67
+ (closes[clen - 1] - closes[clen - short_period]) / closes[clen - short_period]
68
+ } else {
69
+ 0.0
70
+ };
71
+
72
+ Some(match (delivery_ratio, price_change) {
73
+ (r, p) if r > 1.3 && p > 0.02 => DeliveryTrend::StrongAccumulation,
74
+ (r, p) if r > 1.1 && p > 0.0 => DeliveryTrend::Accumulation,
75
+ (r, p) if r > 1.3 && p < -0.02 => DeliveryTrend::StrongDistribution,
76
+ (r, p) if r > 1.1 && p < 0.0 => DeliveryTrend::Distribution,
77
+ _ => DeliveryTrend::Neutral,
78
+ })
79
+ }
80
+
81
+ #[cfg(test)]
82
+ mod tests {
83
+ use super::*;
84
+
85
+ #[test]
86
+ fn delivery_pct_basic() {
87
+ assert_eq!(delivery_pct(500_000.0, 1_000_000.0), 50.0);
88
+ assert_eq!(delivery_pct(0.0, 0.0), 0.0);
89
+ }
90
+
91
+ #[test]
92
+ fn accumulation() {
93
+ // High delivery + price up
94
+ let delivery_pcts = vec![40.0, 42.0, 45.0, 50.0, 55.0, 60.0, 65.0, 70.0, 72.0, 75.0];
95
+ let closes = vec![
96
+ 100.0, 101.0, 102.0, 103.0, 104.0, 105.0, 106.0, 107.0, 108.0, 110.0,
97
+ ];
98
+ let result = delivery_trend(&delivery_pcts, &closes, 3, 10).unwrap();
99
+ assert!(
100
+ result == DeliveryTrend::Accumulation || result == DeliveryTrend::StrongAccumulation
101
+ );
102
+ }
103
+
104
+ #[test]
105
+ fn distribution() {
106
+ // High delivery + price down
107
+ let delivery_pcts = vec![40.0, 42.0, 45.0, 50.0, 55.0, 60.0, 65.0, 70.0, 72.0, 75.0];
108
+ let closes = vec![
109
+ 110.0, 109.0, 108.0, 107.0, 106.0, 105.0, 104.0, 103.0, 102.0, 100.0,
110
+ ];
111
+ let result = delivery_trend(&delivery_pcts, &closes, 3, 10).unwrap();
112
+ assert!(
113
+ result == DeliveryTrend::Distribution || result == DeliveryTrend::StrongDistribution
114
+ );
115
+ }
116
+ }
@@ -0,0 +1,2 @@
1
+ pub mod circuit;
2
+ pub mod delivery;
@@ -0,0 +1,6 @@
1
+ pub mod india;
2
+ pub mod momentum;
3
+ pub mod support_resistance;
4
+ pub mod trend;
5
+ pub mod volatility;
6
+ pub mod volume;
@@ -0,0 +1,123 @@
1
+ use crate::core::utils::round;
2
+ use crate::indicators::trend::ema::ema_series;
3
+
4
+ /// MACD crossover direction.
5
+ #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
6
+ pub enum Crossover {
7
+ Bullish,
8
+ Bearish,
9
+ Neutral,
10
+ }
11
+
12
+ impl std::fmt::Display for Crossover {
13
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
14
+ match self {
15
+ Self::Bullish => write!(f, "bullish"),
16
+ Self::Bearish => write!(f, "bearish"),
17
+ Self::Neutral => write!(f, "none"),
18
+ }
19
+ }
20
+ }
21
+
22
+ /// MACD computation result.
23
+ #[derive(Debug, Clone)]
24
+ pub struct MacdResult {
25
+ pub value: f64,
26
+ pub signal: f64,
27
+ pub histogram: f64,
28
+ pub crossover: Crossover,
29
+ }
30
+
31
+ /// MACD (Moving Average Convergence Divergence).
32
+ #[must_use]
33
+ pub fn macd(
34
+ closes: &[f64],
35
+ fast_period: usize,
36
+ slow_period: usize,
37
+ signal_period: usize,
38
+ ) -> Option<MacdResult> {
39
+ if closes.len() < slow_period + signal_period
40
+ || fast_period == 0
41
+ || slow_period == 0
42
+ || signal_period == 0
43
+ || fast_period >= slow_period
44
+ {
45
+ return None;
46
+ }
47
+
48
+ let ema_fast = ema_series(closes, fast_period)?;
49
+ let ema_slow = ema_series(closes, slow_period)?;
50
+
51
+ let offset = slow_period - fast_period;
52
+ if ema_fast.len() <= offset {
53
+ return None;
54
+ }
55
+
56
+ let macd_line: Vec<f64> = ema_fast[offset..]
57
+ .iter()
58
+ .zip(ema_slow.iter())
59
+ .map(|(f, s)| f - s)
60
+ .collect();
61
+
62
+ if macd_line.len() < signal_period {
63
+ return None;
64
+ }
65
+
66
+ let signal_series = ema_series(&macd_line, signal_period)?;
67
+ let current_signal = *signal_series.last()?;
68
+ let current_macd = *macd_line.last()?;
69
+ let histogram = current_macd - current_signal;
70
+
71
+ let prev_signal = if signal_series.len() >= 2 {
72
+ signal_series[signal_series.len() - 2]
73
+ } else {
74
+ current_signal
75
+ };
76
+ let prev_macd = if macd_line.len() >= 2 {
77
+ macd_line[macd_line.len() - 2]
78
+ } else {
79
+ current_macd
80
+ };
81
+ let prev_histogram = prev_macd - prev_signal;
82
+
83
+ let crossover = if prev_histogram <= 0.0 && histogram > 0.0 {
84
+ Crossover::Bullish
85
+ } else if prev_histogram >= 0.0 && histogram < 0.0 {
86
+ Crossover::Bearish
87
+ } else {
88
+ Crossover::Neutral
89
+ };
90
+
91
+ Some(MacdResult {
92
+ value: round(current_macd, 2),
93
+ signal: round(current_signal, 2),
94
+ histogram: round(histogram, 2),
95
+ crossover,
96
+ })
97
+ }
98
+
99
+ #[cfg(test)]
100
+ mod tests {
101
+ use super::*;
102
+
103
+ #[test]
104
+ fn macd_uptrend() {
105
+ let closes: Vec<f64> = (0..50).map(|i| 100.0 + i as f64 * 0.5).collect();
106
+ let result = macd(&closes, 12, 26, 9).unwrap();
107
+ assert!(result.value > 0.0);
108
+ }
109
+
110
+ #[test]
111
+ fn macd_downtrend() {
112
+ let closes: Vec<f64> = (0..50).map(|i| 150.0 - i as f64 * 0.5).collect();
113
+ let result = macd(&closes, 12, 26, 9).unwrap();
114
+ assert!(result.value < 0.0);
115
+ }
116
+
117
+ #[test]
118
+ fn macd_invalid_params() {
119
+ let data = vec![1.0; 50];
120
+ assert!(macd(&data, 0, 26, 9).is_none());
121
+ assert!(macd(&data, 26, 12, 9).is_none());
122
+ }
123
+ }
@@ -0,0 +1,3 @@
1
+ pub mod macd;
2
+ pub mod rsi;
3
+ pub mod stochastic;
@@ -0,0 +1,130 @@
1
+ use crate::core::traits::Indicator;
2
+ use crate::core::types::Candle;
3
+ use crate::core::utils::wilders_step;
4
+
5
+ /// Streaming RSI (Wilder's smoothing).
6
+ pub struct Rsi {
7
+ period: usize,
8
+ count: usize,
9
+ prev_close: f64,
10
+ gain_sum: f64,
11
+ loss_sum: f64,
12
+ avg_gain: f64,
13
+ avg_loss: f64,
14
+ }
15
+
16
+ impl Rsi {
17
+ pub fn new(period: usize) -> Self {
18
+ Self {
19
+ period,
20
+ count: 0,
21
+ prev_close: 0.0,
22
+ gain_sum: 0.0,
23
+ loss_sum: 0.0,
24
+ avg_gain: 0.0,
25
+ avg_loss: 0.0,
26
+ }
27
+ }
28
+ }
29
+
30
+ impl Indicator for Rsi {
31
+ type Output = f64;
32
+
33
+ fn update(&mut self, candle: &Candle) -> Option<f64> {
34
+ self.count += 1;
35
+
36
+ if self.count == 1 {
37
+ self.prev_close = candle.close;
38
+ return None;
39
+ }
40
+
41
+ let change = candle.close - self.prev_close;
42
+ let gain = change.max(0.0);
43
+ let loss = (-change).max(0.0);
44
+ self.prev_close = candle.close;
45
+
46
+ let idx = self.count - 1;
47
+
48
+ if idx <= self.period {
49
+ self.gain_sum += gain;
50
+ self.loss_sum += loss;
51
+ if idx == self.period {
52
+ self.avg_gain = self.gain_sum / self.period as f64;
53
+ self.avg_loss = self.loss_sum / self.period as f64;
54
+ } else {
55
+ return None;
56
+ }
57
+ } else {
58
+ self.avg_gain = wilders_step(self.avg_gain, gain, self.period);
59
+ self.avg_loss = wilders_step(self.avg_loss, loss, self.period);
60
+ }
61
+
62
+ if self.avg_loss < f64::EPSILON {
63
+ return Some(100.0);
64
+ }
65
+
66
+ let rs = self.avg_gain / self.avg_loss;
67
+ Some(100.0 - 100.0 / (1.0 + rs))
68
+ }
69
+
70
+ fn reset(&mut self) {
71
+ *self = Self::new(self.period);
72
+ }
73
+ }
74
+
75
+ /// Convenience function: RSI from close values.
76
+ #[must_use]
77
+ pub fn rsi(closes: &[f64], period: usize) -> Option<f64> {
78
+ if closes.len() < period + 1 || period == 0 {
79
+ return None;
80
+ }
81
+ let candles: Vec<Candle> = closes.iter().map(|&c| Candle::from_close(c)).collect();
82
+ let mut ind = Rsi::new(period);
83
+ ind.compute_last(&candles)
84
+ }
85
+
86
+ #[cfg(test)]
87
+ mod tests {
88
+ use super::*;
89
+
90
+ #[test]
91
+ fn rsi_all_gains() {
92
+ let closes: Vec<f64> = (0..20).map(|i| 100.0 + i as f64).collect();
93
+ assert_eq!(rsi(&closes, 14), Some(100.0));
94
+ }
95
+
96
+ #[test]
97
+ fn rsi_all_losses() {
98
+ let closes: Vec<f64> = (0..20).map(|i| 100.0 - i as f64).collect();
99
+ let result = rsi(&closes, 14).unwrap();
100
+ assert!(result < 1.0);
101
+ }
102
+
103
+ #[test]
104
+ fn rsi_midrange() {
105
+ let closes = vec![
106
+ 100.0, 102.0, 100.0, 102.0, 100.0, 102.0, 100.0, 102.0, 100.0, 102.0, 100.0, 102.0,
107
+ 100.0, 102.0, 100.0, 102.0,
108
+ ];
109
+ let result = rsi(&closes, 14).unwrap();
110
+ assert!((result - 50.0).abs() < 5.0);
111
+ }
112
+
113
+ #[test]
114
+ fn rsi_insufficient() {
115
+ assert_eq!(rsi(&[100.0, 101.0], 14), None);
116
+ }
117
+
118
+ #[test]
119
+ fn rsi_streaming() {
120
+ let closes: Vec<f64> = (0..20).map(|i| 100.0 + i as f64).collect();
121
+ let fn_result = rsi(&closes, 14).unwrap();
122
+
123
+ let mut ind = Rsi::new(14);
124
+ let mut last = None;
125
+ for &c in &closes {
126
+ last = ind.update(&Candle::from_close(c));
127
+ }
128
+ assert!((last.unwrap() - fn_result).abs() < 0.01);
129
+ }
130
+ }
@@ -0,0 +1,99 @@
1
+ use crate::core::utils::round;
2
+ use std::collections::VecDeque;
3
+
4
+ /// Stochastic Oscillator result.
5
+ #[derive(Debug, Clone, Copy)]
6
+ pub struct StochasticResult {
7
+ pub k: f64, // Fast line (0-100)
8
+ pub d: f64, // Slow line (SMA of K)
9
+ }
10
+
11
+ /// Stochastic Oscillator (%K, %D).
12
+ /// %K = (close - lowest_low) / (highest_high - lowest_low) * 100
13
+ /// %D = SMA of %K over d_period
14
+ #[must_use]
15
+ pub fn stochastic(
16
+ highs: &[f64],
17
+ lows: &[f64],
18
+ closes: &[f64],
19
+ k_period: usize,
20
+ d_period: usize,
21
+ ) -> Option<StochasticResult> {
22
+ let len = closes.len();
23
+ if len < k_period + d_period - 1
24
+ || highs.len() < len
25
+ || lows.len() < len
26
+ || k_period == 0
27
+ || d_period == 0
28
+ {
29
+ return None;
30
+ }
31
+
32
+ // Compute %K values
33
+ let mut k_values = VecDeque::with_capacity(d_period);
34
+
35
+ for i in (k_period - 1)..len {
36
+ let window_highs = &highs[i + 1 - k_period..=i];
37
+ let window_lows = &lows[i + 1 - k_period..=i];
38
+
39
+ let highest = window_highs
40
+ .iter()
41
+ .cloned()
42
+ .fold(f64::NEG_INFINITY, f64::max);
43
+ let lowest = window_lows.iter().cloned().fold(f64::INFINITY, f64::min);
44
+
45
+ let range = highest - lowest;
46
+ let k = if range < f64::EPSILON {
47
+ 50.0
48
+ } else {
49
+ (closes[i] - lowest) / range * 100.0
50
+ };
51
+
52
+ k_values.push_back(k);
53
+ if k_values.len() > d_period {
54
+ k_values.pop_front();
55
+ }
56
+ }
57
+
58
+ if k_values.len() < d_period {
59
+ return None;
60
+ }
61
+
62
+ let k = *k_values.back()?;
63
+ let d = k_values.iter().sum::<f64>() / d_period as f64;
64
+
65
+ Some(StochasticResult {
66
+ k: round(k, 2),
67
+ d: round(d, 2),
68
+ })
69
+ }
70
+
71
+ #[cfg(test)]
72
+ mod tests {
73
+ use super::*;
74
+
75
+ #[test]
76
+ fn stochastic_basic() {
77
+ let highs = vec![10.0, 12.0, 14.0, 13.0, 15.0, 14.0, 16.0, 15.0, 17.0, 16.0];
78
+ let lows = vec![8.0, 9.0, 11.0, 10.0, 12.0, 11.0, 13.0, 12.0, 14.0, 13.0];
79
+ let closes = vec![9.0, 11.0, 13.0, 12.0, 14.0, 13.0, 15.0, 14.0, 16.0, 15.0];
80
+ let result = stochastic(&highs, &lows, &closes, 5, 3).unwrap();
81
+ assert!(result.k >= 0.0 && result.k <= 100.0);
82
+ assert!(result.d >= 0.0 && result.d <= 100.0);
83
+ }
84
+
85
+ #[test]
86
+ fn stochastic_overbought() {
87
+ // Price at highs = %K near 100
88
+ let highs: Vec<f64> = (0..20).map(|i| 100.0 + i as f64).collect();
89
+ let lows: Vec<f64> = highs.iter().map(|h| h - 5.0).collect();
90
+ let closes = highs.clone(); // Close at high
91
+ let result = stochastic(&highs, &lows, &closes, 14, 3).unwrap();
92
+ assert!(result.k > 80.0);
93
+ }
94
+
95
+ #[test]
96
+ fn stochastic_insufficient() {
97
+ assert!(stochastic(&[1.0; 5], &[1.0; 5], &[1.0; 5], 14, 3).is_none());
98
+ }
99
+ }
@@ -0,0 +1 @@
1
+ pub mod pivot;
@@ -0,0 +1,40 @@
1
+ use crate::core::utils::round;
2
+
3
+ #[derive(Debug, Clone)]
4
+ pub struct PivotPointsResult {
5
+ pub r3: f64,
6
+ pub r2: f64,
7
+ pub r1: f64,
8
+ pub pivot: f64,
9
+ pub s1: f64,
10
+ pub s2: f64,
11
+ pub s3: f64,
12
+ }
13
+
14
+ /// Classic Pivot Points from a single candle.
15
+ #[must_use]
16
+ pub fn pivot_points(high: f64, low: f64, close: f64) -> PivotPointsResult {
17
+ let pivot = (high + low + close) / 3.0;
18
+ PivotPointsResult {
19
+ r3: round(high + 2.0 * (pivot - low), 2),
20
+ r2: round(pivot + (high - low), 2),
21
+ r1: round(2.0 * pivot - low, 2),
22
+ pivot: round(pivot, 2),
23
+ s1: round(2.0 * pivot - high, 2),
24
+ s2: round(pivot - (high - low), 2),
25
+ s3: round(low - 2.0 * (high - pivot), 2),
26
+ }
27
+ }
28
+
29
+ #[cfg(test)]
30
+ mod tests {
31
+ use super::*;
32
+
33
+ #[test]
34
+ fn pivot_basic() {
35
+ let r = pivot_points(110.0, 90.0, 100.0);
36
+ assert_eq!(r.pivot, 100.0);
37
+ assert!(r.r1 > r.pivot);
38
+ assert!(r.s1 < r.pivot);
39
+ }
40
+ }