@devanshhq/indica 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci.yml +31 -0
- package/Cargo.lock +354 -0
- package/Cargo.toml +29 -0
- package/LICENSE +21 -0
- package/PLAN.md +355 -0
- package/README.md +160 -0
- package/build.rs +5 -0
- package/index.d.ts +66 -0
- package/indica.node +0 -0
- package/package.json +23 -0
- package/src/atr.rs +66 -0
- package/src/batch.rs +139 -0
- package/src/bollinger.rs +73 -0
- package/src/lib.rs +25 -0
- package/src/macd.rs +132 -0
- package/src/moving_avg.rs +71 -0
- package/src/napi_bindings.rs +166 -0
- package/src/pivot.rs +58 -0
- package/src/relative_strength.rs +67 -0
- package/src/rsi.rs +74 -0
- package/src/utils.rs +17 -0
- package/src/volume.rs +64 -0
package/PLAN.md
ADDED
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
# indica — Implementation Plan
|
|
2
|
+
|
|
3
|
+
> Fast technical analysis indicators for stock markets. Built in Rust.
|
|
4
|
+
> Learning Rust by building something real.
|
|
5
|
+
|
|
6
|
+
## What This Is
|
|
7
|
+
|
|
8
|
+
A Rust library that computes stock market technical indicators (SMA, RSI, MACD, Bollinger Bands, etc.) — the same math currently done in JavaScript in the Metis codebase (`lib/stock/indicators.ts`). The goal is to eventually use this from Node.js via NAPI-RS for 10-50x faster batch screening.
|
|
9
|
+
|
|
10
|
+
## Why Rust
|
|
11
|
+
|
|
12
|
+
- Learn Rust by building, not reading books
|
|
13
|
+
- Real performance gains for batch stock screening (2,000+ stocks)
|
|
14
|
+
- Publishable to crates.io (Rust) and npm (Node.js via NAPI)
|
|
15
|
+
- Portfolio project — first Rust TA library for Indian markets
|
|
16
|
+
|
|
17
|
+
## Current JS Reference
|
|
18
|
+
|
|
19
|
+
The TypeScript file we're reimplementing (`metis-2/lib/stock/indicators.ts`) has these functions:
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
sma(values, period) → number Simple Moving Average
|
|
23
|
+
ema(values, period) → number Exponential Moving Average
|
|
24
|
+
rsi(closes, period) → number RSI (Wilder's smoothing)
|
|
25
|
+
macd(closes, fast, slow, sig) → MACDResult MACD with crossover detection
|
|
26
|
+
bollingerBands(closes, p, m) → BBResult Bollinger Bands
|
|
27
|
+
atr(highs, lows, closes, p) → number Average True Range
|
|
28
|
+
pivotPoints(high, low, close) → PivotResult Classic Pivot Points
|
|
29
|
+
volumeTrend(volumes) → string Volume trend classification
|
|
30
|
+
relativeStrength(stock, bench, period) → number RS vs benchmark
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Phase 0: Rust Fundamentals (You Are Here)
|
|
36
|
+
|
|
37
|
+
**Goal:** Get comfortable with Rust basics before writing indicators.
|
|
38
|
+
|
|
39
|
+
### Concepts to learn:
|
|
40
|
+
- [x] `fn`, return types, `pub`
|
|
41
|
+
- [x] `let` (immutable) vs `let mut`
|
|
42
|
+
- [x] `println!` macro
|
|
43
|
+
- [x] `#[test]` and `assert_eq!`
|
|
44
|
+
- [ ] **Ownership & borrowing** — Rust's core concept (`&`, `&mut`)
|
|
45
|
+
- [ ] **Slices** (`&[f64]`) — how Rust handles arrays without copying
|
|
46
|
+
- [ ] **Option** (`Some(value)` / `None`) — Rust's null replacement
|
|
47
|
+
- [ ] **Vec<f64>** — growable arrays (like JS arrays)
|
|
48
|
+
- [ ] **Iterators** (`.iter()`, `.map()`, `.sum()`, `.fold()`)
|
|
49
|
+
- [ ] **Structs** — like TypeScript interfaces but with data
|
|
50
|
+
- [ ] **Enums** — like TypeScript union types but more powerful
|
|
51
|
+
- [ ] **Error handling** (`Result<T, E>`) — no try/catch in Rust
|
|
52
|
+
|
|
53
|
+
### Mini exercises (do these in src/lib.rs):
|
|
54
|
+
1. Write a function that takes `&[f64]` and returns the sum
|
|
55
|
+
2. Write a function that returns `Option<f64>` (None if empty slice)
|
|
56
|
+
3. Write a function that returns a `Vec<f64>` (new computed array)
|
|
57
|
+
4. Create a struct with named fields and a method on it
|
|
58
|
+
5. Create an enum with variants and match on it
|
|
59
|
+
|
|
60
|
+
**When you're done:** You'll understand enough Rust to start Phase 1.
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## Phase 1: Moving Averages — SMA & EMA
|
|
65
|
+
|
|
66
|
+
**Goal:** First real indicator. Teaches slices, iterators, Option.
|
|
67
|
+
|
|
68
|
+
### What to build:
|
|
69
|
+
```rust
|
|
70
|
+
/// Simple Moving Average of the last `period` values
|
|
71
|
+
pub fn sma(values: &[f64], period: usize) -> Option<f64>
|
|
72
|
+
|
|
73
|
+
/// Exponential Moving Average
|
|
74
|
+
pub fn ema(values: &[f64], period: usize) -> Option<f64>
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Rust concepts you'll use:
|
|
78
|
+
- `&[f64]` — borrowing a slice (read-only view of an array)
|
|
79
|
+
- `Option<f64>` — returns `None` instead of `NaN` when data is insufficient
|
|
80
|
+
- `.len()`, `.iter()`, `.sum::<f64>()` — iterator methods
|
|
81
|
+
- `.last()` — safely get last element
|
|
82
|
+
|
|
83
|
+
### Tests to write:
|
|
84
|
+
- SMA of `[1, 2, 3, 4, 5]` with period 3 → 4.0
|
|
85
|
+
- SMA with insufficient data → None
|
|
86
|
+
- EMA matches known values
|
|
87
|
+
- Edge cases: empty slice, period=1, period=len
|
|
88
|
+
|
|
89
|
+
### File structure:
|
|
90
|
+
```
|
|
91
|
+
src/
|
|
92
|
+
lib.rs ← re-exports everything
|
|
93
|
+
moving_avg.rs ← sma() and ema()
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## Phase 2: RSI — Wilder's Smoothing
|
|
99
|
+
|
|
100
|
+
**Goal:** More complex math. Teaches mutable state + loops.
|
|
101
|
+
|
|
102
|
+
### What to build:
|
|
103
|
+
```rust
|
|
104
|
+
/// Relative Strength Index (Wilder's smoothing)
|
|
105
|
+
pub fn rsi(closes: &[f64], period: usize) -> Option<f64>
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Rust concepts you'll use:
|
|
109
|
+
- `let mut` — mutable variables (for running averages)
|
|
110
|
+
- `for i in 1..closes.len()` — range-based loops
|
|
111
|
+
- `.abs()` — method on f64
|
|
112
|
+
- Pattern: seed with initial average, then smooth iteratively
|
|
113
|
+
|
|
114
|
+
### Tests:
|
|
115
|
+
- RSI of known data matches expected output
|
|
116
|
+
- Oversold (RSI < 30) and overbought (RSI > 70) cases
|
|
117
|
+
- All gains → RSI = 100, all losses → RSI = 0
|
|
118
|
+
- Edge: insufficient data → None
|
|
119
|
+
|
|
120
|
+
### File:
|
|
121
|
+
```
|
|
122
|
+
src/
|
|
123
|
+
rsi.rs
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## Phase 3: MACD — Structs & Enums
|
|
129
|
+
|
|
130
|
+
**Goal:** Return complex data. Teaches structs, enums, derive macros.
|
|
131
|
+
|
|
132
|
+
### What to build:
|
|
133
|
+
```rust
|
|
134
|
+
#[derive(Debug, Clone)]
|
|
135
|
+
pub enum Crossover {
|
|
136
|
+
Bullish,
|
|
137
|
+
Bearish,
|
|
138
|
+
None,
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
#[derive(Debug, Clone)]
|
|
142
|
+
pub struct MacdResult {
|
|
143
|
+
pub value: f64,
|
|
144
|
+
pub signal: f64,
|
|
145
|
+
pub histogram: f64,
|
|
146
|
+
pub crossover: Crossover,
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
pub fn macd(
|
|
150
|
+
closes: &[f64],
|
|
151
|
+
fast: usize, // default 12
|
|
152
|
+
slow: usize, // default 26
|
|
153
|
+
signal: usize, // default 9
|
|
154
|
+
) -> Option<MacdResult>
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### Rust concepts:
|
|
158
|
+
- `struct` with `pub` fields
|
|
159
|
+
- `enum` with variants (no values)
|
|
160
|
+
- `#[derive(Debug, Clone)]` — auto-generate formatting and copying
|
|
161
|
+
- `Vec<f64>` — building the MACD line series
|
|
162
|
+
- Internal helper: reuse `ema()` from Phase 1 or compute inline
|
|
163
|
+
|
|
164
|
+
### File:
|
|
165
|
+
```
|
|
166
|
+
src/
|
|
167
|
+
macd.rs
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## Phase 4: Bollinger Bands & ATR
|
|
173
|
+
|
|
174
|
+
**Goal:** Statistics (std dev) and multi-input functions.
|
|
175
|
+
|
|
176
|
+
### What to build:
|
|
177
|
+
```rust
|
|
178
|
+
#[derive(Debug, Clone)]
|
|
179
|
+
pub struct BollingerBands {
|
|
180
|
+
pub upper: f64,
|
|
181
|
+
pub middle: f64,
|
|
182
|
+
pub lower: f64,
|
|
183
|
+
pub percent_b: f64,
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
pub fn bollinger_bands(closes: &[f64], period: usize, std_dev_mult: f64) -> Option<BollingerBands>
|
|
187
|
+
|
|
188
|
+
/// Average True Range (Wilder's smoothing)
|
|
189
|
+
pub fn atr(highs: &[f64], lows: &[f64], closes: &[f64], period: usize) -> Option<f64>
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### Rust concepts:
|
|
193
|
+
- `.sqrt()`, `.powi(2)` — math on f64
|
|
194
|
+
- Multiple slice parameters (`highs`, `lows`, `closes`)
|
|
195
|
+
- `.zip()` — iterating multiple slices together
|
|
196
|
+
- `f64::max()` / `.max()` — finding maximum of values
|
|
197
|
+
|
|
198
|
+
### Files:
|
|
199
|
+
```
|
|
200
|
+
src/
|
|
201
|
+
bollinger.rs
|
|
202
|
+
atr.rs
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
## Phase 5: Pivot Points, Volume Trend, Relative Strength
|
|
208
|
+
|
|
209
|
+
**Goal:** Complete the indicator set. Teaches &str returns and simple logic.
|
|
210
|
+
|
|
211
|
+
### What to build:
|
|
212
|
+
```rust
|
|
213
|
+
#[derive(Debug, Clone)]
|
|
214
|
+
pub struct PivotPoints {
|
|
215
|
+
pub r3: f64, pub r2: f64, pub r1: f64,
|
|
216
|
+
pub pivot: f64,
|
|
217
|
+
pub s1: f64, pub s2: f64, pub s3: f64,
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
pub fn pivot_points(high: f64, low: f64, close: f64) -> PivotPoints
|
|
221
|
+
|
|
222
|
+
pub fn volume_trend(volumes: &[f64]) -> &'static str
|
|
223
|
+
|
|
224
|
+
pub fn relative_strength(stock: &[f64], benchmark: &[f64], period: usize) -> Option<f64>
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### Rust concepts:
|
|
228
|
+
- `&'static str` — string literals that live forever (like "surging", "declining")
|
|
229
|
+
- Simple arithmetic (no new concepts, just practice)
|
|
230
|
+
- Lifetime annotation basics (the `'static` part)
|
|
231
|
+
|
|
232
|
+
### Files:
|
|
233
|
+
```
|
|
234
|
+
src/
|
|
235
|
+
pivot.rs
|
|
236
|
+
volume.rs
|
|
237
|
+
relative_strength.rs
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
---
|
|
241
|
+
|
|
242
|
+
## Phase 6: Batch Processing
|
|
243
|
+
|
|
244
|
+
**Goal:** Compute indicators for 2,000+ stocks at once. Where Rust shines.
|
|
245
|
+
|
|
246
|
+
### What to build:
|
|
247
|
+
```rust
|
|
248
|
+
#[derive(Debug, Clone)]
|
|
249
|
+
pub struct StockData {
|
|
250
|
+
pub symbol: String,
|
|
251
|
+
pub closes: Vec<f64>,
|
|
252
|
+
pub highs: Vec<f64>,
|
|
253
|
+
pub lows: Vec<f64>,
|
|
254
|
+
pub volumes: Vec<f64>,
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
#[derive(Debug, Clone)]
|
|
258
|
+
pub struct IndicatorSnapshot {
|
|
259
|
+
pub symbol: String,
|
|
260
|
+
pub sma_20: Option<f64>,
|
|
261
|
+
pub sma_50: Option<f64>,
|
|
262
|
+
pub rsi_14: Option<f64>,
|
|
263
|
+
pub macd: Option<MacdResult>,
|
|
264
|
+
pub bb: Option<BollingerBands>,
|
|
265
|
+
pub atr_14: Option<f64>,
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/// Compute all indicators for multiple stocks
|
|
269
|
+
pub fn batch_compute(stocks: &[StockData]) -> Vec<IndicatorSnapshot>
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
### Rust concepts:
|
|
273
|
+
- `String` vs `&str` — owned vs borrowed strings
|
|
274
|
+
- `Vec<T>` — vectors of structs
|
|
275
|
+
- **Rayon** (parallel iterator crate) — `stocks.par_iter().map(...)` for multi-core
|
|
276
|
+
- `cargo add rayon` — adding dependencies
|
|
277
|
+
|
|
278
|
+
### Why this matters:
|
|
279
|
+
This is the function that Metis would call from Node.js. Screening 2,000 stocks through all indicators in parallel — Rust + Rayon can do this in milliseconds vs seconds in JS.
|
|
280
|
+
|
|
281
|
+
---
|
|
282
|
+
|
|
283
|
+
## Phase 7: NAPI-RS Bindings (Connect to Node.js)
|
|
284
|
+
|
|
285
|
+
**Goal:** Make the library callable from Metis's TypeScript code.
|
|
286
|
+
|
|
287
|
+
### What to do:
|
|
288
|
+
1. Add napi-rs dependencies to Cargo.toml
|
|
289
|
+
2. Create `src/napi.rs` with `#[napi]` annotated wrapper functions
|
|
290
|
+
3. Build with `cargo build --release`
|
|
291
|
+
4. Import in Metis: `import { sma, rsi, macd } from 'indica'`
|
|
292
|
+
|
|
293
|
+
### Rust concepts:
|
|
294
|
+
- `#[napi]` macro — generates JS bindings
|
|
295
|
+
- Type conversion: JS number[] ↔ Rust Vec<f64>
|
|
296
|
+
- Publishing to npm via napi-rs CLI
|
|
297
|
+
|
|
298
|
+
### This is the payoff:
|
|
299
|
+
```typescript
|
|
300
|
+
// In Metis — same API, 50x faster
|
|
301
|
+
import { batchCompute } from 'indica';
|
|
302
|
+
|
|
303
|
+
const results = batchCompute(allStocks); // milliseconds, not seconds
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
---
|
|
307
|
+
|
|
308
|
+
## File Structure (final)
|
|
309
|
+
|
|
310
|
+
```
|
|
311
|
+
indica/
|
|
312
|
+
├── Cargo.toml
|
|
313
|
+
├── src/
|
|
314
|
+
│ ├── lib.rs ← re-exports all modules
|
|
315
|
+
│ ├── moving_avg.rs ← Phase 1: SMA, EMA
|
|
316
|
+
│ ├── rsi.rs ← Phase 2: RSI
|
|
317
|
+
│ ├── macd.rs ← Phase 3: MACD
|
|
318
|
+
│ ├── bollinger.rs ← Phase 4: Bollinger Bands
|
|
319
|
+
│ ├── atr.rs ← Phase 4: ATR
|
|
320
|
+
│ ├── pivot.rs ← Phase 5: Pivot Points
|
|
321
|
+
│ ├── volume.rs ← Phase 5: Volume Trend
|
|
322
|
+
│ ├── relative_strength.rs← Phase 5: RS vs Benchmark
|
|
323
|
+
│ ├── batch.rs ← Phase 6: Batch processing
|
|
324
|
+
│ └── napi.rs ← Phase 7: Node.js bindings
|
|
325
|
+
├── tests/
|
|
326
|
+
│ └── integration.rs ← Cross-module integration tests
|
|
327
|
+
├── benches/
|
|
328
|
+
│ └── indicators.rs ← Performance benchmarks
|
|
329
|
+
├── PLAN.md ← This file
|
|
330
|
+
└── README.md
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
---
|
|
334
|
+
|
|
335
|
+
## Learning Resources
|
|
336
|
+
|
|
337
|
+
- **The Rust Book** (free): https://doc.rust-lang.org/book/
|
|
338
|
+
- Ch 4: Ownership (READ THIS FIRST)
|
|
339
|
+
- Ch 5: Structs
|
|
340
|
+
- Ch 6: Enums and Pattern Matching
|
|
341
|
+
- Ch 8: Vectors
|
|
342
|
+
- Ch 13: Iterators
|
|
343
|
+
- **Rust by Example** (free): https://doc.rust-lang.org/rust-by-example/
|
|
344
|
+
- **Exercism Rust Track** (free): https://exercism.org/tracks/rust
|
|
345
|
+
- **ta crate** (study code): https://github.com/greyblake/ta-rs
|
|
346
|
+
|
|
347
|
+
---
|
|
348
|
+
|
|
349
|
+
## How We'll Work
|
|
350
|
+
|
|
351
|
+
1. Start each phase by reading the relevant Rust Book chapter
|
|
352
|
+
2. Write the functions with tests
|
|
353
|
+
3. `cargo test -- --nocapture` to verify
|
|
354
|
+
4. Commit after each phase
|
|
355
|
+
5. Ask questions when stuck — that's the whole point
|
package/README.md
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# indica
|
|
2
|
+
|
|
3
|
+
Fast technical analysis indicators for stock markets. Built in Rust.
|
|
4
|
+
|
|
5
|
+
[](https://crates.io/crates/indica)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
|
|
8
|
+
## Why
|
|
9
|
+
|
|
10
|
+
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
|
+
| Stocks | Sequential | Parallel (Rayon) |
|
|
13
|
+
|--------|-----------|-----------------|
|
|
14
|
+
| 100 | 0.5ms | 0.3ms |
|
|
15
|
+
| 2,000 | 11ms | 6ms |
|
|
16
|
+
|
|
17
|
+
## Indicators
|
|
18
|
+
|
|
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
|
|
28
|
+
|
|
29
|
+
## Installation
|
|
30
|
+
|
|
31
|
+
### Rust
|
|
32
|
+
|
|
33
|
+
```toml
|
|
34
|
+
[dependencies]
|
|
35
|
+
indica = "0.1"
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Node.js (via NAPI-RS)
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
npm install indica
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Usage (Rust)
|
|
45
|
+
|
|
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];
|
|
53
|
+
|
|
54
|
+
// Simple Moving Average
|
|
55
|
+
let sma_20 = sma(&closes, 20);
|
|
56
|
+
println!("SMA(20): {:?}", sma_20); // Some(45.52)
|
|
57
|
+
|
|
58
|
+
// RSI
|
|
59
|
+
let rsi_14 = rsi(&closes, 14);
|
|
60
|
+
println!("RSI(14): {:?}", rsi_14); // Some(55.37)
|
|
61
|
+
|
|
62
|
+
// MACD
|
|
63
|
+
if let Some(result) = macd(&closes, 12, 26, 9) {
|
|
64
|
+
println!("MACD: {}, Signal: {}, Crossover: {:?}",
|
|
65
|
+
result.value, result.signal, result.crossover);
|
|
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);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Batch Processing (screen thousands of stocks)
|
|
76
|
+
|
|
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
|
+
```
|
|
98
|
+
|
|
99
|
+
## Usage (Node.js)
|
|
100
|
+
|
|
101
|
+
```javascript
|
|
102
|
+
const { calcRsi, calcMacd, batchComputeIndicators } = require('indica');
|
|
103
|
+
|
|
104
|
+
// Single indicator
|
|
105
|
+
const rsi = calcRsi([44.34, 44.09, /* ... */], 14);
|
|
106
|
+
console.log('RSI:', rsi); // 55.37
|
|
107
|
+
|
|
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' }
|
|
111
|
+
|
|
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
|
|
119
|
+
```
|
|
120
|
+
|
|
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>` |
|
|
136
|
+
|
|
137
|
+
### Batch
|
|
138
|
+
|
|
139
|
+
| Function | Input | Output |
|
|
140
|
+
|----------|-------|--------|
|
|
141
|
+
| `batch_compute(stocks)` | `&[StockData]` | `Vec<IndicatorSnapshot>` |
|
|
142
|
+
| `batch_compute_parallel(stocks)` | `&[StockData]` | `Vec<IndicatorSnapshot>` |
|
|
143
|
+
|
|
144
|
+
All functions return `Option` (Rust's null) when there isn't enough data for the calculation. No panics, no NaN.
|
|
145
|
+
|
|
146
|
+
## Building from Source
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
# Rust library
|
|
150
|
+
cargo build --release
|
|
151
|
+
cargo test
|
|
152
|
+
|
|
153
|
+
# Node.js native addon
|
|
154
|
+
npm install
|
|
155
|
+
npm run build
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## License
|
|
159
|
+
|
|
160
|
+
MIT
|
package/build.rs
ADDED
package/index.d.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
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
ADDED
|
Binary file
|
package/package.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@devanshhq/indica",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Fast technical analysis indicators for stock markets. Built in Rust.",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"types": "index.d.ts",
|
|
7
|
+
"repository": "https://github.com/Devansh-365/indica",
|
|
8
|
+
"license": "MIT",
|
|
9
|
+
"keywords": ["trading", "technical-analysis", "indicators", "rust", "napi", "stocks", "nse"],
|
|
10
|
+
"napi": {
|
|
11
|
+
"name": "indica",
|
|
12
|
+
"triples": {
|
|
13
|
+
"defaults": true
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "napi build --release",
|
|
18
|
+
"build:debug": "napi build"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@napi-rs/cli": "^3.0.0"
|
|
22
|
+
}
|
|
23
|
+
}
|
package/src/atr.rs
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
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
|
+
}
|