@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,44 @@
1
+ /// On-Balance Volume.
2
+ /// Running total: +volume on up days, -volume on down days.
3
+ #[must_use]
4
+ pub fn obv(closes: &[f64], volumes: &[f64]) -> Option<f64> {
5
+ let len = closes.len();
6
+ if len < 2 || volumes.len() < len {
7
+ return None;
8
+ }
9
+ let mut value = 0.0;
10
+ for i in 1..len {
11
+ if closes[i] > closes[i - 1] {
12
+ value += volumes[i];
13
+ } else if closes[i] < closes[i - 1] {
14
+ value -= volumes[i];
15
+ }
16
+ }
17
+ Some(value)
18
+ }
19
+
20
+ #[cfg(test)]
21
+ mod tests {
22
+ use super::*;
23
+
24
+ #[test]
25
+ fn obv_up_trend() {
26
+ let closes = vec![10.0, 11.0, 12.0, 13.0, 14.0];
27
+ let volumes = vec![100.0, 200.0, 300.0, 400.0, 500.0];
28
+ let result = obv(&closes, &volumes).unwrap();
29
+ assert_eq!(result, 1400.0); // 200+300+400+500
30
+ }
31
+
32
+ #[test]
33
+ fn obv_mixed() {
34
+ let closes = vec![10.0, 12.0, 11.0, 13.0];
35
+ let volumes = vec![100.0, 200.0, 300.0, 400.0];
36
+ let result = obv(&closes, &volumes).unwrap();
37
+ assert_eq!(result, 300.0); // +200 -300 +400
38
+ }
39
+
40
+ #[test]
41
+ fn obv_insufficient() {
42
+ assert!(obv(&[10.0], &[100.0]).is_none());
43
+ }
44
+ }
@@ -1,20 +1,15 @@
1
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".
2
+ #[must_use]
4
3
  pub fn volume_trend(volumes: &[f64]) -> &'static str {
5
4
  if volumes.len() < 20 {
6
5
  return "insufficient data";
7
6
  }
8
-
9
7
  let avg5: f64 = volumes[volumes.len() - 5..].iter().sum::<f64>() / 5.0;
10
8
  let avg20: f64 = volumes[volumes.len() - 20..].iter().sum::<f64>() / 20.0;
11
-
12
- if avg20 == 0.0 {
9
+ if avg20 < f64::EPSILON {
13
10
  return "insufficient data";
14
11
  }
15
-
16
12
  let ratio = avg5 / avg20;
17
-
18
13
  if ratio > 1.5 {
19
14
  "surging"
20
15
  } else if ratio > 1.1 {
@@ -33,32 +28,19 @@ mod tests {
33
28
  use super::*;
34
29
 
35
30
  #[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");
31
+ fn stable() {
32
+ assert_eq!(volume_trend(&vec![1000.0; 25]), "stable");
49
33
  }
50
34
 
51
35
  #[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");
36
+ fn surging() {
37
+ let mut v = vec![100.0; 20];
38
+ v.extend_from_slice(&[500.0; 5]);
39
+ assert_eq!(volume_trend(&v), "surging");
58
40
  }
59
41
 
60
42
  #[test]
61
- fn volume_insufficient() {
43
+ fn insufficient() {
62
44
  assert_eq!(volume_trend(&[100.0; 10]), "insufficient data");
63
45
  }
64
46
  }
@@ -0,0 +1,53 @@
1
+ use crate::core::utils::round;
2
+
3
+ /// Volume Weighted Average Price.
4
+ /// VWAP = sum(typical_price * volume) / sum(volume)
5
+ /// where typical_price = (high + low + close) / 3
6
+ #[must_use]
7
+ pub fn vwap(highs: &[f64], lows: &[f64], closes: &[f64], volumes: &[f64]) -> Option<f64> {
8
+ let len = closes.len();
9
+ if len == 0 || highs.len() < len || lows.len() < len || volumes.len() < len {
10
+ return None;
11
+ }
12
+
13
+ let mut cum_tp_vol = 0.0;
14
+ let mut cum_vol = 0.0;
15
+
16
+ for i in 0..len {
17
+ let typical_price = (highs[i] + lows[i] + closes[i]) / 3.0;
18
+ cum_tp_vol += typical_price * volumes[i];
19
+ cum_vol += volumes[i];
20
+ }
21
+
22
+ if cum_vol < f64::EPSILON {
23
+ return None;
24
+ }
25
+
26
+ Some(round(cum_tp_vol / cum_vol, 2))
27
+ }
28
+
29
+ #[cfg(test)]
30
+ mod tests {
31
+ use super::*;
32
+
33
+ #[test]
34
+ fn vwap_basic() {
35
+ let highs = vec![12.0, 13.0, 14.0];
36
+ let lows = vec![10.0, 11.0, 12.0];
37
+ let closes = vec![11.0, 12.0, 13.0];
38
+ let volumes = vec![1000.0, 2000.0, 3000.0];
39
+ let result = vwap(&highs, &lows, &closes, &volumes).unwrap();
40
+ assert!(result > 11.0 && result < 14.0);
41
+ }
42
+
43
+ #[test]
44
+ fn vwap_zero_volume() {
45
+ assert!(vwap(&[10.0], &[10.0], &[10.0], &[0.0]).is_none());
46
+ }
47
+
48
+ #[test]
49
+ fn vwap_empty() {
50
+ let empty: Vec<f64> = vec![];
51
+ assert!(vwap(&empty, &empty, &empty, &empty).is_none());
52
+ }
53
+ }
package/src/lib.rs CHANGED
@@ -1,25 +1,75 @@
1
1
  //! # indica
2
2
  //!
3
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;
4
+ //! Built for Indian markets. Screening to signals.
5
+ //!
6
+ //! ## Indicators
7
+ //!
8
+ //! **Trend:** SMA, EMA, Supertrend, ADX
9
+ //! **Momentum:** RSI, MACD, Stochastic
10
+ //! **Volatility:** Bollinger Bands, ATR
11
+ //! **Volume:** OBV, VWAP, Volume Trend
12
+ //! **Support/Resistance:** Pivot Points
13
+ //! **India-Specific:** Delivery Analysis, Circuit Limits
14
+ //!
15
+ //! ## Usage
16
+ //!
17
+ //! ```rust
18
+ //! use indica::{sma, rsi, supertrend};
19
+ //! ```
20
+
15
21
  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;
22
+ pub mod core;
23
+ pub mod indicators;
24
+ pub mod signals;
25
+
26
+ // ── Backward-compatible convenience re-exports ──
27
+
28
+ // Trend
29
+ pub use indicators::trend::adx::adx;
30
+ pub use indicators::trend::ema::ema;
31
+ pub use indicators::trend::sma::sma;
32
+ pub use indicators::trend::supertrend::{SupertrendDirection, SupertrendResult, supertrend};
33
+
34
+ // Momentum
35
+ pub use indicators::momentum::macd::{Crossover, MacdResult, macd};
36
+ pub use indicators::momentum::rsi::rsi;
37
+ pub use indicators::momentum::stochastic::{StochasticResult, stochastic};
38
+
39
+ // Volatility
40
+ pub use indicators::volatility::atr::atr;
41
+ pub use indicators::volatility::bollinger::{BollingerBandsResult, bollinger_bands};
42
+
43
+ // Volume
44
+ pub use indicators::volume::obv::obv;
45
+ pub use indicators::volume::volume_trend::volume_trend;
46
+ pub use indicators::volume::vwap::vwap;
47
+
48
+ // Support/Resistance
49
+ pub use indicators::support_resistance::pivot::{PivotPointsResult, pivot_points};
50
+
51
+ // India-specific
52
+ pub use indicators::india::circuit::{CircuitLimit, CircuitStatus, circuit_proximity};
53
+ pub use indicators::india::delivery::{DeliveryTrend, delivery_pct, delivery_trend};
54
+
55
+ // Core types
56
+ pub use core::traits::Indicator;
57
+ pub use core::types::Candle;
58
+
59
+ // Streaming indicator structs
60
+ pub use indicators::momentum::rsi::Rsi;
61
+ pub use indicators::trend::adx::Adx;
62
+ pub use indicators::trend::ema::Ema;
63
+ pub use indicators::trend::sma::Sma;
64
+ pub use indicators::trend::supertrend::Supertrend;
65
+
66
+ // Signals
67
+ pub use signals::engine::{IndicatorValues, SignalEngine, SignalRule};
68
+ pub use signals::presets::{momentum_trader, swing_trader};
69
+ pub use signals::types::{Signal, SignalStrength, SignalVote};
70
+
71
+ // Batch processing
72
+ pub use batch::compute::{
73
+ IndicatorSnapshot, StockData, batch_compute, batch_compute_parallel, compute_snapshot,
74
+ };
75
+ pub use batch::screen::{ScreenFilter, ScreenResult, screen, screen_precomputed};
@@ -0,0 +1,139 @@
1
+ use crate::indicators::india::delivery::DeliveryTrend;
2
+ use crate::indicators::momentum::macd::MacdResult;
3
+ use crate::indicators::momentum::stochastic::StochasticResult;
4
+ use crate::indicators::trend::supertrend::SupertrendResult;
5
+ use crate::indicators::volatility::bollinger::BollingerBandsResult;
6
+
7
+ use super::types::{Signal, SignalStrength, SignalVote};
8
+
9
+ /// Snapshot of all computed indicator values for a single stock.
10
+ #[derive(Debug, Clone, Default)]
11
+ pub struct IndicatorValues {
12
+ pub rsi: Option<f64>,
13
+ pub macd: Option<MacdResult>,
14
+ pub supertrend: Option<SupertrendResult>,
15
+ pub bollinger: Option<BollingerBandsResult>,
16
+ pub volume_trend: String,
17
+ pub adx: Option<f64>,
18
+ pub stochastic: Option<StochasticResult>,
19
+ pub delivery_trend: Option<DeliveryTrend>,
20
+ }
21
+
22
+ /// Trait for individual signal rules.
23
+ /// Each rule inspects indicator values and optionally casts a vote.
24
+ pub trait SignalRule: Send + Sync {
25
+ fn evaluate(&self, snapshot: &IndicatorValues) -> Option<SignalVote>;
26
+ }
27
+
28
+ /// Engine that collects votes from multiple rules and produces a composite signal.
29
+ pub struct SignalEngine {
30
+ rules: Vec<Box<dyn SignalRule>>,
31
+ }
32
+
33
+ impl SignalEngine {
34
+ pub fn new() -> Self {
35
+ Self { rules: Vec::new() }
36
+ }
37
+
38
+ /// Add a rule to the engine.
39
+ pub fn add_rule(&mut self, rule: Box<dyn SignalRule>) {
40
+ self.rules.push(rule);
41
+ }
42
+
43
+ /// Evaluate all rules against the given indicator values.
44
+ pub fn evaluate(&self, values: &IndicatorValues) -> Signal {
45
+ let votes: Vec<SignalVote> = self
46
+ .rules
47
+ .iter()
48
+ .filter_map(|rule| rule.evaluate(values))
49
+ .collect();
50
+
51
+ if votes.is_empty() {
52
+ return Signal {
53
+ strength: SignalStrength::Neutral,
54
+ confidence: 0.0,
55
+ reasons: vec!["No indicator data available".to_string()],
56
+ };
57
+ }
58
+
59
+ let total_weight: f64 = votes.iter().map(|v| v.weight).sum();
60
+ if total_weight < f64::EPSILON {
61
+ return Signal {
62
+ strength: SignalStrength::Neutral,
63
+ confidence: 0.0,
64
+ reasons: vec!["All weights are zero".to_string()],
65
+ };
66
+ }
67
+
68
+ // Weighted average score
69
+ let weighted_score: f64 = votes
70
+ .iter()
71
+ .map(|v| v.strength.score() * v.weight)
72
+ .sum::<f64>()
73
+ / total_weight;
74
+
75
+ let strength = if weighted_score >= 1.5 {
76
+ SignalStrength::StrongBuy
77
+ } else if weighted_score >= 0.5 {
78
+ SignalStrength::Buy
79
+ } else if weighted_score > -0.5 {
80
+ SignalStrength::Neutral
81
+ } else if weighted_score > -1.5 {
82
+ SignalStrength::Sell
83
+ } else {
84
+ SignalStrength::StrongSell
85
+ };
86
+
87
+ // Confidence = how much the votes agree (0..1)
88
+ // Perfect agreement = 1.0, evenly split = 0.0
89
+ let max_possible = 2.0; // max absolute score
90
+ let confidence = (weighted_score.abs() / max_possible).min(1.0);
91
+
92
+ let reasons: Vec<String> = votes.iter().map(|v| v.reason.clone()).collect();
93
+
94
+ Signal {
95
+ strength,
96
+ confidence,
97
+ reasons,
98
+ }
99
+ }
100
+ }
101
+
102
+ impl Default for SignalEngine {
103
+ fn default() -> Self {
104
+ Self::new()
105
+ }
106
+ }
107
+
108
+ #[cfg(test)]
109
+ mod tests {
110
+ use super::*;
111
+
112
+ struct AlwaysBuy;
113
+ impl SignalRule for AlwaysBuy {
114
+ fn evaluate(&self, _snapshot: &IndicatorValues) -> Option<SignalVote> {
115
+ Some(SignalVote {
116
+ strength: SignalStrength::Buy,
117
+ weight: 1.0,
118
+ reason: "Always buy".to_string(),
119
+ })
120
+ }
121
+ }
122
+
123
+ #[test]
124
+ fn engine_single_rule() {
125
+ let mut engine = SignalEngine::new();
126
+ engine.add_rule(Box::new(AlwaysBuy));
127
+ let signal = engine.evaluate(&IndicatorValues::default());
128
+ assert_eq!(signal.strength, SignalStrength::Buy);
129
+ assert!(!signal.reasons.is_empty());
130
+ }
131
+
132
+ #[test]
133
+ fn engine_no_rules() {
134
+ let engine = SignalEngine::new();
135
+ let signal = engine.evaluate(&IndicatorValues::default());
136
+ assert_eq!(signal.strength, SignalStrength::Neutral);
137
+ assert_eq!(signal.confidence, 0.0);
138
+ }
139
+ }
@@ -0,0 +1,4 @@
1
+ pub mod engine;
2
+ pub mod presets;
3
+ pub mod rules;
4
+ pub mod types;
@@ -0,0 +1,109 @@
1
+ use super::engine::SignalEngine;
2
+ use super::rules::{
3
+ AdxTrendRule, MacdCrossoverRule, RsiRule, StochasticRule, SupertrendRule, VolumeTrendRule,
4
+ };
5
+
6
+ /// Swing trader strategy: RSI + MACD + Supertrend + Volume.
7
+ /// Best for multi-day holds on NSE/BSE.
8
+ pub fn swing_trader() -> SignalEngine {
9
+ let mut engine = SignalEngine::new();
10
+ engine.add_rule(Box::new(RsiRule));
11
+ engine.add_rule(Box::new(MacdCrossoverRule));
12
+ engine.add_rule(Box::new(SupertrendRule));
13
+ engine.add_rule(Box::new(VolumeTrendRule));
14
+ engine
15
+ }
16
+
17
+ /// Momentum trader strategy: RSI + Stochastic + ADX + Volume.
18
+ /// Best for short-term momentum plays.
19
+ pub fn momentum_trader() -> SignalEngine {
20
+ let mut engine = SignalEngine::new();
21
+ engine.add_rule(Box::new(RsiRule));
22
+ engine.add_rule(Box::new(StochasticRule));
23
+ engine.add_rule(Box::new(AdxTrendRule));
24
+ engine.add_rule(Box::new(VolumeTrendRule));
25
+ engine
26
+ }
27
+
28
+ #[cfg(test)]
29
+ mod tests {
30
+ use super::*;
31
+ use crate::indicators::momentum::macd::{Crossover, MacdResult};
32
+ use crate::indicators::momentum::stochastic::StochasticResult;
33
+ use crate::indicators::trend::supertrend::{SupertrendDirection, SupertrendResult};
34
+ use crate::signals::engine::IndicatorValues;
35
+ use crate::signals::types::SignalStrength;
36
+
37
+ #[test]
38
+ fn swing_trader_bullish() {
39
+ let engine = swing_trader();
40
+ let vals = IndicatorValues {
41
+ rsi: Some(28.0),
42
+ macd: Some(MacdResult {
43
+ value: 1.0,
44
+ signal: 0.5,
45
+ histogram: 0.5,
46
+ crossover: Crossover::Bullish,
47
+ }),
48
+ supertrend: Some(SupertrendResult {
49
+ value: 95.0,
50
+ direction: SupertrendDirection::Up,
51
+ }),
52
+ volume_trend: "surging".to_string(),
53
+ ..Default::default()
54
+ };
55
+ let signal = engine.evaluate(&vals);
56
+ assert_eq!(signal.strength, SignalStrength::Buy);
57
+ assert!(signal.confidence > 0.0);
58
+ assert_eq!(signal.reasons.len(), 4);
59
+ }
60
+
61
+ #[test]
62
+ fn swing_trader_bearish() {
63
+ let engine = swing_trader();
64
+ let vals = IndicatorValues {
65
+ rsi: Some(75.0),
66
+ macd: Some(MacdResult {
67
+ value: -1.0,
68
+ signal: -0.5,
69
+ histogram: -0.5,
70
+ crossover: Crossover::Bearish,
71
+ }),
72
+ supertrend: Some(SupertrendResult {
73
+ value: 120.0,
74
+ direction: SupertrendDirection::Down,
75
+ }),
76
+ volume_trend: "declining".to_string(),
77
+ ..Default::default()
78
+ };
79
+ let signal = engine.evaluate(&vals);
80
+ assert_eq!(signal.strength, SignalStrength::Sell);
81
+ assert!(signal.confidence > 0.0);
82
+ }
83
+
84
+ #[test]
85
+ fn momentum_trader_bullish() {
86
+ let engine = momentum_trader();
87
+ let vals = IndicatorValues {
88
+ rsi: Some(25.0),
89
+ stochastic: Some(StochasticResult { k: 18.0, d: 15.0 }),
90
+ adx: Some(45.0),
91
+ volume_trend: "surging".to_string(),
92
+ ..Default::default()
93
+ };
94
+ let signal = engine.evaluate(&vals);
95
+ assert!(
96
+ signal.strength == SignalStrength::Buy || signal.strength == SignalStrength::StrongBuy
97
+ );
98
+ assert!(signal.confidence > 0.0);
99
+ }
100
+
101
+ #[test]
102
+ fn swing_trader_no_data() {
103
+ let engine = swing_trader();
104
+ let vals = IndicatorValues::default();
105
+ let signal = engine.evaluate(&vals);
106
+ // volume_trend defaults to "" which returns None from VolumeTrendRule
107
+ assert_eq!(signal.strength, SignalStrength::Neutral);
108
+ }
109
+ }