@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.
package/src/batch.rs ADDED
@@ -0,0 +1,139 @@
1
+ use crate::*;
2
+ use rayon::prelude::*;
3
+
4
+ /// Input data for a single stock.
5
+ #[derive(Debug, Clone)]
6
+ pub struct StockData {
7
+ pub symbol: String,
8
+ pub closes: Vec<f64>,
9
+ pub highs: Vec<f64>,
10
+ pub lows: Vec<f64>,
11
+ pub volumes: Vec<f64>,
12
+ }
13
+
14
+ /// All indicators computed for a single stock.
15
+ #[derive(Debug, Clone)]
16
+ pub struct IndicatorSnapshot {
17
+ pub symbol: String,
18
+ pub sma_20: Option<f64>,
19
+ pub sma_50: Option<f64>,
20
+ pub sma_200: Option<f64>,
21
+ pub ema_20: Option<f64>,
22
+ pub rsi_14: Option<f64>,
23
+ pub macd_result: Option<MacdResult>,
24
+ pub bollinger: Option<BollingerBandsResult>,
25
+ pub atr_14: Option<f64>,
26
+ pub volume_trend: String,
27
+ }
28
+
29
+ /// Compute all indicators for a single stock.
30
+ pub fn compute_indicators(stock: &StockData) -> IndicatorSnapshot {
31
+ IndicatorSnapshot {
32
+ symbol: stock.symbol.clone(),
33
+ sma_20: sma(&stock.closes, 20),
34
+ sma_50: sma(&stock.closes, 50),
35
+ sma_200: sma(&stock.closes, 200),
36
+ ema_20: ema(&stock.closes, 20),
37
+ rsi_14: rsi(&stock.closes, 14),
38
+ macd_result: macd(&stock.closes, 12, 26, 9),
39
+ bollinger: bollinger_bands(&stock.closes, 20, 2.0),
40
+ atr_14: atr(&stock.highs, &stock.lows, &stock.closes, 14),
41
+ volume_trend: volume_trend(&stock.volumes).to_string(),
42
+ }
43
+ }
44
+
45
+ /// Compute indicators for multiple stocks sequentially.
46
+ pub fn batch_compute(stocks: &[StockData]) -> Vec<IndicatorSnapshot> {
47
+ stocks.iter().map(compute_indicators).collect()
48
+ }
49
+
50
+ /// Compute indicators for multiple stocks in parallel using Rayon.
51
+ /// Automatically distributes work across all CPU cores.
52
+ pub fn batch_compute_parallel(stocks: &[StockData]) -> Vec<IndicatorSnapshot> {
53
+ stocks.par_iter().map(compute_indicators).collect()
54
+ }
55
+
56
+ #[cfg(test)]
57
+ mod tests {
58
+ use super::*;
59
+
60
+ fn make_stock(symbol: &str, days: usize) -> StockData {
61
+ let closes: Vec<f64> = (0..days).map(|i| 100.0 + (i as f64 * 0.5)).collect();
62
+ let highs: Vec<f64> = closes.iter().map(|c| c + 2.0).collect();
63
+ let lows: Vec<f64> = closes.iter().map(|c| c - 2.0).collect();
64
+ let volumes: Vec<f64> = vec![1_000_000.0; days];
65
+ StockData {
66
+ symbol: symbol.to_string(),
67
+ closes,
68
+ highs,
69
+ lows,
70
+ volumes,
71
+ }
72
+ }
73
+
74
+ #[test]
75
+ fn single_stock() {
76
+ let stock = make_stock("RELIANCE", 250);
77
+ let result = compute_indicators(&stock);
78
+ assert_eq!(result.symbol, "RELIANCE");
79
+ assert!(result.sma_20.is_some());
80
+ assert!(result.sma_50.is_some());
81
+ assert!(result.sma_200.is_some());
82
+ assert!(result.rsi_14.is_some());
83
+ assert!(result.macd_result.is_some());
84
+ assert!(result.bollinger.is_some());
85
+ assert!(result.atr_14.is_some());
86
+ assert_eq!(result.volume_trend, "stable");
87
+ }
88
+
89
+ #[test]
90
+ fn batch_multiple() {
91
+ let stocks: Vec<StockData> = ["RELIANCE", "TCS", "INFY", "SBIN", "HDFCBANK"]
92
+ .iter()
93
+ .map(|s| make_stock(s, 250))
94
+ .collect();
95
+
96
+ let results = batch_compute(&stocks);
97
+ assert_eq!(results.len(), 5);
98
+ assert_eq!(results[0].symbol, "RELIANCE");
99
+ assert_eq!(results[4].symbol, "HDFCBANK");
100
+ }
101
+
102
+ #[test]
103
+ fn batch_2000_stocks() {
104
+ let stocks: Vec<StockData> = (0..2000)
105
+ .map(|i| make_stock(&format!("STOCK{}", i), 250))
106
+ .collect();
107
+
108
+ let start = std::time::Instant::now();
109
+ let results = batch_compute(&stocks);
110
+ let elapsed = start.elapsed();
111
+
112
+ assert_eq!(results.len(), 2000);
113
+ println!("2000 stocks sequential: {:?}", elapsed);
114
+ }
115
+
116
+ #[test]
117
+ fn batch_2000_parallel() {
118
+ let stocks: Vec<StockData> = (0..2000)
119
+ .map(|i| make_stock(&format!("STOCK{}", i), 250))
120
+ .collect();
121
+
122
+ let start = std::time::Instant::now();
123
+ let results = batch_compute_parallel(&stocks);
124
+ let elapsed = start.elapsed();
125
+
126
+ assert_eq!(results.len(), 2000);
127
+ println!("2000 stocks parallel: {:?}", elapsed);
128
+ }
129
+
130
+ #[test]
131
+ fn insufficient_data_stock() {
132
+ let stock = make_stock("NEWIPO", 10);
133
+ let result = compute_indicators(&stock);
134
+ assert_eq!(result.symbol, "NEWIPO");
135
+ assert!(result.sma_20.is_none());
136
+ assert!(result.rsi_14.is_none());
137
+ assert!(result.macd_result.is_none());
138
+ }
139
+ }
@@ -0,0 +1,73 @@
1
+ use crate::utils::round;
2
+
3
+ /// Bollinger Bands result.
4
+ #[derive(Debug, Clone)]
5
+ pub struct BollingerBandsResult {
6
+ pub upper: f64,
7
+ pub middle: f64,
8
+ pub lower: f64,
9
+ pub percent_b: f64,
10
+ }
11
+
12
+ /// Bollinger Bands (default: period=20, std_dev_multiplier=2.0).
13
+ /// Returns `None` if insufficient data.
14
+ pub fn bollinger_bands(
15
+ closes: &[f64],
16
+ period: usize,
17
+ std_dev_multiplier: f64,
18
+ ) -> Option<BollingerBandsResult> {
19
+ if closes.len() < period || period == 0 {
20
+ return None;
21
+ }
22
+
23
+ let slice = &closes[closes.len() - period..];
24
+ 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;
28
+ let std_dev = variance.sqrt();
29
+
30
+ let upper = middle + std_dev_multiplier * std_dev;
31
+ let lower = middle - std_dev_multiplier * std_dev;
32
+
33
+ let current_price = *closes.last().unwrap();
34
+ let percent_b = if (upper - lower).abs() < f64::EPSILON {
35
+ 0.5
36
+ } else {
37
+ (current_price - lower) / (upper - lower)
38
+ };
39
+
40
+ Some(BollingerBandsResult {
41
+ upper: round(upper, 2),
42
+ middle: round(middle, 2),
43
+ lower: round(lower, 2),
44
+ percent_b: round(percent_b, 2),
45
+ })
46
+ }
47
+
48
+ #[cfg(test)]
49
+ mod tests {
50
+ use super::*;
51
+
52
+ #[test]
53
+ fn bb_basic() {
54
+ let closes: Vec<f64> = (1..=20).map(|i| i as f64).collect();
55
+ let result = bollinger_bands(&closes, 20, 2.0).unwrap();
56
+ assert!(result.upper > result.middle);
57
+ 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
+ }
68
+
69
+ #[test]
70
+ fn bb_insufficient_data() {
71
+ assert!(bollinger_bands(&[1.0; 5], 20, 2.0).is_none());
72
+ }
73
+ }
package/src/lib.rs ADDED
@@ -0,0 +1,25 @@
1
+ //! # indica
2
+ //!
3
+ //! Fast technical analysis indicators for stock markets.
4
+ //! SMA, EMA, RSI, MACD, Bollinger Bands, ATR, Pivot Points, and more.
5
+
6
+ mod moving_avg;
7
+ mod rsi;
8
+ mod macd;
9
+ mod bollinger;
10
+ mod atr;
11
+ mod pivot;
12
+ mod volume;
13
+ mod relative_strength;
14
+ mod utils;
15
+ pub mod batch;
16
+ mod napi_bindings;
17
+
18
+ pub use moving_avg::{sma, ema};
19
+ pub use rsi::rsi;
20
+ pub use macd::{macd, MacdResult, Crossover};
21
+ pub use bollinger::{bollinger_bands, BollingerBandsResult};
22
+ pub use atr::atr;
23
+ pub use pivot::{pivot_points, PivotPointsResult};
24
+ pub use volume::volume_trend;
25
+ pub use relative_strength::relative_strength;
package/src/macd.rs ADDED
@@ -0,0 +1,132 @@
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
+ }
@@ -0,0 +1,71 @@
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
+ }
@@ -0,0 +1,166 @@
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 ADDED
@@ -0,0 +1,58 @@
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
+ }