@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,44 @@
|
|
|
1
|
+
/// On-Balance Volume.
|
|
2
|
+
/// Running total: +volume on up days, -volume on down days.
|
|
3
|
+
#[must_use]
|
|
4
|
+
pub fn obv(closes: &[f64], volumes: &[f64]) -> Option<f64> {
|
|
5
|
+
let len = closes.len();
|
|
6
|
+
if len < 2 || volumes.len() < len {
|
|
7
|
+
return None;
|
|
8
|
+
}
|
|
9
|
+
let mut value = 0.0;
|
|
10
|
+
for i in 1..len {
|
|
11
|
+
if closes[i] > closes[i - 1] {
|
|
12
|
+
value += volumes[i];
|
|
13
|
+
} else if closes[i] < closes[i - 1] {
|
|
14
|
+
value -= volumes[i];
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
Some(value)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
#[cfg(test)]
|
|
21
|
+
mod tests {
|
|
22
|
+
use super::*;
|
|
23
|
+
|
|
24
|
+
#[test]
|
|
25
|
+
fn obv_up_trend() {
|
|
26
|
+
let closes = vec![10.0, 11.0, 12.0, 13.0, 14.0];
|
|
27
|
+
let volumes = vec![100.0, 200.0, 300.0, 400.0, 500.0];
|
|
28
|
+
let result = obv(&closes, &volumes).unwrap();
|
|
29
|
+
assert_eq!(result, 1400.0); // 200+300+400+500
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
#[test]
|
|
33
|
+
fn obv_mixed() {
|
|
34
|
+
let closes = vec![10.0, 12.0, 11.0, 13.0];
|
|
35
|
+
let volumes = vec![100.0, 200.0, 300.0, 400.0];
|
|
36
|
+
let result = obv(&closes, &volumes).unwrap();
|
|
37
|
+
assert_eq!(result, 300.0); // +200 -300 +400
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
#[test]
|
|
41
|
+
fn obv_insufficient() {
|
|
42
|
+
assert!(obv(&[10.0], &[100.0]).is_none());
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -1,20 +1,15 @@
|
|
|
1
1
|
/// Classify volume trend by comparing recent 5-day avg to 20-day avg.
|
|
2
|
-
|
|
3
|
-
/// or "insufficient data".
|
|
2
|
+
#[must_use]
|
|
4
3
|
pub fn volume_trend(volumes: &[f64]) -> &'static str {
|
|
5
4
|
if volumes.len() < 20 {
|
|
6
5
|
return "insufficient data";
|
|
7
6
|
}
|
|
8
|
-
|
|
9
7
|
let avg5: f64 = volumes[volumes.len() - 5..].iter().sum::<f64>() / 5.0;
|
|
10
8
|
let avg20: f64 = volumes[volumes.len() - 20..].iter().sum::<f64>() / 20.0;
|
|
11
|
-
|
|
12
|
-
if avg20 == 0.0 {
|
|
9
|
+
if avg20 < f64::EPSILON {
|
|
13
10
|
return "insufficient data";
|
|
14
11
|
}
|
|
15
|
-
|
|
16
12
|
let ratio = avg5 / avg20;
|
|
17
|
-
|
|
18
13
|
if ratio > 1.5 {
|
|
19
14
|
"surging"
|
|
20
15
|
} else if ratio > 1.1 {
|
|
@@ -33,32 +28,19 @@ mod tests {
|
|
|
33
28
|
use super::*;
|
|
34
29
|
|
|
35
30
|
#[test]
|
|
36
|
-
fn
|
|
37
|
-
|
|
38
|
-
assert_eq!(volume_trend(&volumes), "stable");
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
#[test]
|
|
42
|
-
fn volume_surging() {
|
|
43
|
-
let mut volumes = vec![100.0; 20];
|
|
44
|
-
// Last 5 are much higher
|
|
45
|
-
for _ in 0..5 {
|
|
46
|
-
volumes.push(500.0);
|
|
47
|
-
}
|
|
48
|
-
assert_eq!(volume_trend(&volumes), "surging");
|
|
31
|
+
fn stable() {
|
|
32
|
+
assert_eq!(volume_trend(&vec![1000.0; 25]), "stable");
|
|
49
33
|
}
|
|
50
34
|
|
|
51
35
|
#[test]
|
|
52
|
-
fn
|
|
53
|
-
let mut
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}
|
|
57
|
-
assert_eq!(volume_trend(&volumes), "drying up");
|
|
36
|
+
fn surging() {
|
|
37
|
+
let mut v = vec![100.0; 20];
|
|
38
|
+
v.extend_from_slice(&[500.0; 5]);
|
|
39
|
+
assert_eq!(volume_trend(&v), "surging");
|
|
58
40
|
}
|
|
59
41
|
|
|
60
42
|
#[test]
|
|
61
|
-
fn
|
|
43
|
+
fn insufficient() {
|
|
62
44
|
assert_eq!(volume_trend(&[100.0; 10]), "insufficient data");
|
|
63
45
|
}
|
|
64
46
|
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
use crate::core::utils::round;
|
|
2
|
+
|
|
3
|
+
/// Volume Weighted Average Price.
|
|
4
|
+
/// VWAP = sum(typical_price * volume) / sum(volume)
|
|
5
|
+
/// where typical_price = (high + low + close) / 3
|
|
6
|
+
#[must_use]
|
|
7
|
+
pub fn vwap(highs: &[f64], lows: &[f64], closes: &[f64], volumes: &[f64]) -> Option<f64> {
|
|
8
|
+
let len = closes.len();
|
|
9
|
+
if len == 0 || highs.len() < len || lows.len() < len || volumes.len() < len {
|
|
10
|
+
return None;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let mut cum_tp_vol = 0.0;
|
|
14
|
+
let mut cum_vol = 0.0;
|
|
15
|
+
|
|
16
|
+
for i in 0..len {
|
|
17
|
+
let typical_price = (highs[i] + lows[i] + closes[i]) / 3.0;
|
|
18
|
+
cum_tp_vol += typical_price * volumes[i];
|
|
19
|
+
cum_vol += volumes[i];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if cum_vol < f64::EPSILON {
|
|
23
|
+
return None;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
Some(round(cum_tp_vol / cum_vol, 2))
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
#[cfg(test)]
|
|
30
|
+
mod tests {
|
|
31
|
+
use super::*;
|
|
32
|
+
|
|
33
|
+
#[test]
|
|
34
|
+
fn vwap_basic() {
|
|
35
|
+
let highs = vec![12.0, 13.0, 14.0];
|
|
36
|
+
let lows = vec![10.0, 11.0, 12.0];
|
|
37
|
+
let closes = vec![11.0, 12.0, 13.0];
|
|
38
|
+
let volumes = vec![1000.0, 2000.0, 3000.0];
|
|
39
|
+
let result = vwap(&highs, &lows, &closes, &volumes).unwrap();
|
|
40
|
+
assert!(result > 11.0 && result < 14.0);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
#[test]
|
|
44
|
+
fn vwap_zero_volume() {
|
|
45
|
+
assert!(vwap(&[10.0], &[10.0], &[10.0], &[0.0]).is_none());
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
#[test]
|
|
49
|
+
fn vwap_empty() {
|
|
50
|
+
let empty: Vec<f64> = vec![];
|
|
51
|
+
assert!(vwap(&empty, &empty, &empty, &empty).is_none());
|
|
52
|
+
}
|
|
53
|
+
}
|
package/src/lib.rs
CHANGED
|
@@ -1,25 +1,75 @@
|
|
|
1
1
|
//! # indica
|
|
2
2
|
//!
|
|
3
3
|
//! Fast technical analysis indicators for stock markets.
|
|
4
|
-
//!
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
4
|
+
//! Built for Indian markets. Screening to signals.
|
|
5
|
+
//!
|
|
6
|
+
//! ## Indicators
|
|
7
|
+
//!
|
|
8
|
+
//! **Trend:** SMA, EMA, Supertrend, ADX
|
|
9
|
+
//! **Momentum:** RSI, MACD, Stochastic
|
|
10
|
+
//! **Volatility:** Bollinger Bands, ATR
|
|
11
|
+
//! **Volume:** OBV, VWAP, Volume Trend
|
|
12
|
+
//! **Support/Resistance:** Pivot Points
|
|
13
|
+
//! **India-Specific:** Delivery Analysis, Circuit Limits
|
|
14
|
+
//!
|
|
15
|
+
//! ## Usage
|
|
16
|
+
//!
|
|
17
|
+
//! ```rust
|
|
18
|
+
//! use indica::{sma, rsi, supertrend};
|
|
19
|
+
//! ```
|
|
20
|
+
|
|
15
21
|
pub mod batch;
|
|
16
|
-
mod
|
|
17
|
-
|
|
18
|
-
pub
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
pub use
|
|
24
|
-
pub use
|
|
25
|
-
pub use
|
|
22
|
+
pub mod core;
|
|
23
|
+
pub mod indicators;
|
|
24
|
+
pub mod signals;
|
|
25
|
+
|
|
26
|
+
// ── Backward-compatible convenience re-exports ──
|
|
27
|
+
|
|
28
|
+
// Trend
|
|
29
|
+
pub use indicators::trend::adx::adx;
|
|
30
|
+
pub use indicators::trend::ema::ema;
|
|
31
|
+
pub use indicators::trend::sma::sma;
|
|
32
|
+
pub use indicators::trend::supertrend::{SupertrendDirection, SupertrendResult, supertrend};
|
|
33
|
+
|
|
34
|
+
// Momentum
|
|
35
|
+
pub use indicators::momentum::macd::{Crossover, MacdResult, macd};
|
|
36
|
+
pub use indicators::momentum::rsi::rsi;
|
|
37
|
+
pub use indicators::momentum::stochastic::{StochasticResult, stochastic};
|
|
38
|
+
|
|
39
|
+
// Volatility
|
|
40
|
+
pub use indicators::volatility::atr::atr;
|
|
41
|
+
pub use indicators::volatility::bollinger::{BollingerBandsResult, bollinger_bands};
|
|
42
|
+
|
|
43
|
+
// Volume
|
|
44
|
+
pub use indicators::volume::obv::obv;
|
|
45
|
+
pub use indicators::volume::volume_trend::volume_trend;
|
|
46
|
+
pub use indicators::volume::vwap::vwap;
|
|
47
|
+
|
|
48
|
+
// Support/Resistance
|
|
49
|
+
pub use indicators::support_resistance::pivot::{PivotPointsResult, pivot_points};
|
|
50
|
+
|
|
51
|
+
// India-specific
|
|
52
|
+
pub use indicators::india::circuit::{CircuitLimit, CircuitStatus, circuit_proximity};
|
|
53
|
+
pub use indicators::india::delivery::{DeliveryTrend, delivery_pct, delivery_trend};
|
|
54
|
+
|
|
55
|
+
// Core types
|
|
56
|
+
pub use core::traits::Indicator;
|
|
57
|
+
pub use core::types::Candle;
|
|
58
|
+
|
|
59
|
+
// Streaming indicator structs
|
|
60
|
+
pub use indicators::momentum::rsi::Rsi;
|
|
61
|
+
pub use indicators::trend::adx::Adx;
|
|
62
|
+
pub use indicators::trend::ema::Ema;
|
|
63
|
+
pub use indicators::trend::sma::Sma;
|
|
64
|
+
pub use indicators::trend::supertrend::Supertrend;
|
|
65
|
+
|
|
66
|
+
// Signals
|
|
67
|
+
pub use signals::engine::{IndicatorValues, SignalEngine, SignalRule};
|
|
68
|
+
pub use signals::presets::{momentum_trader, swing_trader};
|
|
69
|
+
pub use signals::types::{Signal, SignalStrength, SignalVote};
|
|
70
|
+
|
|
71
|
+
// Batch processing
|
|
72
|
+
pub use batch::compute::{
|
|
73
|
+
IndicatorSnapshot, StockData, batch_compute, batch_compute_parallel, compute_snapshot,
|
|
74
|
+
};
|
|
75
|
+
pub use batch::screen::{ScreenFilter, ScreenResult, screen, screen_precomputed};
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
use crate::indicators::india::delivery::DeliveryTrend;
|
|
2
|
+
use crate::indicators::momentum::macd::MacdResult;
|
|
3
|
+
use crate::indicators::momentum::stochastic::StochasticResult;
|
|
4
|
+
use crate::indicators::trend::supertrend::SupertrendResult;
|
|
5
|
+
use crate::indicators::volatility::bollinger::BollingerBandsResult;
|
|
6
|
+
|
|
7
|
+
use super::types::{Signal, SignalStrength, SignalVote};
|
|
8
|
+
|
|
9
|
+
/// Snapshot of all computed indicator values for a single stock.
|
|
10
|
+
#[derive(Debug, Clone, Default)]
|
|
11
|
+
pub struct IndicatorValues {
|
|
12
|
+
pub rsi: Option<f64>,
|
|
13
|
+
pub macd: Option<MacdResult>,
|
|
14
|
+
pub supertrend: Option<SupertrendResult>,
|
|
15
|
+
pub bollinger: Option<BollingerBandsResult>,
|
|
16
|
+
pub volume_trend: String,
|
|
17
|
+
pub adx: Option<f64>,
|
|
18
|
+
pub stochastic: Option<StochasticResult>,
|
|
19
|
+
pub delivery_trend: Option<DeliveryTrend>,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/// Trait for individual signal rules.
|
|
23
|
+
/// Each rule inspects indicator values and optionally casts a vote.
|
|
24
|
+
pub trait SignalRule: Send + Sync {
|
|
25
|
+
fn evaluate(&self, snapshot: &IndicatorValues) -> Option<SignalVote>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/// Engine that collects votes from multiple rules and produces a composite signal.
|
|
29
|
+
pub struct SignalEngine {
|
|
30
|
+
rules: Vec<Box<dyn SignalRule>>,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
impl SignalEngine {
|
|
34
|
+
pub fn new() -> Self {
|
|
35
|
+
Self { rules: Vec::new() }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/// Add a rule to the engine.
|
|
39
|
+
pub fn add_rule(&mut self, rule: Box<dyn SignalRule>) {
|
|
40
|
+
self.rules.push(rule);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/// Evaluate all rules against the given indicator values.
|
|
44
|
+
pub fn evaluate(&self, values: &IndicatorValues) -> Signal {
|
|
45
|
+
let votes: Vec<SignalVote> = self
|
|
46
|
+
.rules
|
|
47
|
+
.iter()
|
|
48
|
+
.filter_map(|rule| rule.evaluate(values))
|
|
49
|
+
.collect();
|
|
50
|
+
|
|
51
|
+
if votes.is_empty() {
|
|
52
|
+
return Signal {
|
|
53
|
+
strength: SignalStrength::Neutral,
|
|
54
|
+
confidence: 0.0,
|
|
55
|
+
reasons: vec!["No indicator data available".to_string()],
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
let total_weight: f64 = votes.iter().map(|v| v.weight).sum();
|
|
60
|
+
if total_weight < f64::EPSILON {
|
|
61
|
+
return Signal {
|
|
62
|
+
strength: SignalStrength::Neutral,
|
|
63
|
+
confidence: 0.0,
|
|
64
|
+
reasons: vec!["All weights are zero".to_string()],
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Weighted average score
|
|
69
|
+
let weighted_score: f64 = votes
|
|
70
|
+
.iter()
|
|
71
|
+
.map(|v| v.strength.score() * v.weight)
|
|
72
|
+
.sum::<f64>()
|
|
73
|
+
/ total_weight;
|
|
74
|
+
|
|
75
|
+
let strength = if weighted_score >= 1.5 {
|
|
76
|
+
SignalStrength::StrongBuy
|
|
77
|
+
} else if weighted_score >= 0.5 {
|
|
78
|
+
SignalStrength::Buy
|
|
79
|
+
} else if weighted_score > -0.5 {
|
|
80
|
+
SignalStrength::Neutral
|
|
81
|
+
} else if weighted_score > -1.5 {
|
|
82
|
+
SignalStrength::Sell
|
|
83
|
+
} else {
|
|
84
|
+
SignalStrength::StrongSell
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
// Confidence = how much the votes agree (0..1)
|
|
88
|
+
// Perfect agreement = 1.0, evenly split = 0.0
|
|
89
|
+
let max_possible = 2.0; // max absolute score
|
|
90
|
+
let confidence = (weighted_score.abs() / max_possible).min(1.0);
|
|
91
|
+
|
|
92
|
+
let reasons: Vec<String> = votes.iter().map(|v| v.reason.clone()).collect();
|
|
93
|
+
|
|
94
|
+
Signal {
|
|
95
|
+
strength,
|
|
96
|
+
confidence,
|
|
97
|
+
reasons,
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
impl Default for SignalEngine {
|
|
103
|
+
fn default() -> Self {
|
|
104
|
+
Self::new()
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
#[cfg(test)]
|
|
109
|
+
mod tests {
|
|
110
|
+
use super::*;
|
|
111
|
+
|
|
112
|
+
struct AlwaysBuy;
|
|
113
|
+
impl SignalRule for AlwaysBuy {
|
|
114
|
+
fn evaluate(&self, _snapshot: &IndicatorValues) -> Option<SignalVote> {
|
|
115
|
+
Some(SignalVote {
|
|
116
|
+
strength: SignalStrength::Buy,
|
|
117
|
+
weight: 1.0,
|
|
118
|
+
reason: "Always buy".to_string(),
|
|
119
|
+
})
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
#[test]
|
|
124
|
+
fn engine_single_rule() {
|
|
125
|
+
let mut engine = SignalEngine::new();
|
|
126
|
+
engine.add_rule(Box::new(AlwaysBuy));
|
|
127
|
+
let signal = engine.evaluate(&IndicatorValues::default());
|
|
128
|
+
assert_eq!(signal.strength, SignalStrength::Buy);
|
|
129
|
+
assert!(!signal.reasons.is_empty());
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
#[test]
|
|
133
|
+
fn engine_no_rules() {
|
|
134
|
+
let engine = SignalEngine::new();
|
|
135
|
+
let signal = engine.evaluate(&IndicatorValues::default());
|
|
136
|
+
assert_eq!(signal.strength, SignalStrength::Neutral);
|
|
137
|
+
assert_eq!(signal.confidence, 0.0);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
use super::engine::SignalEngine;
|
|
2
|
+
use super::rules::{
|
|
3
|
+
AdxTrendRule, MacdCrossoverRule, RsiRule, StochasticRule, SupertrendRule, VolumeTrendRule,
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
/// Swing trader strategy: RSI + MACD + Supertrend + Volume.
|
|
7
|
+
/// Best for multi-day holds on NSE/BSE.
|
|
8
|
+
pub fn swing_trader() -> SignalEngine {
|
|
9
|
+
let mut engine = SignalEngine::new();
|
|
10
|
+
engine.add_rule(Box::new(RsiRule));
|
|
11
|
+
engine.add_rule(Box::new(MacdCrossoverRule));
|
|
12
|
+
engine.add_rule(Box::new(SupertrendRule));
|
|
13
|
+
engine.add_rule(Box::new(VolumeTrendRule));
|
|
14
|
+
engine
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/// Momentum trader strategy: RSI + Stochastic + ADX + Volume.
|
|
18
|
+
/// Best for short-term momentum plays.
|
|
19
|
+
pub fn momentum_trader() -> SignalEngine {
|
|
20
|
+
let mut engine = SignalEngine::new();
|
|
21
|
+
engine.add_rule(Box::new(RsiRule));
|
|
22
|
+
engine.add_rule(Box::new(StochasticRule));
|
|
23
|
+
engine.add_rule(Box::new(AdxTrendRule));
|
|
24
|
+
engine.add_rule(Box::new(VolumeTrendRule));
|
|
25
|
+
engine
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
#[cfg(test)]
|
|
29
|
+
mod tests {
|
|
30
|
+
use super::*;
|
|
31
|
+
use crate::indicators::momentum::macd::{Crossover, MacdResult};
|
|
32
|
+
use crate::indicators::momentum::stochastic::StochasticResult;
|
|
33
|
+
use crate::indicators::trend::supertrend::{SupertrendDirection, SupertrendResult};
|
|
34
|
+
use crate::signals::engine::IndicatorValues;
|
|
35
|
+
use crate::signals::types::SignalStrength;
|
|
36
|
+
|
|
37
|
+
#[test]
|
|
38
|
+
fn swing_trader_bullish() {
|
|
39
|
+
let engine = swing_trader();
|
|
40
|
+
let vals = IndicatorValues {
|
|
41
|
+
rsi: Some(28.0),
|
|
42
|
+
macd: Some(MacdResult {
|
|
43
|
+
value: 1.0,
|
|
44
|
+
signal: 0.5,
|
|
45
|
+
histogram: 0.5,
|
|
46
|
+
crossover: Crossover::Bullish,
|
|
47
|
+
}),
|
|
48
|
+
supertrend: Some(SupertrendResult {
|
|
49
|
+
value: 95.0,
|
|
50
|
+
direction: SupertrendDirection::Up,
|
|
51
|
+
}),
|
|
52
|
+
volume_trend: "surging".to_string(),
|
|
53
|
+
..Default::default()
|
|
54
|
+
};
|
|
55
|
+
let signal = engine.evaluate(&vals);
|
|
56
|
+
assert_eq!(signal.strength, SignalStrength::Buy);
|
|
57
|
+
assert!(signal.confidence > 0.0);
|
|
58
|
+
assert_eq!(signal.reasons.len(), 4);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
#[test]
|
|
62
|
+
fn swing_trader_bearish() {
|
|
63
|
+
let engine = swing_trader();
|
|
64
|
+
let vals = IndicatorValues {
|
|
65
|
+
rsi: Some(75.0),
|
|
66
|
+
macd: Some(MacdResult {
|
|
67
|
+
value: -1.0,
|
|
68
|
+
signal: -0.5,
|
|
69
|
+
histogram: -0.5,
|
|
70
|
+
crossover: Crossover::Bearish,
|
|
71
|
+
}),
|
|
72
|
+
supertrend: Some(SupertrendResult {
|
|
73
|
+
value: 120.0,
|
|
74
|
+
direction: SupertrendDirection::Down,
|
|
75
|
+
}),
|
|
76
|
+
volume_trend: "declining".to_string(),
|
|
77
|
+
..Default::default()
|
|
78
|
+
};
|
|
79
|
+
let signal = engine.evaluate(&vals);
|
|
80
|
+
assert_eq!(signal.strength, SignalStrength::Sell);
|
|
81
|
+
assert!(signal.confidence > 0.0);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
#[test]
|
|
85
|
+
fn momentum_trader_bullish() {
|
|
86
|
+
let engine = momentum_trader();
|
|
87
|
+
let vals = IndicatorValues {
|
|
88
|
+
rsi: Some(25.0),
|
|
89
|
+
stochastic: Some(StochasticResult { k: 18.0, d: 15.0 }),
|
|
90
|
+
adx: Some(45.0),
|
|
91
|
+
volume_trend: "surging".to_string(),
|
|
92
|
+
..Default::default()
|
|
93
|
+
};
|
|
94
|
+
let signal = engine.evaluate(&vals);
|
|
95
|
+
assert!(
|
|
96
|
+
signal.strength == SignalStrength::Buy || signal.strength == SignalStrength::StrongBuy
|
|
97
|
+
);
|
|
98
|
+
assert!(signal.confidence > 0.0);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
#[test]
|
|
102
|
+
fn swing_trader_no_data() {
|
|
103
|
+
let engine = swing_trader();
|
|
104
|
+
let vals = IndicatorValues::default();
|
|
105
|
+
let signal = engine.evaluate(&vals);
|
|
106
|
+
// volume_trend defaults to "" which returns None from VolumeTrendRule
|
|
107
|
+
assert_eq!(signal.strength, SignalStrength::Neutral);
|
|
108
|
+
}
|
|
109
|
+
}
|