@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/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
+ [![Crates.io](https://img.shields.io/crates/v/indica)](https://crates.io/crates/indica)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](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
@@ -0,0 +1,5 @@
1
+ extern crate napi_build;
2
+
3
+ fn main() {
4
+ napi_build::setup();
5
+ }
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
+ }