@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
package/README.md CHANGED
@@ -1,160 +1,149 @@
1
1
  # indica
2
2
 
3
- Fast technical analysis indicators for stock markets. Built in Rust.
3
+ Fast technical analysis indicators for stock markets. Built in Rust. Built for Indian markets.
4
4
 
5
- [![Crates.io](https://img.shields.io/crates/v/indica)](https://crates.io/crates/indica)
5
+ [![npm](https://img.shields.io/npm/v/@devanshhq/indica)](https://www.npmjs.com/package/@devanshhq/indica)
6
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
7
+ [![CI](https://github.com/Devansh-365/indica/actions/workflows/ci.yml/badge.svg)](https://github.com/Devansh-365/indica/actions)
7
8
 
8
9
  ## Why
9
10
 
10
11
  JavaScript/Python TA libraries are slow when screening thousands of stocks. indica computes all indicators for **2,000 stocks in 6ms** using Rust + Rayon parallelism.
11
12
 
12
- | Stocks | Sequential | Parallel (Rayon) |
13
- |--------|-----------|-----------------|
14
- | 100 | 0.5ms | 0.3ms |
15
- | 2,000 | 11ms | 6ms |
13
+ No other TA library has India-specific indicators (delivery volume analysis, circuit limit detection).
16
14
 
17
- ## Indicators
15
+ ## Indicators (16)
18
16
 
19
- - **SMA** Simple Moving Average
20
- - **EMA** — Exponential Moving Average
21
- - **RSI** Relative Strength Index (Wilder's smoothing)
22
- - **MACD** Moving Average Convergence Divergence with crossover detection
23
- - **Bollinger Bands** — Upper, middle, lower bands + %B
24
- - **ATR** Average True Range (Wilder's smoothing)
25
- - **Pivot Points** Classic (R3/R2/R1/Pivot/S1/S2/S3)
26
- - **Volume Trend** Surging / increasing / stable / declining / drying up
27
- - **Relative Strength** — Stock vs benchmark comparison
17
+ | Category | Indicators |
18
+ |----------|-----------|
19
+ | **Trend** | SMA, EMA, Supertrend, ADX |
20
+ | **Momentum** | RSI, MACD (with crossover), Stochastic (%K/%D) |
21
+ | **Volatility** | Bollinger Bands (%B), ATR |
22
+ | **Volume** | OBV, VWAP, Volume Trend |
23
+ | **Support/Resistance** | Classic Pivot Points (R3-S3) |
24
+ | **India-Specific** | Delivery % Analysis, Circuit Limit Proximity |
28
25
 
29
26
  ## Installation
30
27
 
31
- ### Rust
32
-
33
28
  ```toml
29
+ # Rust
34
30
  [dependencies]
35
31
  indica = "0.1"
36
32
  ```
37
33
 
38
- ### Node.js (via NAPI-RS)
34
+ ## Quick Start
39
35
 
40
- ```bash
41
- npm install indica
36
+ ```rust
37
+ use indica::{sma, rsi, macd, supertrend, bollinger_bands, stochastic, vwap, obv};
38
+
39
+ let closes = vec![44.34, 44.09, 44.15, 43.61, 44.33, 44.83, 45.10,
40
+ 45.42, 45.84, 46.08, 45.89, 46.03, 45.61, 46.28,
41
+ 46.28, 46.00, 46.03, 46.41, 46.22, 46.21];
42
+
43
+ sma(&closes, 20); // Some(45.52)
44
+ rsi(&closes, 14); // Some(55.37)
45
+ macd(&closes, 12, 26, 9); // Some(MacdResult { crossover: Bullish, ... })
46
+ bollinger_bands(&closes, 20, 2.0); // Some(BollingerBandsResult { upper, lower, %B })
47
+ stochastic(&highs, &lows, &closes, 14, 3); // Some(StochasticResult { k, d })
48
+ supertrend(&highs, &lows, &closes, 10, 3.0); // Some(SupertrendResult { direction: Up })
49
+ vwap(&highs, &lows, &closes, &volumes); // Some(245.67)
50
+ obv(&closes, &volumes); // Some(1234567.0)
42
51
  ```
43
52
 
44
- ## Usage (Rust)
53
+ ## Streaming API (real-time)
45
54
 
46
- ```rust
47
- use indica::{sma, rsi, macd, bollinger_bands, atr, pivot_points};
48
-
49
- fn main() {
50
- let closes = vec![44.34, 44.09, 44.15, 43.61, 44.33, 44.83, 45.10,
51
- 45.42, 45.84, 46.08, 45.89, 46.03, 45.61, 46.28,
52
- 46.28, 46.00, 46.03, 46.41, 46.22, 46.21];
55
+ Feed candles one at a time — O(1) updates, no recomputation:
53
56
 
54
- // Simple Moving Average
55
- let sma_20 = sma(&closes, 20);
56
- println!("SMA(20): {:?}", sma_20); // Some(45.52)
57
+ ```rust
58
+ use indica::{Rsi, Sma, Candle, Indicator};
57
59
 
58
- // RSI
59
- let rsi_14 = rsi(&closes, 14);
60
- println!("RSI(14): {:?}", rsi_14); // Some(55.37)
60
+ let mut rsi = Rsi::new(14);
61
+ let mut sma = Sma::new(20);
61
62
 
62
- // MACD
63
- if let Some(result) = macd(&closes, 12, 26, 9) {
64
- println!("MACD: {}, Signal: {}, Crossover: {:?}",
65
- result.value, result.signal, result.crossover);
63
+ for candle in live_feed {
64
+ if let Some(value) = rsi.update(&candle) {
65
+ println!("RSI: {:.1}", value);
66
66
  }
67
-
68
- // Bollinger Bands
69
- if let Some(bb) = bollinger_bands(&closes, 20, 2.0) {
70
- println!("BB Upper: {}, Lower: {}, %B: {}", bb.upper, bb.lower, bb.percent_b);
67
+ if let Some(value) = sma.update(&candle) {
68
+ println!("SMA: {:.2}", value);
71
69
  }
72
70
  }
73
71
  ```
74
72
 
75
- ## Batch Processing (screen thousands of stocks)
73
+ Available streaming indicators: `Sma`, `Ema`, `Rsi`, `Supertrend`, `Adx`
76
74
 
77
- ```rust
78
- use indica::batch::{StockData, batch_compute_parallel};
79
-
80
- let stocks = vec![
81
- StockData {
82
- symbol: "RELIANCE".to_string(),
83
- closes: vec![/* 250 daily closes */],
84
- highs: vec![/* ... */],
85
- lows: vec![/* ... */],
86
- volumes: vec![/* ... */],
87
- },
88
- // ... 2000 more stocks
89
- ];
90
-
91
- // Computes ALL indicators for ALL stocks using all CPU cores
92
- let results = batch_compute_parallel(&stocks);
93
-
94
- for snap in &results {
95
- println!("{}: RSI={:?}, SMA20={:?}", snap.symbol, snap.rsi_14, snap.sma_20);
96
- }
97
- ```
75
+ ## India-Specific Indicators
98
76
 
99
- ## Usage (Node.js)
77
+ Indicators that only work with Indian market data (NSE/BSE):
100
78
 
101
- ```javascript
102
- const { calcRsi, calcMacd, batchComputeIndicators } = require('indica');
79
+ ```rust
80
+ use indica::{delivery_pct, delivery_trend, circuit_proximity, CircuitLimit};
103
81
 
104
- // Single indicator
105
- const rsi = calcRsi([44.34, 44.09, /* ... */], 14);
106
- console.log('RSI:', rsi); // 55.37
82
+ // Delivery Volume Analysis (unique to NSE/BSE)
83
+ let pct = delivery_pct(500_000.0, 1_000_000.0); // 50.0%
107
84
 
108
- // MACD with crossover detection
109
- const macd = calcMacd(closes, 12, 26, 9);
110
- console.log(macd); // { value: 0.34, signal: 0.28, histogram: 0.06, crossover: 'bullish' }
85
+ // Delivery trend: high delivery + price up = genuine buying
86
+ let trend = delivery_trend(&delivery_pcts, &closes, 3, 10);
87
+ // Some(DeliveryTrend::Accumulation)
111
88
 
112
- // Batch: screen 2000 stocks at once
113
- const results = batchComputeIndicators([
114
- { symbol: 'RELIANCE', closes: [...], highs: [...], lows: [...], volumes: [...] },
115
- { symbol: 'TCS', closes: [...], highs: [...], lows: [...], volumes: [...] },
116
- // ... thousands more
117
- ]);
118
- // Returns in ~6ms using all CPU cores
89
+ // Circuit Limit Proximity
90
+ let status = circuit_proximity(108.0, 100.0, CircuitLimit::Percent10);
91
+ // CircuitStatus { upper_limit: 110.0, near_upper: false, ... }
119
92
  ```
120
93
 
121
- ## API Reference
122
-
123
- ### Single Indicators
124
-
125
- | Function | Input | Output |
126
- |----------|-------|--------|
127
- | `sma(values, period)` | `&[f64], usize` | `Option<f64>` |
128
- | `ema(values, period)` | `&[f64], usize` | `Option<f64>` |
129
- | `rsi(closes, period)` | `&[f64], usize` | `Option<f64>` |
130
- | `macd(closes, fast, slow, signal)` | `&[f64], usize, usize, usize` | `Option<MacdResult>` |
131
- | `bollinger_bands(closes, period, std_dev)` | `&[f64], usize, f64` | `Option<BollingerBandsResult>` |
132
- | `atr(highs, lows, closes, period)` | `&[f64], &[f64], &[f64], usize` | `Option<f64>` |
133
- | `pivot_points(high, low, close)` | `f64, f64, f64` | `PivotPointsResult` |
134
- | `volume_trend(volumes)` | `&[f64]` | `&str` |
135
- | `relative_strength(stock, bench, period)` | `&[f64], &[f64], usize` | `Option<f64>` |
94
+ ## Architecture
136
95
 
137
- ### Batch
138
-
139
- | Function | Input | Output |
140
- |----------|-------|--------|
141
- | `batch_compute(stocks)` | `&[StockData]` | `Vec<IndicatorSnapshot>` |
142
- | `batch_compute_parallel(stocks)` | `&[StockData]` | `Vec<IndicatorSnapshot>` |
96
+ ```
97
+ src/
98
+ ├── core/ ← Candle, Indicator trait, utils
99
+ ├── indicators/
100
+ │ ├── trend/ ← SMA, EMA, Supertrend, ADX
101
+ │ ├── momentum/ ← RSI, MACD, Stochastic
102
+ │ ├── volatility/ ← Bollinger Bands, ATR
103
+ │ ├── volume/ ← OBV, VWAP, Volume Trend
104
+ │ ├── support_resistance/ ← Pivot Points
105
+ │ └── india/ ← Delivery Analysis, Circuit Limits
106
+ ├── signals/ ← Signal engine (Buy/Sell with confidence)
107
+ └── batch/ ← Parallel batch processing + screening
108
+ ```
143
109
 
144
- All functions return `Option` (Rust's null) when there isn't enough data for the calculation. No panics, no NaN.
110
+ ## API Reference
145
111
 
146
- ## Building from Source
112
+ ### Convenience Functions
113
+
114
+ | Function | Returns |
115
+ |----------|---------|
116
+ | `sma(closes, period)` | `Option<f64>` |
117
+ | `ema(closes, period)` | `Option<f64>` |
118
+ | `rsi(closes, period)` | `Option<f64>` |
119
+ | `macd(closes, fast, slow, signal)` | `Option<MacdResult>` |
120
+ | `stochastic(highs, lows, closes, k, d)` | `Option<StochasticResult>` |
121
+ | `bollinger_bands(closes, period, mult)` | `Option<BollingerBandsResult>` |
122
+ | `atr(highs, lows, closes, period)` | `Option<f64>` |
123
+ | `supertrend(highs, lows, closes, period, mult)` | `Option<SupertrendResult>` |
124
+ | `adx(highs, lows, closes, period)` | `Option<f64>` |
125
+ | `obv(closes, volumes)` | `Option<f64>` |
126
+ | `vwap(highs, lows, closes, volumes)` | `Option<f64>` |
127
+ | `volume_trend(volumes)` | `&str` |
128
+ | `pivot_points(high, low, close)` | `PivotPointsResult` |
129
+ | `delivery_pct(delivery_vol, total_vol)` | `f64` |
130
+ | `delivery_trend(pcts, closes, short, long)` | `Option<DeliveryTrend>` |
131
+ | `circuit_proximity(price, prev_close, limit)` | `CircuitStatus` |
132
+
133
+ All functions return `Option` when there isn't enough data. No panics, no NaN.
134
+
135
+ ## Building
147
136
 
148
137
  ```bash
149
- # Rust library
150
138
  cargo build --release
151
139
  cargo test
152
-
153
- # Node.js native addon
154
- npm install
155
- npm run build
140
+ cargo clippy -- -D warnings
156
141
  ```
157
142
 
143
+ ## Contributing
144
+
145
+ PRs welcome. Run `cargo test && cargo clippy -- -D warnings && cargo fmt --check` before submitting.
146
+
158
147
  ## License
159
148
 
160
149
  MIT
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@devanshhq/indica",
3
- "version": "0.1.0",
4
- "description": "Fast technical analysis indicators for stock markets. Built in Rust.",
3
+ "version": "0.2.0",
4
+ "description": "Fast technical analysis indicators for Indian stock markets. 16 indicators, signal engine, batch screening.",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
7
7
  "repository": "https://github.com/Devansh-365/indica",
@@ -0,0 +1,165 @@
1
+ use rayon::prelude::*;
2
+
3
+ use crate::indicators::india::delivery::{DeliveryTrend, delivery_pct, delivery_trend};
4
+ use crate::indicators::momentum::macd::{MacdResult, macd};
5
+ use crate::indicators::momentum::rsi::rsi;
6
+ use crate::indicators::momentum::stochastic::{StochasticResult, stochastic};
7
+ use crate::indicators::trend::adx::adx;
8
+ use crate::indicators::trend::ema::ema;
9
+ use crate::indicators::trend::sma::sma;
10
+ use crate::indicators::trend::supertrend::{SupertrendResult, supertrend};
11
+ use crate::indicators::volatility::atr::atr;
12
+ use crate::indicators::volatility::bollinger::{BollingerBandsResult, bollinger_bands};
13
+ use crate::indicators::volume::obv::obv;
14
+ use crate::indicators::volume::volume_trend::volume_trend;
15
+ use crate::indicators::volume::vwap::vwap;
16
+
17
+ /// Input data for one stock.
18
+ #[derive(Debug, Clone)]
19
+ pub struct StockData {
20
+ pub symbol: String,
21
+ pub opens: Vec<f64>,
22
+ pub highs: Vec<f64>,
23
+ pub lows: Vec<f64>,
24
+ pub closes: Vec<f64>,
25
+ pub volumes: Vec<f64>,
26
+ /// Delivery volumes (NSE/BSE specific). Optional.
27
+ pub delivery_volumes: Option<Vec<f64>>,
28
+ }
29
+
30
+ /// Complete indicator snapshot for a single stock.
31
+ #[derive(Debug, Clone)]
32
+ pub struct IndicatorSnapshot {
33
+ pub symbol: String,
34
+ pub sma_20: Option<f64>,
35
+ pub ema_20: Option<f64>,
36
+ pub rsi_14: Option<f64>,
37
+ pub macd_result: Option<MacdResult>,
38
+ pub bollinger: Option<BollingerBandsResult>,
39
+ pub atr_14: Option<f64>,
40
+ pub supertrend: Option<SupertrendResult>,
41
+ pub stochastic: Option<StochasticResult>,
42
+ pub adx_14: Option<f64>,
43
+ pub obv: Option<f64>,
44
+ pub vwap: Option<f64>,
45
+ pub volume_trend: String,
46
+ pub delivery_trend: Option<DeliveryTrend>,
47
+ }
48
+
49
+ /// Compute all indicators for a single stock.
50
+ pub fn compute_snapshot(data: &StockData) -> IndicatorSnapshot {
51
+ let delivery_trend_val = data.delivery_volumes.as_ref().and_then(|dv| {
52
+ // Compute delivery percentages from delivery_volume / total_volume
53
+ let delivery_pcts: Vec<f64> = dv
54
+ .iter()
55
+ .zip(data.volumes.iter())
56
+ .map(|(&d, &t)| delivery_pct(d, t))
57
+ .collect();
58
+ delivery_trend(&delivery_pcts, &data.closes, 5, 20)
59
+ });
60
+
61
+ IndicatorSnapshot {
62
+ symbol: data.symbol.clone(),
63
+ sma_20: sma(&data.closes, 20),
64
+ ema_20: ema(&data.closes, 20),
65
+ rsi_14: rsi(&data.closes, 14),
66
+ macd_result: macd(&data.closes, 12, 26, 9),
67
+ bollinger: bollinger_bands(&data.closes, 20, 2.0),
68
+ atr_14: atr(&data.highs, &data.lows, &data.closes, 14),
69
+ supertrend: supertrend(&data.highs, &data.lows, &data.closes, 10, 3.0),
70
+ stochastic: stochastic(&data.highs, &data.lows, &data.closes, 14, 3),
71
+ adx_14: adx(&data.highs, &data.lows, &data.closes, 14),
72
+ obv: obv(&data.closes, &data.volumes),
73
+ vwap: vwap(&data.highs, &data.lows, &data.closes, &data.volumes),
74
+ volume_trend: volume_trend(&data.volumes).to_string(),
75
+ delivery_trend: delivery_trend_val,
76
+ }
77
+ }
78
+
79
+ /// Batch compute indicators for multiple stocks (sequential).
80
+ pub fn batch_compute(stocks: &[StockData]) -> Vec<IndicatorSnapshot> {
81
+ stocks.iter().map(compute_snapshot).collect()
82
+ }
83
+
84
+ /// Batch compute indicators for multiple stocks (parallel via rayon).
85
+ pub fn batch_compute_parallel(stocks: &[StockData]) -> Vec<IndicatorSnapshot> {
86
+ stocks.par_iter().map(compute_snapshot).collect()
87
+ }
88
+
89
+ #[cfg(test)]
90
+ mod tests {
91
+ use super::*;
92
+
93
+ fn make_stock(symbol: &str, len: usize) -> StockData {
94
+ let closes: Vec<f64> = (0..len).map(|i| 100.0 + (i as f64 * 0.5)).collect();
95
+ let highs: Vec<f64> = closes.iter().map(|c| c + 2.0).collect();
96
+ let lows: Vec<f64> = closes.iter().map(|c| c - 2.0).collect();
97
+ let opens: Vec<f64> = closes.iter().map(|c| c - 0.5).collect();
98
+ let volumes: Vec<f64> = vec![1_000_000.0; len];
99
+ StockData {
100
+ symbol: symbol.to_string(),
101
+ opens,
102
+ highs,
103
+ lows,
104
+ closes,
105
+ volumes,
106
+ delivery_volumes: None,
107
+ }
108
+ }
109
+
110
+ #[test]
111
+ fn single_stock_snapshot() {
112
+ let stock = make_stock("RELIANCE", 60);
113
+ let snap = compute_snapshot(&stock);
114
+ assert_eq!(snap.symbol, "RELIANCE");
115
+ assert!(snap.sma_20.is_some());
116
+ assert!(snap.ema_20.is_some());
117
+ assert!(snap.rsi_14.is_some());
118
+ assert!(snap.macd_result.is_some());
119
+ assert!(snap.bollinger.is_some());
120
+ assert!(snap.atr_14.is_some());
121
+ assert!(snap.supertrend.is_some());
122
+ assert!(snap.stochastic.is_some());
123
+ assert!(snap.obv.is_some());
124
+ assert!(snap.vwap.is_some());
125
+ assert_ne!(snap.volume_trend, "insufficient data");
126
+ }
127
+
128
+ #[test]
129
+ fn batch_compute_100_stocks() {
130
+ let stocks: Vec<StockData> = (0..100)
131
+ .map(|i| make_stock(&format!("STOCK{i}"), 60))
132
+ .collect();
133
+ let results = batch_compute(&stocks);
134
+ assert_eq!(results.len(), 100);
135
+ for snap in &results {
136
+ assert!(snap.rsi_14.is_some());
137
+ }
138
+ }
139
+
140
+ #[test]
141
+ fn batch_compute_parallel_100_stocks() {
142
+ let stocks: Vec<StockData> = (0..100)
143
+ .map(|i| make_stock(&format!("STOCK{i}"), 60))
144
+ .collect();
145
+ let results = batch_compute_parallel(&stocks);
146
+ assert_eq!(results.len(), 100);
147
+ for snap in &results {
148
+ assert!(snap.rsi_14.is_some());
149
+ }
150
+ }
151
+
152
+ #[test]
153
+ fn batch_with_delivery_data() {
154
+ let mut stock = make_stock("HDFC", 60);
155
+ stock.delivery_volumes = Some(vec![600_000.0; 60]);
156
+ let snap = compute_snapshot(&stock);
157
+ assert!(snap.delivery_trend.is_some());
158
+ }
159
+
160
+ #[test]
161
+ fn batch_empty() {
162
+ let results = batch_compute(&[]);
163
+ assert!(results.is_empty());
164
+ }
165
+ }
@@ -0,0 +1,2 @@
1
+ pub mod compute;
2
+ pub mod screen;
@@ -0,0 +1,176 @@
1
+ use crate::indicators::trend::supertrend::SupertrendDirection;
2
+
3
+ use super::compute::{IndicatorSnapshot, StockData, compute_snapshot};
4
+
5
+ /// Filter criteria for stock screening.
6
+ #[derive(Debug, Clone)]
7
+ pub enum ScreenFilter {
8
+ RsiBelow(f64),
9
+ RsiAbove(f64),
10
+ SupertrendUp,
11
+ SupertrendDown,
12
+ AdxAbove(f64),
13
+ VolumeAbove(f64),
14
+ }
15
+
16
+ /// A stock that passed all screening filters.
17
+ #[derive(Debug, Clone)]
18
+ pub struct ScreenResult {
19
+ pub symbol: String,
20
+ pub snapshot: IndicatorSnapshot,
21
+ }
22
+
23
+ fn matches_filter(snap: &IndicatorSnapshot, filter: &ScreenFilter, data: &StockData) -> bool {
24
+ match filter {
25
+ ScreenFilter::RsiBelow(threshold) => snap.rsi_14.is_some_and(|rsi| rsi < *threshold),
26
+ ScreenFilter::RsiAbove(threshold) => snap.rsi_14.is_some_and(|rsi| rsi > *threshold),
27
+ ScreenFilter::SupertrendUp => snap
28
+ .supertrend
29
+ .as_ref()
30
+ .is_some_and(|st| st.direction == SupertrendDirection::Up),
31
+ ScreenFilter::SupertrendDown => snap
32
+ .supertrend
33
+ .as_ref()
34
+ .is_some_and(|st| st.direction == SupertrendDirection::Down),
35
+ ScreenFilter::AdxAbove(threshold) => {
36
+ snap.adx_14.is_some_and(|adx_val| adx_val > *threshold)
37
+ }
38
+ ScreenFilter::VolumeAbove(threshold) => {
39
+ data.volumes.last().is_some_and(|&vol| vol > *threshold)
40
+ }
41
+ }
42
+ }
43
+
44
+ /// Screen stocks: compute indicators and return those matching ALL filters.
45
+ pub fn screen(stocks: &[StockData], filters: &[ScreenFilter]) -> Vec<ScreenResult> {
46
+ stocks
47
+ .iter()
48
+ .filter_map(|data| {
49
+ let snapshot = compute_snapshot(data);
50
+ let passes = filters.iter().all(|f| matches_filter(&snapshot, f, data));
51
+ if passes {
52
+ Some(ScreenResult {
53
+ symbol: data.symbol.clone(),
54
+ snapshot,
55
+ })
56
+ } else {
57
+ None
58
+ }
59
+ })
60
+ .collect()
61
+ }
62
+
63
+ /// Screen with pre-computed snapshots (avoids recomputing).
64
+ pub fn screen_precomputed(
65
+ snapshots: &[(StockData, IndicatorSnapshot)],
66
+ filters: &[ScreenFilter],
67
+ ) -> Vec<ScreenResult> {
68
+ snapshots
69
+ .iter()
70
+ .filter_map(|(data, snapshot)| {
71
+ let passes = filters.iter().all(|f| matches_filter(snapshot, f, data));
72
+ if passes {
73
+ Some(ScreenResult {
74
+ symbol: data.symbol.clone(),
75
+ snapshot: snapshot.clone(),
76
+ })
77
+ } else {
78
+ None
79
+ }
80
+ })
81
+ .collect()
82
+ }
83
+
84
+ #[cfg(test)]
85
+ mod tests {
86
+ use super::*;
87
+
88
+ fn make_uptrend_stock(symbol: &str) -> StockData {
89
+ let len = 60;
90
+ let closes: Vec<f64> = (0..len).map(|i| 100.0 + i as f64 * 2.0).collect();
91
+ let highs: Vec<f64> = closes.iter().map(|c| c + 2.0).collect();
92
+ let lows: Vec<f64> = closes.iter().map(|c| c - 2.0).collect();
93
+ let opens: Vec<f64> = closes.iter().map(|c| c - 0.5).collect();
94
+ let volumes: Vec<f64> = vec![2_000_000.0; len];
95
+ StockData {
96
+ symbol: symbol.to_string(),
97
+ opens,
98
+ highs,
99
+ lows,
100
+ closes,
101
+ volumes,
102
+ delivery_volumes: None,
103
+ }
104
+ }
105
+
106
+ fn make_downtrend_stock(symbol: &str) -> StockData {
107
+ let len = 60;
108
+ let closes: Vec<f64> = (0..len).map(|i| 200.0 - i as f64 * 2.0).collect();
109
+ let highs: Vec<f64> = closes.iter().map(|c| c + 2.0).collect();
110
+ let lows: Vec<f64> = closes.iter().map(|c| c - 2.0).collect();
111
+ let opens: Vec<f64> = closes.iter().map(|c| c + 0.5).collect();
112
+ let volumes: Vec<f64> = vec![500_000.0; len];
113
+ StockData {
114
+ symbol: symbol.to_string(),
115
+ opens,
116
+ highs,
117
+ lows,
118
+ closes,
119
+ volumes,
120
+ delivery_volumes: None,
121
+ }
122
+ }
123
+
124
+ #[test]
125
+ fn screen_supertrend_up() {
126
+ let stocks = vec![
127
+ make_uptrend_stock("RELIANCE"),
128
+ make_downtrend_stock("YESBANK"),
129
+ ];
130
+ let results = screen(&stocks, &[ScreenFilter::SupertrendUp]);
131
+ assert!(results.iter().any(|r| r.symbol == "RELIANCE"));
132
+ assert!(!results.iter().any(|r| r.symbol == "YESBANK"));
133
+ }
134
+
135
+ #[test]
136
+ fn screen_volume_above() {
137
+ let stocks = vec![
138
+ make_uptrend_stock("RELIANCE"), // 2M volume
139
+ make_downtrend_stock("YESBANK"), // 500K volume
140
+ ];
141
+ let results = screen(&stocks, &[ScreenFilter::VolumeAbove(1_000_000.0)]);
142
+ assert_eq!(results.len(), 1);
143
+ assert_eq!(results[0].symbol, "RELIANCE");
144
+ }
145
+
146
+ #[test]
147
+ fn screen_multiple_filters() {
148
+ let stocks = vec![
149
+ make_uptrend_stock("RELIANCE"),
150
+ make_downtrend_stock("YESBANK"),
151
+ ];
152
+ let results = screen(
153
+ &stocks,
154
+ &[
155
+ ScreenFilter::SupertrendUp,
156
+ ScreenFilter::VolumeAbove(1_000_000.0),
157
+ ],
158
+ );
159
+ // Only RELIANCE should match both
160
+ assert!(results.iter().all(|r| r.symbol == "RELIANCE"));
161
+ }
162
+
163
+ #[test]
164
+ fn screen_empty_filters_returns_all() {
165
+ let stocks = vec![make_uptrend_stock("A"), make_downtrend_stock("B")];
166
+ let results = screen(&stocks, &[]);
167
+ assert_eq!(results.len(), 2);
168
+ }
169
+
170
+ #[test]
171
+ fn screen_no_matches() {
172
+ let stocks = vec![make_uptrend_stock("RELIANCE")];
173
+ let results = screen(&stocks, &[ScreenFilter::SupertrendDown]);
174
+ assert!(results.is_empty());
175
+ }
176
+ }
@@ -0,0 +1,3 @@
1
+ pub mod traits;
2
+ pub mod types;
3
+ pub mod utils;
@@ -0,0 +1,37 @@
1
+ use super::types::Candle;
2
+
3
+ /// Core trait for all streaming indicators.
4
+ ///
5
+ /// Supports two modes:
6
+ /// - **Streaming**: feed candles one at a time via `update()` — O(1) per update
7
+ /// - **Batch**: pass a full array via `compute()` or `compute_last()`
8
+ ///
9
+ /// All indicators implement this trait, enabling polymorphic batch processing
10
+ /// and consistent API across the library.
11
+ pub trait Indicator: Send + Sync {
12
+ /// The output type (f64, MacdResult, BollingerBandsResult, etc.)
13
+ type Output: Clone;
14
+
15
+ /// Feed one new candle. Returns `None` while building up, `Some` once ready.
16
+ fn update(&mut self, candle: &Candle) -> Option<Self::Output>;
17
+
18
+ /// Reset internal state to start fresh.
19
+ fn reset(&mut self);
20
+
21
+ /// Compute the indicator for every candle in the array.
22
+ /// Returns a Vec of the same length as input.
23
+ fn compute(&mut self, candles: &[Candle]) -> Vec<Option<Self::Output>> {
24
+ self.reset();
25
+ candles.iter().map(|c| self.update(c)).collect()
26
+ }
27
+
28
+ /// Compute only the final value (most common use case).
29
+ fn compute_last(&mut self, candles: &[Candle]) -> Option<Self::Output> {
30
+ self.reset();
31
+ let mut last = None;
32
+ for c in candles {
33
+ last = self.update(c);
34
+ }
35
+ last
36
+ }
37
+ }
@@ -0,0 +1,22 @@
1
+ /// A single OHLCV candle.
2
+ #[derive(Debug, Clone, Copy)]
3
+ pub struct Candle {
4
+ pub open: f64,
5
+ pub high: f64,
6
+ pub low: f64,
7
+ pub close: f64,
8
+ pub volume: f64,
9
+ }
10
+
11
+ impl Candle {
12
+ /// Create a candle from just a close price (for indicators that only need close).
13
+ pub fn from_close(close: f64) -> Self {
14
+ Self {
15
+ open: close,
16
+ high: close,
17
+ low: close,
18
+ close,
19
+ volume: 0.0,
20
+ }
21
+ }
22
+ }