@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
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
use crate::indicators::momentum::macd::Crossover;
|
|
2
|
+
use crate::indicators::trend::supertrend::SupertrendDirection;
|
|
3
|
+
|
|
4
|
+
use super::engine::{IndicatorValues, SignalRule};
|
|
5
|
+
use super::types::{SignalStrength, SignalVote};
|
|
6
|
+
|
|
7
|
+
/// RSI-based signal: oversold = Buy, overbought = Sell.
|
|
8
|
+
pub struct RsiRule;
|
|
9
|
+
|
|
10
|
+
impl SignalRule for RsiRule {
|
|
11
|
+
fn evaluate(&self, snapshot: &IndicatorValues) -> Option<SignalVote> {
|
|
12
|
+
let rsi = snapshot.rsi?;
|
|
13
|
+
let (strength, reason) = if rsi < 20.0 {
|
|
14
|
+
(
|
|
15
|
+
SignalStrength::StrongBuy,
|
|
16
|
+
format!("RSI {rsi:.1} — deeply oversold"),
|
|
17
|
+
)
|
|
18
|
+
} else if rsi < 30.0 {
|
|
19
|
+
(SignalStrength::Buy, format!("RSI {rsi:.1} — oversold"))
|
|
20
|
+
} else if rsi > 80.0 {
|
|
21
|
+
(
|
|
22
|
+
SignalStrength::StrongSell,
|
|
23
|
+
format!("RSI {rsi:.1} — deeply overbought"),
|
|
24
|
+
)
|
|
25
|
+
} else if rsi > 70.0 {
|
|
26
|
+
(SignalStrength::Sell, format!("RSI {rsi:.1} — overbought"))
|
|
27
|
+
} else {
|
|
28
|
+
(
|
|
29
|
+
SignalStrength::Neutral,
|
|
30
|
+
format!("RSI {rsi:.1} — neutral zone"),
|
|
31
|
+
)
|
|
32
|
+
};
|
|
33
|
+
Some(SignalVote {
|
|
34
|
+
strength,
|
|
35
|
+
weight: 0.8,
|
|
36
|
+
reason,
|
|
37
|
+
})
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/// MACD crossover signal.
|
|
42
|
+
pub struct MacdCrossoverRule;
|
|
43
|
+
|
|
44
|
+
impl SignalRule for MacdCrossoverRule {
|
|
45
|
+
fn evaluate(&self, snapshot: &IndicatorValues) -> Option<SignalVote> {
|
|
46
|
+
let macd = snapshot.macd.as_ref()?;
|
|
47
|
+
let (strength, reason) = match macd.crossover {
|
|
48
|
+
Crossover::Bullish => (
|
|
49
|
+
SignalStrength::Buy,
|
|
50
|
+
format!("MACD bullish crossover (histogram: {:.2})", macd.histogram),
|
|
51
|
+
),
|
|
52
|
+
Crossover::Bearish => (
|
|
53
|
+
SignalStrength::Sell,
|
|
54
|
+
format!("MACD bearish crossover (histogram: {:.2})", macd.histogram),
|
|
55
|
+
),
|
|
56
|
+
Crossover::Neutral => {
|
|
57
|
+
if macd.histogram > 0.0 {
|
|
58
|
+
(
|
|
59
|
+
SignalStrength::Buy,
|
|
60
|
+
format!("MACD positive histogram ({:.2})", macd.histogram),
|
|
61
|
+
)
|
|
62
|
+
} else if macd.histogram < 0.0 {
|
|
63
|
+
(
|
|
64
|
+
SignalStrength::Sell,
|
|
65
|
+
format!("MACD negative histogram ({:.2})", macd.histogram),
|
|
66
|
+
)
|
|
67
|
+
} else {
|
|
68
|
+
(SignalStrength::Neutral, "MACD flat".to_string())
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
Some(SignalVote {
|
|
73
|
+
strength,
|
|
74
|
+
weight: 0.9,
|
|
75
|
+
reason,
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/// Supertrend direction signal.
|
|
81
|
+
pub struct SupertrendRule;
|
|
82
|
+
|
|
83
|
+
impl SignalRule for SupertrendRule {
|
|
84
|
+
fn evaluate(&self, snapshot: &IndicatorValues) -> Option<SignalVote> {
|
|
85
|
+
let st = snapshot.supertrend.as_ref()?;
|
|
86
|
+
let (strength, reason) = match st.direction {
|
|
87
|
+
SupertrendDirection::Up => (
|
|
88
|
+
SignalStrength::Buy,
|
|
89
|
+
format!("Supertrend UP — support at {:.2}", st.value),
|
|
90
|
+
),
|
|
91
|
+
SupertrendDirection::Down => (
|
|
92
|
+
SignalStrength::Sell,
|
|
93
|
+
format!("Supertrend DOWN — resistance at {:.2}", st.value),
|
|
94
|
+
),
|
|
95
|
+
};
|
|
96
|
+
Some(SignalVote {
|
|
97
|
+
strength,
|
|
98
|
+
weight: 1.0,
|
|
99
|
+
reason,
|
|
100
|
+
})
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/// Volume trend confirmation / warning.
|
|
105
|
+
pub struct VolumeTrendRule;
|
|
106
|
+
|
|
107
|
+
impl SignalRule for VolumeTrendRule {
|
|
108
|
+
fn evaluate(&self, snapshot: &IndicatorValues) -> Option<SignalVote> {
|
|
109
|
+
let trend = snapshot.volume_trend.as_str();
|
|
110
|
+
let (strength, reason) = match trend {
|
|
111
|
+
"surging" => (
|
|
112
|
+
SignalStrength::Buy,
|
|
113
|
+
"Volume surging — confirms momentum".to_string(),
|
|
114
|
+
),
|
|
115
|
+
"increasing" => (
|
|
116
|
+
SignalStrength::Buy,
|
|
117
|
+
"Volume increasing — supports trend".to_string(),
|
|
118
|
+
),
|
|
119
|
+
"declining" => (
|
|
120
|
+
SignalStrength::Sell,
|
|
121
|
+
"Volume declining — trend weakening".to_string(),
|
|
122
|
+
),
|
|
123
|
+
"drying up" => (
|
|
124
|
+
SignalStrength::StrongSell,
|
|
125
|
+
"Volume drying up — caution".to_string(),
|
|
126
|
+
),
|
|
127
|
+
"stable" => (SignalStrength::Neutral, "Volume stable".to_string()),
|
|
128
|
+
_ => return None,
|
|
129
|
+
};
|
|
130
|
+
Some(SignalVote {
|
|
131
|
+
strength,
|
|
132
|
+
weight: 0.6,
|
|
133
|
+
reason,
|
|
134
|
+
})
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/// ADX trend strength — confirms or weakens signals.
|
|
139
|
+
pub struct AdxTrendRule;
|
|
140
|
+
|
|
141
|
+
impl SignalRule for AdxTrendRule {
|
|
142
|
+
fn evaluate(&self, snapshot: &IndicatorValues) -> Option<SignalVote> {
|
|
143
|
+
let adx_val = snapshot.adx?;
|
|
144
|
+
let (strength, reason) = if adx_val > 40.0 {
|
|
145
|
+
(
|
|
146
|
+
SignalStrength::StrongBuy,
|
|
147
|
+
format!("ADX {adx_val:.1} — very strong trend"),
|
|
148
|
+
)
|
|
149
|
+
} else if adx_val > 25.0 {
|
|
150
|
+
(
|
|
151
|
+
SignalStrength::Neutral,
|
|
152
|
+
format!("ADX {adx_val:.1} — trending (confirms direction)"),
|
|
153
|
+
)
|
|
154
|
+
} else if adx_val > 20.0 {
|
|
155
|
+
(
|
|
156
|
+
SignalStrength::Neutral,
|
|
157
|
+
format!("ADX {adx_val:.1} — weak trend"),
|
|
158
|
+
)
|
|
159
|
+
} else {
|
|
160
|
+
(
|
|
161
|
+
SignalStrength::Neutral,
|
|
162
|
+
format!("ADX {adx_val:.1} — no trend, range-bound"),
|
|
163
|
+
)
|
|
164
|
+
};
|
|
165
|
+
Some(SignalVote {
|
|
166
|
+
strength,
|
|
167
|
+
weight: 0.5,
|
|
168
|
+
reason,
|
|
169
|
+
})
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/// Stochastic Oscillator rule.
|
|
174
|
+
pub struct StochasticRule;
|
|
175
|
+
|
|
176
|
+
impl SignalRule for StochasticRule {
|
|
177
|
+
fn evaluate(&self, snapshot: &IndicatorValues) -> Option<SignalVote> {
|
|
178
|
+
let stoch = snapshot.stochastic.as_ref()?;
|
|
179
|
+
let (strength, reason) = if stoch.k < 20.0 && stoch.d < 20.0 {
|
|
180
|
+
(
|
|
181
|
+
SignalStrength::StrongBuy,
|
|
182
|
+
format!(
|
|
183
|
+
"Stochastic K={:.1} D={:.1} — deeply oversold",
|
|
184
|
+
stoch.k, stoch.d
|
|
185
|
+
),
|
|
186
|
+
)
|
|
187
|
+
} else if stoch.k < 30.0 {
|
|
188
|
+
(
|
|
189
|
+
SignalStrength::Buy,
|
|
190
|
+
format!("Stochastic K={:.1} — oversold zone", stoch.k),
|
|
191
|
+
)
|
|
192
|
+
} else if stoch.k > 80.0 && stoch.d > 80.0 {
|
|
193
|
+
(
|
|
194
|
+
SignalStrength::StrongSell,
|
|
195
|
+
format!(
|
|
196
|
+
"Stochastic K={:.1} D={:.1} — deeply overbought",
|
|
197
|
+
stoch.k, stoch.d
|
|
198
|
+
),
|
|
199
|
+
)
|
|
200
|
+
} else if stoch.k > 70.0 {
|
|
201
|
+
(
|
|
202
|
+
SignalStrength::Sell,
|
|
203
|
+
format!("Stochastic K={:.1} — overbought zone", stoch.k),
|
|
204
|
+
)
|
|
205
|
+
} else {
|
|
206
|
+
(
|
|
207
|
+
SignalStrength::Neutral,
|
|
208
|
+
format!("Stochastic K={:.1} — neutral zone", stoch.k),
|
|
209
|
+
)
|
|
210
|
+
};
|
|
211
|
+
Some(SignalVote {
|
|
212
|
+
strength,
|
|
213
|
+
weight: 0.7,
|
|
214
|
+
reason,
|
|
215
|
+
})
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
#[cfg(test)]
|
|
220
|
+
mod tests {
|
|
221
|
+
use super::*;
|
|
222
|
+
use crate::indicators::momentum::macd::MacdResult;
|
|
223
|
+
use crate::indicators::momentum::stochastic::StochasticResult;
|
|
224
|
+
use crate::indicators::trend::supertrend::SupertrendResult;
|
|
225
|
+
|
|
226
|
+
#[test]
|
|
227
|
+
fn rsi_oversold() {
|
|
228
|
+
let vals = IndicatorValues {
|
|
229
|
+
rsi: Some(25.0),
|
|
230
|
+
..Default::default()
|
|
231
|
+
};
|
|
232
|
+
let vote = RsiRule.evaluate(&vals).unwrap();
|
|
233
|
+
assert_eq!(vote.strength, SignalStrength::Buy);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
#[test]
|
|
237
|
+
fn rsi_overbought() {
|
|
238
|
+
let vals = IndicatorValues {
|
|
239
|
+
rsi: Some(75.0),
|
|
240
|
+
..Default::default()
|
|
241
|
+
};
|
|
242
|
+
let vote = RsiRule.evaluate(&vals).unwrap();
|
|
243
|
+
assert_eq!(vote.strength, SignalStrength::Sell);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
#[test]
|
|
247
|
+
fn rsi_deeply_oversold() {
|
|
248
|
+
let vals = IndicatorValues {
|
|
249
|
+
rsi: Some(15.0),
|
|
250
|
+
..Default::default()
|
|
251
|
+
};
|
|
252
|
+
let vote = RsiRule.evaluate(&vals).unwrap();
|
|
253
|
+
assert_eq!(vote.strength, SignalStrength::StrongBuy);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
#[test]
|
|
257
|
+
fn macd_bullish_crossover() {
|
|
258
|
+
let vals = IndicatorValues {
|
|
259
|
+
macd: Some(MacdResult {
|
|
260
|
+
value: 1.0,
|
|
261
|
+
signal: 0.5,
|
|
262
|
+
histogram: 0.5,
|
|
263
|
+
crossover: Crossover::Bullish,
|
|
264
|
+
}),
|
|
265
|
+
..Default::default()
|
|
266
|
+
};
|
|
267
|
+
let vote = MacdCrossoverRule.evaluate(&vals).unwrap();
|
|
268
|
+
assert_eq!(vote.strength, SignalStrength::Buy);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
#[test]
|
|
272
|
+
fn supertrend_up() {
|
|
273
|
+
let vals = IndicatorValues {
|
|
274
|
+
supertrend: Some(SupertrendResult {
|
|
275
|
+
value: 100.0,
|
|
276
|
+
direction: SupertrendDirection::Up,
|
|
277
|
+
}),
|
|
278
|
+
..Default::default()
|
|
279
|
+
};
|
|
280
|
+
let vote = SupertrendRule.evaluate(&vals).unwrap();
|
|
281
|
+
assert_eq!(vote.strength, SignalStrength::Buy);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
#[test]
|
|
285
|
+
fn volume_surging() {
|
|
286
|
+
let vals = IndicatorValues {
|
|
287
|
+
volume_trend: "surging".to_string(),
|
|
288
|
+
..Default::default()
|
|
289
|
+
};
|
|
290
|
+
let vote = VolumeTrendRule.evaluate(&vals).unwrap();
|
|
291
|
+
assert_eq!(vote.strength, SignalStrength::Buy);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
#[test]
|
|
295
|
+
fn adx_strong() {
|
|
296
|
+
let vals = IndicatorValues {
|
|
297
|
+
adx: Some(45.0),
|
|
298
|
+
..Default::default()
|
|
299
|
+
};
|
|
300
|
+
let vote = AdxTrendRule.evaluate(&vals).unwrap();
|
|
301
|
+
assert_eq!(vote.strength, SignalStrength::StrongBuy);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
#[test]
|
|
305
|
+
fn stochastic_oversold() {
|
|
306
|
+
let vals = IndicatorValues {
|
|
307
|
+
stochastic: Some(StochasticResult { k: 15.0, d: 15.0 }),
|
|
308
|
+
..Default::default()
|
|
309
|
+
};
|
|
310
|
+
let vote = StochasticRule.evaluate(&vals).unwrap();
|
|
311
|
+
assert_eq!(vote.strength, SignalStrength::StrongBuy);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
#[test]
|
|
315
|
+
fn stochastic_overbought() {
|
|
316
|
+
let vals = IndicatorValues {
|
|
317
|
+
stochastic: Some(StochasticResult { k: 85.0, d: 85.0 }),
|
|
318
|
+
..Default::default()
|
|
319
|
+
};
|
|
320
|
+
let vote = StochasticRule.evaluate(&vals).unwrap();
|
|
321
|
+
assert_eq!(vote.strength, SignalStrength::StrongSell);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
#[test]
|
|
325
|
+
fn missing_data_returns_none() {
|
|
326
|
+
let vals = IndicatorValues::default();
|
|
327
|
+
assert!(RsiRule.evaluate(&vals).is_none());
|
|
328
|
+
assert!(MacdCrossoverRule.evaluate(&vals).is_none());
|
|
329
|
+
assert!(SupertrendRule.evaluate(&vals).is_none());
|
|
330
|
+
assert!(AdxTrendRule.evaluate(&vals).is_none());
|
|
331
|
+
assert!(StochasticRule.evaluate(&vals).is_none());
|
|
332
|
+
}
|
|
333
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/// Signal strength classification.
|
|
2
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
3
|
+
pub enum SignalStrength {
|
|
4
|
+
StrongBuy,
|
|
5
|
+
Buy,
|
|
6
|
+
Neutral,
|
|
7
|
+
Sell,
|
|
8
|
+
StrongSell,
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
impl SignalStrength {
|
|
12
|
+
/// Numeric score: StrongBuy = 2, Buy = 1, Neutral = 0, Sell = -1, StrongSell = -2.
|
|
13
|
+
pub fn score(self) -> f64 {
|
|
14
|
+
match self {
|
|
15
|
+
Self::StrongBuy => 2.0,
|
|
16
|
+
Self::Buy => 1.0,
|
|
17
|
+
Self::Neutral => 0.0,
|
|
18
|
+
Self::Sell => -1.0,
|
|
19
|
+
Self::StrongSell => -2.0,
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
impl std::fmt::Display for SignalStrength {
|
|
25
|
+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
26
|
+
match self {
|
|
27
|
+
Self::StrongBuy => write!(f, "strong_buy"),
|
|
28
|
+
Self::Buy => write!(f, "buy"),
|
|
29
|
+
Self::Neutral => write!(f, "neutral"),
|
|
30
|
+
Self::Sell => write!(f, "sell"),
|
|
31
|
+
Self::StrongSell => write!(f, "strong_sell"),
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/// Composite signal after aggregating all rule votes.
|
|
37
|
+
#[derive(Debug, Clone)]
|
|
38
|
+
pub struct Signal {
|
|
39
|
+
pub strength: SignalStrength,
|
|
40
|
+
pub confidence: f64,
|
|
41
|
+
pub reasons: Vec<String>,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/// A single vote from one signal rule.
|
|
45
|
+
#[derive(Debug, Clone)]
|
|
46
|
+
pub struct SignalVote {
|
|
47
|
+
pub strength: SignalStrength,
|
|
48
|
+
pub weight: f64,
|
|
49
|
+
pub reason: String,
|
|
50
|
+
}
|
package/index.d.ts
DELETED
|
@@ -1,66 +0,0 @@
|
|
|
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
DELETED
|
Binary file
|
package/src/atr.rs
DELETED
|
@@ -1,66 +0,0 @@
|
|
|
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
|
-
}
|
package/src/batch.rs
DELETED
|
@@ -1,139 +0,0 @@
|
|
|
1
|
-
use crate::*;
|
|
2
|
-
use rayon::prelude::*;
|
|
3
|
-
|
|
4
|
-
/// Input data for a single stock.
|
|
5
|
-
#[derive(Debug, Clone)]
|
|
6
|
-
pub struct StockData {
|
|
7
|
-
pub symbol: String,
|
|
8
|
-
pub closes: Vec<f64>,
|
|
9
|
-
pub highs: Vec<f64>,
|
|
10
|
-
pub lows: Vec<f64>,
|
|
11
|
-
pub volumes: Vec<f64>,
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/// All indicators computed for a single stock.
|
|
15
|
-
#[derive(Debug, Clone)]
|
|
16
|
-
pub struct IndicatorSnapshot {
|
|
17
|
-
pub symbol: String,
|
|
18
|
-
pub sma_20: Option<f64>,
|
|
19
|
-
pub sma_50: Option<f64>,
|
|
20
|
-
pub sma_200: Option<f64>,
|
|
21
|
-
pub ema_20: Option<f64>,
|
|
22
|
-
pub rsi_14: Option<f64>,
|
|
23
|
-
pub macd_result: Option<MacdResult>,
|
|
24
|
-
pub bollinger: Option<BollingerBandsResult>,
|
|
25
|
-
pub atr_14: Option<f64>,
|
|
26
|
-
pub volume_trend: String,
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/// Compute all indicators for a single stock.
|
|
30
|
-
pub fn compute_indicators(stock: &StockData) -> IndicatorSnapshot {
|
|
31
|
-
IndicatorSnapshot {
|
|
32
|
-
symbol: stock.symbol.clone(),
|
|
33
|
-
sma_20: sma(&stock.closes, 20),
|
|
34
|
-
sma_50: sma(&stock.closes, 50),
|
|
35
|
-
sma_200: sma(&stock.closes, 200),
|
|
36
|
-
ema_20: ema(&stock.closes, 20),
|
|
37
|
-
rsi_14: rsi(&stock.closes, 14),
|
|
38
|
-
macd_result: macd(&stock.closes, 12, 26, 9),
|
|
39
|
-
bollinger: bollinger_bands(&stock.closes, 20, 2.0),
|
|
40
|
-
atr_14: atr(&stock.highs, &stock.lows, &stock.closes, 14),
|
|
41
|
-
volume_trend: volume_trend(&stock.volumes).to_string(),
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/// Compute indicators for multiple stocks sequentially.
|
|
46
|
-
pub fn batch_compute(stocks: &[StockData]) -> Vec<IndicatorSnapshot> {
|
|
47
|
-
stocks.iter().map(compute_indicators).collect()
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/// Compute indicators for multiple stocks in parallel using Rayon.
|
|
51
|
-
/// Automatically distributes work across all CPU cores.
|
|
52
|
-
pub fn batch_compute_parallel(stocks: &[StockData]) -> Vec<IndicatorSnapshot> {
|
|
53
|
-
stocks.par_iter().map(compute_indicators).collect()
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
#[cfg(test)]
|
|
57
|
-
mod tests {
|
|
58
|
-
use super::*;
|
|
59
|
-
|
|
60
|
-
fn make_stock(symbol: &str, days: usize) -> StockData {
|
|
61
|
-
let closes: Vec<f64> = (0..days).map(|i| 100.0 + (i as f64 * 0.5)).collect();
|
|
62
|
-
let highs: Vec<f64> = closes.iter().map(|c| c + 2.0).collect();
|
|
63
|
-
let lows: Vec<f64> = closes.iter().map(|c| c - 2.0).collect();
|
|
64
|
-
let volumes: Vec<f64> = vec![1_000_000.0; days];
|
|
65
|
-
StockData {
|
|
66
|
-
symbol: symbol.to_string(),
|
|
67
|
-
closes,
|
|
68
|
-
highs,
|
|
69
|
-
lows,
|
|
70
|
-
volumes,
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
#[test]
|
|
75
|
-
fn single_stock() {
|
|
76
|
-
let stock = make_stock("RELIANCE", 250);
|
|
77
|
-
let result = compute_indicators(&stock);
|
|
78
|
-
assert_eq!(result.symbol, "RELIANCE");
|
|
79
|
-
assert!(result.sma_20.is_some());
|
|
80
|
-
assert!(result.sma_50.is_some());
|
|
81
|
-
assert!(result.sma_200.is_some());
|
|
82
|
-
assert!(result.rsi_14.is_some());
|
|
83
|
-
assert!(result.macd_result.is_some());
|
|
84
|
-
assert!(result.bollinger.is_some());
|
|
85
|
-
assert!(result.atr_14.is_some());
|
|
86
|
-
assert_eq!(result.volume_trend, "stable");
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
#[test]
|
|
90
|
-
fn batch_multiple() {
|
|
91
|
-
let stocks: Vec<StockData> = ["RELIANCE", "TCS", "INFY", "SBIN", "HDFCBANK"]
|
|
92
|
-
.iter()
|
|
93
|
-
.map(|s| make_stock(s, 250))
|
|
94
|
-
.collect();
|
|
95
|
-
|
|
96
|
-
let results = batch_compute(&stocks);
|
|
97
|
-
assert_eq!(results.len(), 5);
|
|
98
|
-
assert_eq!(results[0].symbol, "RELIANCE");
|
|
99
|
-
assert_eq!(results[4].symbol, "HDFCBANK");
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
#[test]
|
|
103
|
-
fn batch_2000_stocks() {
|
|
104
|
-
let stocks: Vec<StockData> = (0..2000)
|
|
105
|
-
.map(|i| make_stock(&format!("STOCK{}", i), 250))
|
|
106
|
-
.collect();
|
|
107
|
-
|
|
108
|
-
let start = std::time::Instant::now();
|
|
109
|
-
let results = batch_compute(&stocks);
|
|
110
|
-
let elapsed = start.elapsed();
|
|
111
|
-
|
|
112
|
-
assert_eq!(results.len(), 2000);
|
|
113
|
-
println!("2000 stocks sequential: {:?}", elapsed);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
#[test]
|
|
117
|
-
fn batch_2000_parallel() {
|
|
118
|
-
let stocks: Vec<StockData> = (0..2000)
|
|
119
|
-
.map(|i| make_stock(&format!("STOCK{}", i), 250))
|
|
120
|
-
.collect();
|
|
121
|
-
|
|
122
|
-
let start = std::time::Instant::now();
|
|
123
|
-
let results = batch_compute_parallel(&stocks);
|
|
124
|
-
let elapsed = start.elapsed();
|
|
125
|
-
|
|
126
|
-
assert_eq!(results.len(), 2000);
|
|
127
|
-
println!("2000 stocks parallel: {:?}", elapsed);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
#[test]
|
|
131
|
-
fn insufficient_data_stock() {
|
|
132
|
-
let stock = make_stock("NEWIPO", 10);
|
|
133
|
-
let result = compute_indicators(&stock);
|
|
134
|
-
assert_eq!(result.symbol, "NEWIPO");
|
|
135
|
-
assert!(result.sma_20.is_none());
|
|
136
|
-
assert!(result.rsi_14.is_none());
|
|
137
|
-
assert!(result.macd_result.is_none());
|
|
138
|
-
}
|
|
139
|
-
}
|