@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,333 @@
1
+ use crate::indicators::momentum::macd::Crossover;
2
+ use crate::indicators::trend::supertrend::SupertrendDirection;
3
+
4
+ use super::engine::{IndicatorValues, SignalRule};
5
+ use super::types::{SignalStrength, SignalVote};
6
+
7
+ /// RSI-based signal: oversold = Buy, overbought = Sell.
8
+ pub struct RsiRule;
9
+
10
+ impl SignalRule for RsiRule {
11
+ fn evaluate(&self, snapshot: &IndicatorValues) -> Option<SignalVote> {
12
+ let rsi = snapshot.rsi?;
13
+ let (strength, reason) = if rsi < 20.0 {
14
+ (
15
+ SignalStrength::StrongBuy,
16
+ format!("RSI {rsi:.1} — deeply oversold"),
17
+ )
18
+ } else if rsi < 30.0 {
19
+ (SignalStrength::Buy, format!("RSI {rsi:.1} — oversold"))
20
+ } else if rsi > 80.0 {
21
+ (
22
+ SignalStrength::StrongSell,
23
+ format!("RSI {rsi:.1} — deeply overbought"),
24
+ )
25
+ } else if rsi > 70.0 {
26
+ (SignalStrength::Sell, format!("RSI {rsi:.1} — overbought"))
27
+ } else {
28
+ (
29
+ SignalStrength::Neutral,
30
+ format!("RSI {rsi:.1} — neutral zone"),
31
+ )
32
+ };
33
+ Some(SignalVote {
34
+ strength,
35
+ weight: 0.8,
36
+ reason,
37
+ })
38
+ }
39
+ }
40
+
41
+ /// MACD crossover signal.
42
+ pub struct MacdCrossoverRule;
43
+
44
+ impl SignalRule for MacdCrossoverRule {
45
+ fn evaluate(&self, snapshot: &IndicatorValues) -> Option<SignalVote> {
46
+ let macd = snapshot.macd.as_ref()?;
47
+ let (strength, reason) = match macd.crossover {
48
+ Crossover::Bullish => (
49
+ SignalStrength::Buy,
50
+ format!("MACD bullish crossover (histogram: {:.2})", macd.histogram),
51
+ ),
52
+ Crossover::Bearish => (
53
+ SignalStrength::Sell,
54
+ format!("MACD bearish crossover (histogram: {:.2})", macd.histogram),
55
+ ),
56
+ Crossover::Neutral => {
57
+ if macd.histogram > 0.0 {
58
+ (
59
+ SignalStrength::Buy,
60
+ format!("MACD positive histogram ({:.2})", macd.histogram),
61
+ )
62
+ } else if macd.histogram < 0.0 {
63
+ (
64
+ SignalStrength::Sell,
65
+ format!("MACD negative histogram ({:.2})", macd.histogram),
66
+ )
67
+ } else {
68
+ (SignalStrength::Neutral, "MACD flat".to_string())
69
+ }
70
+ }
71
+ };
72
+ Some(SignalVote {
73
+ strength,
74
+ weight: 0.9,
75
+ reason,
76
+ })
77
+ }
78
+ }
79
+
80
+ /// Supertrend direction signal.
81
+ pub struct SupertrendRule;
82
+
83
+ impl SignalRule for SupertrendRule {
84
+ fn evaluate(&self, snapshot: &IndicatorValues) -> Option<SignalVote> {
85
+ let st = snapshot.supertrend.as_ref()?;
86
+ let (strength, reason) = match st.direction {
87
+ SupertrendDirection::Up => (
88
+ SignalStrength::Buy,
89
+ format!("Supertrend UP — support at {:.2}", st.value),
90
+ ),
91
+ SupertrendDirection::Down => (
92
+ SignalStrength::Sell,
93
+ format!("Supertrend DOWN — resistance at {:.2}", st.value),
94
+ ),
95
+ };
96
+ Some(SignalVote {
97
+ strength,
98
+ weight: 1.0,
99
+ reason,
100
+ })
101
+ }
102
+ }
103
+
104
+ /// Volume trend confirmation / warning.
105
+ pub struct VolumeTrendRule;
106
+
107
+ impl SignalRule for VolumeTrendRule {
108
+ fn evaluate(&self, snapshot: &IndicatorValues) -> Option<SignalVote> {
109
+ let trend = snapshot.volume_trend.as_str();
110
+ let (strength, reason) = match trend {
111
+ "surging" => (
112
+ SignalStrength::Buy,
113
+ "Volume surging — confirms momentum".to_string(),
114
+ ),
115
+ "increasing" => (
116
+ SignalStrength::Buy,
117
+ "Volume increasing — supports trend".to_string(),
118
+ ),
119
+ "declining" => (
120
+ SignalStrength::Sell,
121
+ "Volume declining — trend weakening".to_string(),
122
+ ),
123
+ "drying up" => (
124
+ SignalStrength::StrongSell,
125
+ "Volume drying up — caution".to_string(),
126
+ ),
127
+ "stable" => (SignalStrength::Neutral, "Volume stable".to_string()),
128
+ _ => return None,
129
+ };
130
+ Some(SignalVote {
131
+ strength,
132
+ weight: 0.6,
133
+ reason,
134
+ })
135
+ }
136
+ }
137
+
138
+ /// ADX trend strength — confirms or weakens signals.
139
+ pub struct AdxTrendRule;
140
+
141
+ impl SignalRule for AdxTrendRule {
142
+ fn evaluate(&self, snapshot: &IndicatorValues) -> Option<SignalVote> {
143
+ let adx_val = snapshot.adx?;
144
+ let (strength, reason) = if adx_val > 40.0 {
145
+ (
146
+ SignalStrength::StrongBuy,
147
+ format!("ADX {adx_val:.1} — very strong trend"),
148
+ )
149
+ } else if adx_val > 25.0 {
150
+ (
151
+ SignalStrength::Neutral,
152
+ format!("ADX {adx_val:.1} — trending (confirms direction)"),
153
+ )
154
+ } else if adx_val > 20.0 {
155
+ (
156
+ SignalStrength::Neutral,
157
+ format!("ADX {adx_val:.1} — weak trend"),
158
+ )
159
+ } else {
160
+ (
161
+ SignalStrength::Neutral,
162
+ format!("ADX {adx_val:.1} — no trend, range-bound"),
163
+ )
164
+ };
165
+ Some(SignalVote {
166
+ strength,
167
+ weight: 0.5,
168
+ reason,
169
+ })
170
+ }
171
+ }
172
+
173
+ /// Stochastic Oscillator rule.
174
+ pub struct StochasticRule;
175
+
176
+ impl SignalRule for StochasticRule {
177
+ fn evaluate(&self, snapshot: &IndicatorValues) -> Option<SignalVote> {
178
+ let stoch = snapshot.stochastic.as_ref()?;
179
+ let (strength, reason) = if stoch.k < 20.0 && stoch.d < 20.0 {
180
+ (
181
+ SignalStrength::StrongBuy,
182
+ format!(
183
+ "Stochastic K={:.1} D={:.1} — deeply oversold",
184
+ stoch.k, stoch.d
185
+ ),
186
+ )
187
+ } else if stoch.k < 30.0 {
188
+ (
189
+ SignalStrength::Buy,
190
+ format!("Stochastic K={:.1} — oversold zone", stoch.k),
191
+ )
192
+ } else if stoch.k > 80.0 && stoch.d > 80.0 {
193
+ (
194
+ SignalStrength::StrongSell,
195
+ format!(
196
+ "Stochastic K={:.1} D={:.1} — deeply overbought",
197
+ stoch.k, stoch.d
198
+ ),
199
+ )
200
+ } else if stoch.k > 70.0 {
201
+ (
202
+ SignalStrength::Sell,
203
+ format!("Stochastic K={:.1} — overbought zone", stoch.k),
204
+ )
205
+ } else {
206
+ (
207
+ SignalStrength::Neutral,
208
+ format!("Stochastic K={:.1} — neutral zone", stoch.k),
209
+ )
210
+ };
211
+ Some(SignalVote {
212
+ strength,
213
+ weight: 0.7,
214
+ reason,
215
+ })
216
+ }
217
+ }
218
+
219
+ #[cfg(test)]
220
+ mod tests {
221
+ use super::*;
222
+ use crate::indicators::momentum::macd::MacdResult;
223
+ use crate::indicators::momentum::stochastic::StochasticResult;
224
+ use crate::indicators::trend::supertrend::SupertrendResult;
225
+
226
+ #[test]
227
+ fn rsi_oversold() {
228
+ let vals = IndicatorValues {
229
+ rsi: Some(25.0),
230
+ ..Default::default()
231
+ };
232
+ let vote = RsiRule.evaluate(&vals).unwrap();
233
+ assert_eq!(vote.strength, SignalStrength::Buy);
234
+ }
235
+
236
+ #[test]
237
+ fn rsi_overbought() {
238
+ let vals = IndicatorValues {
239
+ rsi: Some(75.0),
240
+ ..Default::default()
241
+ };
242
+ let vote = RsiRule.evaluate(&vals).unwrap();
243
+ assert_eq!(vote.strength, SignalStrength::Sell);
244
+ }
245
+
246
+ #[test]
247
+ fn rsi_deeply_oversold() {
248
+ let vals = IndicatorValues {
249
+ rsi: Some(15.0),
250
+ ..Default::default()
251
+ };
252
+ let vote = RsiRule.evaluate(&vals).unwrap();
253
+ assert_eq!(vote.strength, SignalStrength::StrongBuy);
254
+ }
255
+
256
+ #[test]
257
+ fn macd_bullish_crossover() {
258
+ let vals = IndicatorValues {
259
+ macd: Some(MacdResult {
260
+ value: 1.0,
261
+ signal: 0.5,
262
+ histogram: 0.5,
263
+ crossover: Crossover::Bullish,
264
+ }),
265
+ ..Default::default()
266
+ };
267
+ let vote = MacdCrossoverRule.evaluate(&vals).unwrap();
268
+ assert_eq!(vote.strength, SignalStrength::Buy);
269
+ }
270
+
271
+ #[test]
272
+ fn supertrend_up() {
273
+ let vals = IndicatorValues {
274
+ supertrend: Some(SupertrendResult {
275
+ value: 100.0,
276
+ direction: SupertrendDirection::Up,
277
+ }),
278
+ ..Default::default()
279
+ };
280
+ let vote = SupertrendRule.evaluate(&vals).unwrap();
281
+ assert_eq!(vote.strength, SignalStrength::Buy);
282
+ }
283
+
284
+ #[test]
285
+ fn volume_surging() {
286
+ let vals = IndicatorValues {
287
+ volume_trend: "surging".to_string(),
288
+ ..Default::default()
289
+ };
290
+ let vote = VolumeTrendRule.evaluate(&vals).unwrap();
291
+ assert_eq!(vote.strength, SignalStrength::Buy);
292
+ }
293
+
294
+ #[test]
295
+ fn adx_strong() {
296
+ let vals = IndicatorValues {
297
+ adx: Some(45.0),
298
+ ..Default::default()
299
+ };
300
+ let vote = AdxTrendRule.evaluate(&vals).unwrap();
301
+ assert_eq!(vote.strength, SignalStrength::StrongBuy);
302
+ }
303
+
304
+ #[test]
305
+ fn stochastic_oversold() {
306
+ let vals = IndicatorValues {
307
+ stochastic: Some(StochasticResult { k: 15.0, d: 15.0 }),
308
+ ..Default::default()
309
+ };
310
+ let vote = StochasticRule.evaluate(&vals).unwrap();
311
+ assert_eq!(vote.strength, SignalStrength::StrongBuy);
312
+ }
313
+
314
+ #[test]
315
+ fn stochastic_overbought() {
316
+ let vals = IndicatorValues {
317
+ stochastic: Some(StochasticResult { k: 85.0, d: 85.0 }),
318
+ ..Default::default()
319
+ };
320
+ let vote = StochasticRule.evaluate(&vals).unwrap();
321
+ assert_eq!(vote.strength, SignalStrength::StrongSell);
322
+ }
323
+
324
+ #[test]
325
+ fn missing_data_returns_none() {
326
+ let vals = IndicatorValues::default();
327
+ assert!(RsiRule.evaluate(&vals).is_none());
328
+ assert!(MacdCrossoverRule.evaluate(&vals).is_none());
329
+ assert!(SupertrendRule.evaluate(&vals).is_none());
330
+ assert!(AdxTrendRule.evaluate(&vals).is_none());
331
+ assert!(StochasticRule.evaluate(&vals).is_none());
332
+ }
333
+ }
@@ -0,0 +1,50 @@
1
+ /// Signal strength classification.
2
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
3
+ pub enum SignalStrength {
4
+ StrongBuy,
5
+ Buy,
6
+ Neutral,
7
+ Sell,
8
+ StrongSell,
9
+ }
10
+
11
+ impl SignalStrength {
12
+ /// Numeric score: StrongBuy = 2, Buy = 1, Neutral = 0, Sell = -1, StrongSell = -2.
13
+ pub fn score(self) -> f64 {
14
+ match self {
15
+ Self::StrongBuy => 2.0,
16
+ Self::Buy => 1.0,
17
+ Self::Neutral => 0.0,
18
+ Self::Sell => -1.0,
19
+ Self::StrongSell => -2.0,
20
+ }
21
+ }
22
+ }
23
+
24
+ impl std::fmt::Display for SignalStrength {
25
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
26
+ match self {
27
+ Self::StrongBuy => write!(f, "strong_buy"),
28
+ Self::Buy => write!(f, "buy"),
29
+ Self::Neutral => write!(f, "neutral"),
30
+ Self::Sell => write!(f, "sell"),
31
+ Self::StrongSell => write!(f, "strong_sell"),
32
+ }
33
+ }
34
+ }
35
+
36
+ /// Composite signal after aggregating all rule votes.
37
+ #[derive(Debug, Clone)]
38
+ pub struct Signal {
39
+ pub strength: SignalStrength,
40
+ pub confidence: f64,
41
+ pub reasons: Vec<String>,
42
+ }
43
+
44
+ /// A single vote from one signal rule.
45
+ #[derive(Debug, Clone)]
46
+ pub struct SignalVote {
47
+ pub strength: SignalStrength,
48
+ pub weight: f64,
49
+ pub reason: String,
50
+ }
package/index.d.ts DELETED
@@ -1,66 +0,0 @@
1
- /* auto-generated by NAPI-RS */
2
- /* eslint-disable */
3
- export declare function batchComputeIndicators(stocks: Array<JsStockData>): Array<JsIndicatorSnapshot>
4
-
5
- export declare function calcAtr(highs: Array<number>, lows: Array<number>, closes: Array<number>, period: number): number | null
6
-
7
- export declare function calcBollingerBands(closes: Array<number>, period: number, stdDev: number): JsBollingerBands | null
8
-
9
- export declare function calcEma(values: Array<number>, period: number): number | null
10
-
11
- export declare function calcMacd(closes: Array<number>, fast: number, slow: number, signal: number): JsMacdResult | null
12
-
13
- export declare function calcPivotPoints(high: number, low: number, close: number): JsPivotPoints
14
-
15
- export declare function calcRelativeStrength(stock: Array<number>, benchmark: Array<number>, period: number): number | null
16
-
17
- export declare function calcRsi(closes: Array<number>, period: number): number | null
18
-
19
- export declare function calcSma(values: Array<number>, period: number): number | null
20
-
21
- export declare function calcVolumeTrend(volumes: Array<number>): string
22
-
23
- export interface JsBollingerBands {
24
- upper: number
25
- middle: number
26
- lower: number
27
- percentB: number
28
- }
29
-
30
- export interface JsIndicatorSnapshot {
31
- symbol: string
32
- sma20?: number
33
- sma50?: number
34
- sma200?: number
35
- ema20?: number
36
- rsi14?: number
37
- macd?: JsMacdResult
38
- bollinger?: JsBollingerBands
39
- atr14?: number
40
- volumeTrend: string
41
- }
42
-
43
- export interface JsMacdResult {
44
- value: number
45
- signal: number
46
- histogram: number
47
- crossover: string
48
- }
49
-
50
- export interface JsPivotPoints {
51
- r3: number
52
- r2: number
53
- r1: number
54
- pivot: number
55
- s1: number
56
- s2: number
57
- s3: number
58
- }
59
-
60
- export interface JsStockData {
61
- symbol: string
62
- closes: Array<number>
63
- highs: Array<number>
64
- lows: Array<number>
65
- volumes: Array<number>
66
- }
package/indica.node DELETED
Binary file
package/src/atr.rs DELETED
@@ -1,66 +0,0 @@
1
- use crate::utils::round;
2
-
3
- /// Average True Range using Wilder's smoothing.
4
- /// Requires `period + 1` data points minimum.
5
- /// Returns `None` if insufficient data.
6
- pub fn atr(highs: &[f64], lows: &[f64], closes: &[f64], period: usize) -> Option<f64> {
7
- let len = closes.len();
8
- if len < period + 1 || highs.len() < len || lows.len() < len || period == 0 {
9
- return None;
10
- }
11
-
12
- // Compute true ranges
13
- let true_ranges: Vec<f64> = (1..len)
14
- .map(|i| {
15
- let hl = highs[i] - lows[i];
16
- let hc = (highs[i] - closes[i - 1]).abs();
17
- let lc = (lows[i] - closes[i - 1]).abs();
18
- hl.max(hc).max(lc)
19
- })
20
- .collect();
21
-
22
- if true_ranges.len() < period {
23
- return None;
24
- }
25
-
26
- // Initial ATR = simple average of first `period` true ranges
27
- let mut atr_value: f64 = true_ranges[..period].iter().sum::<f64>() / period as f64;
28
-
29
- // Wilder's smoothing
30
- for &tr in &true_ranges[period..] {
31
- atr_value = (atr_value * (period as f64 - 1.0) + tr) / period as f64;
32
- }
33
-
34
- Some(round(atr_value, 2))
35
- }
36
-
37
- #[cfg(test)]
38
- mod tests {
39
- use super::*;
40
-
41
- #[test]
42
- fn atr_basic() {
43
- let highs = vec![48.7, 48.72, 48.9, 48.87, 48.82, 49.05, 49.2, 49.35,
44
- 49.92, 50.19, 50.12, 49.66, 49.88, 50.19, 50.36, 50.57];
45
- let lows = vec![47.79, 48.14, 48.39, 48.37, 48.24, 48.64, 48.94, 48.86,
46
- 49.50, 49.87, 49.20, 48.90, 49.43, 49.73, 49.26, 50.09];
47
- let closes = vec![48.16, 48.61, 48.75, 48.63, 48.74, 49.03, 49.07, 49.32,
48
- 49.91, 50.13, 49.53, 49.50, 49.75, 50.03, 49.99, 50.23];
49
- let result = atr(&highs, &lows, &closes, 14).unwrap();
50
- assert!(result > 0.0);
51
- assert!(result < 2.0); // Reasonable range for this data
52
- }
53
-
54
- #[test]
55
- fn atr_insufficient_data() {
56
- assert!(atr(&[1.0; 5], &[1.0; 5], &[1.0; 5], 14).is_none());
57
- }
58
-
59
- #[test]
60
- fn atr_flat_market() {
61
- // All same price = ATR near 0
62
- let data = vec![100.0; 20];
63
- let result = atr(&data, &data, &data, 14).unwrap();
64
- assert_eq!(result, 0.0);
65
- }
66
- }
package/src/batch.rs DELETED
@@ -1,139 +0,0 @@
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
- }