@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.
- package/.pm/indica-research-report.md +390 -0
- package/ARCHITECTURE.md +470 -0
- package/Cargo.lock +1 -1
- package/Cargo.toml +2 -2
- package/README.md +98 -109
- package/package.json +2 -2
- package/src/batch/compute.rs +165 -0
- package/src/batch/mod.rs +2 -0
- package/src/batch/screen.rs +176 -0
- package/src/core/mod.rs +3 -0
- package/src/core/traits.rs +37 -0
- package/src/core/types.rs +22 -0
- package/src/core/utils.rs +43 -0
- package/src/indicators/india/circuit.rs +101 -0
- package/src/indicators/india/delivery.rs +116 -0
- package/src/indicators/india/mod.rs +2 -0
- package/src/indicators/mod.rs +6 -0
- package/src/indicators/momentum/macd.rs +123 -0
- package/src/indicators/momentum/mod.rs +3 -0
- package/src/indicators/momentum/rsi.rs +130 -0
- package/src/indicators/momentum/stochastic.rs +99 -0
- package/src/indicators/support_resistance/mod.rs +1 -0
- package/src/indicators/support_resistance/pivot.rs +40 -0
- package/src/indicators/trend/adx.rs +168 -0
- package/src/indicators/trend/ema.rs +110 -0
- package/src/indicators/trend/mod.rs +4 -0
- package/src/indicators/trend/sma.rs +75 -0
- package/src/indicators/trend/supertrend.rs +193 -0
- package/src/indicators/volatility/atr.rs +51 -0
- package/src/{bollinger.rs → indicators/volatility/bollinger.rs} +6 -22
- package/src/indicators/volatility/mod.rs +2 -0
- package/src/indicators/volume/mod.rs +3 -0
- package/src/indicators/volume/obv.rs +44 -0
- package/src/{volume.rs → indicators/volume/volume_trend.rs} +9 -27
- package/src/indicators/volume/vwap.rs +53 -0
- package/src/lib.rs +71 -21
- package/src/signals/engine.rs +139 -0
- package/src/signals/mod.rs +4 -0
- package/src/signals/presets.rs +109 -0
- package/src/signals/rules.rs +333 -0
- package/src/signals/types.rs +50 -0
- package/index.d.ts +0 -66
- package/indica.node +0 -0
- package/src/atr.rs +0 -66
- package/src/batch.rs +0 -139
- package/src/macd.rs +0 -132
- package/src/moving_avg.rs +0 -71
- package/src/napi_bindings.rs +0 -166
- package/src/pivot.rs +0 -58
- package/src/relative_strength.rs +0 -67
- package/src/rsi.rs +0 -74
- 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
|
-
[](https://www.npmjs.com/package/@devanshhq/indica)
|
|
6
6
|
[](LICENSE)
|
|
7
|
+
[](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
|
-
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
34
|
+
## Quick Start
|
|
39
35
|
|
|
40
|
-
```
|
|
41
|
-
|
|
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
|
-
##
|
|
53
|
+
## Streaming API (real-time)
|
|
45
54
|
|
|
46
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
println!("SMA(20): {:?}", sma_20); // Some(45.52)
|
|
57
|
+
```rust
|
|
58
|
+
use indica::{Rsi, Sma, Candle, Indicator};
|
|
57
59
|
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
63
|
-
if let Some(
|
|
64
|
-
println!("
|
|
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
|
-
|
|
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
|
-
|
|
73
|
+
Available streaming indicators: `Sma`, `Ema`, `Rsi`, `Supertrend`, `Adx`
|
|
76
74
|
|
|
77
|
-
|
|
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
|
-
|
|
77
|
+
Indicators that only work with Indian market data (NSE/BSE):
|
|
100
78
|
|
|
101
|
-
```
|
|
102
|
-
|
|
79
|
+
```rust
|
|
80
|
+
use indica::{delivery_pct, delivery_trend, circuit_proximity, CircuitLimit};
|
|
103
81
|
|
|
104
|
-
//
|
|
105
|
-
|
|
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
|
-
//
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
//
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
110
|
+
## API Reference
|
|
145
111
|
|
|
146
|
-
|
|
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.
|
|
4
|
-
"description": "Fast technical analysis indicators for stock markets.
|
|
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
|
+
}
|
package/src/batch/mod.rs
ADDED
|
@@ -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
|
+
}
|
package/src/core/mod.rs
ADDED
|
@@ -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
|
+
}
|