@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,43 @@
|
|
|
1
|
+
/// Round to N decimal places.
|
|
2
|
+
pub fn round(value: f64, decimals: u32) -> f64 {
|
|
3
|
+
let factor = 10_f64.powi(decimals as i32);
|
|
4
|
+
(value * factor).round() / factor
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/// Wilder's smoothing step: (prev * (period-1) + val) / period.
|
|
8
|
+
/// Used by RSI and ATR.
|
|
9
|
+
pub fn wilders_step(prev: f64, val: f64, period: usize) -> f64 {
|
|
10
|
+
(prev * (period as f64 - 1.0) + val) / period as f64
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/// EMA smoothing step: val * k + prev * (1 - k), where k = 2/(period+1).
|
|
14
|
+
pub fn ema_step(prev: f64, val: f64, k: f64) -> f64 {
|
|
15
|
+
val * k + prev * (1.0 - k)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/// Compute EMA smoothing factor for a given period.
|
|
19
|
+
pub fn ema_k(period: usize) -> f64 {
|
|
20
|
+
2.0 / (period as f64 + 1.0)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
#[cfg(test)]
|
|
24
|
+
mod tests {
|
|
25
|
+
use super::*;
|
|
26
|
+
|
|
27
|
+
#[test]
|
|
28
|
+
fn test_round() {
|
|
29
|
+
assert_eq!(round(1.23456, 2), 1.23);
|
|
30
|
+
assert_eq!(round(1.235, 2), 1.24);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
#[test]
|
|
34
|
+
fn test_wilders_step() {
|
|
35
|
+
assert_eq!(wilders_step(10.0, 12.0, 2), 11.0);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
#[test]
|
|
39
|
+
fn test_ema_step() {
|
|
40
|
+
let k = ema_k(3); // 0.5
|
|
41
|
+
assert_eq!(ema_step(10.0, 12.0, k), 11.0);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
use crate::core::utils::round;
|
|
2
|
+
|
|
3
|
+
/// Circuit limit bands for Indian stocks.
|
|
4
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
5
|
+
pub enum CircuitLimit {
|
|
6
|
+
Percent2,
|
|
7
|
+
Percent5,
|
|
8
|
+
Percent10,
|
|
9
|
+
Percent20,
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
impl CircuitLimit {
|
|
13
|
+
fn multiplier(&self) -> f64 {
|
|
14
|
+
match self {
|
|
15
|
+
Self::Percent2 => 0.02,
|
|
16
|
+
Self::Percent5 => 0.05,
|
|
17
|
+
Self::Percent10 => 0.10,
|
|
18
|
+
Self::Percent20 => 0.20,
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/// Circuit proximity status.
|
|
24
|
+
#[derive(Debug, Clone)]
|
|
25
|
+
pub struct CircuitStatus {
|
|
26
|
+
pub upper_limit: f64,
|
|
27
|
+
pub lower_limit: f64,
|
|
28
|
+
pub upper_distance_pct: f64, // How far from upper circuit (0 = at limit)
|
|
29
|
+
pub lower_distance_pct: f64, // How far from lower circuit
|
|
30
|
+
pub near_upper: bool, // Within 1% of upper circuit
|
|
31
|
+
pub near_lower: bool, // Within 1% of lower circuit
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/// Check how close a stock is to its circuit limits.
|
|
35
|
+
/// Indian stocks have daily price movement limits (2/5/10/20%).
|
|
36
|
+
#[must_use]
|
|
37
|
+
pub fn circuit_proximity(
|
|
38
|
+
current_price: f64,
|
|
39
|
+
prev_close: f64,
|
|
40
|
+
limit: CircuitLimit,
|
|
41
|
+
) -> CircuitStatus {
|
|
42
|
+
let mult = limit.multiplier();
|
|
43
|
+
let upper_limit = round(prev_close * (1.0 + mult), 2);
|
|
44
|
+
let lower_limit = round(prev_close * (1.0 - mult), 2);
|
|
45
|
+
|
|
46
|
+
let upper_distance_pct = if current_price < upper_limit {
|
|
47
|
+
round((upper_limit - current_price) / upper_limit * 100.0, 2)
|
|
48
|
+
} else {
|
|
49
|
+
0.0
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
let lower_distance_pct = if current_price > lower_limit {
|
|
53
|
+
round((current_price - lower_limit) / lower_limit * 100.0, 2)
|
|
54
|
+
} else {
|
|
55
|
+
0.0
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
CircuitStatus {
|
|
59
|
+
upper_limit,
|
|
60
|
+
lower_limit,
|
|
61
|
+
upper_distance_pct,
|
|
62
|
+
lower_distance_pct,
|
|
63
|
+
near_upper: upper_distance_pct < 1.0,
|
|
64
|
+
near_lower: lower_distance_pct < 1.0,
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
#[cfg(test)]
|
|
69
|
+
mod tests {
|
|
70
|
+
use super::*;
|
|
71
|
+
|
|
72
|
+
#[test]
|
|
73
|
+
fn circuit_basic() {
|
|
74
|
+
let status = circuit_proximity(108.0, 100.0, CircuitLimit::Percent10);
|
|
75
|
+
assert_eq!(status.upper_limit, 110.0);
|
|
76
|
+
assert_eq!(status.lower_limit, 90.0);
|
|
77
|
+
assert!(!status.near_upper);
|
|
78
|
+
assert!(!status.near_lower);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
#[test]
|
|
82
|
+
fn circuit_near_upper() {
|
|
83
|
+
let status = circuit_proximity(109.5, 100.0, CircuitLimit::Percent10);
|
|
84
|
+
assert!(status.near_upper);
|
|
85
|
+
assert!(!status.near_lower);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
#[test]
|
|
89
|
+
fn circuit_at_lower() {
|
|
90
|
+
let status = circuit_proximity(90.0, 100.0, CircuitLimit::Percent10);
|
|
91
|
+
assert_eq!(status.lower_distance_pct, 0.0);
|
|
92
|
+
assert!(status.near_lower);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
#[test]
|
|
96
|
+
fn circuit_2_pct() {
|
|
97
|
+
let status = circuit_proximity(100.0, 100.0, CircuitLimit::Percent2);
|
|
98
|
+
assert_eq!(status.upper_limit, 102.0);
|
|
99
|
+
assert_eq!(status.lower_limit, 98.0);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
use crate::core::utils::round;
|
|
2
|
+
|
|
3
|
+
/// Delivery trend classification.
|
|
4
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
5
|
+
pub enum DeliveryTrend {
|
|
6
|
+
StrongAccumulation, // High delivery % + price up
|
|
7
|
+
Accumulation, // Above avg delivery % + price up
|
|
8
|
+
Distribution, // Above avg delivery % + price down
|
|
9
|
+
StrongDistribution, // High delivery % + price down
|
|
10
|
+
Neutral,
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
impl std::fmt::Display for DeliveryTrend {
|
|
14
|
+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
15
|
+
match self {
|
|
16
|
+
Self::StrongAccumulation => write!(f, "strong_accumulation"),
|
|
17
|
+
Self::Accumulation => write!(f, "accumulation"),
|
|
18
|
+
Self::Distribution => write!(f, "distribution"),
|
|
19
|
+
Self::StrongDistribution => write!(f, "strong_distribution"),
|
|
20
|
+
Self::Neutral => write!(f, "neutral"),
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/// Delivery percentage: delivery_volume / total_volume * 100.
|
|
26
|
+
/// Unique to NSE/BSE — not available in US markets.
|
|
27
|
+
#[must_use]
|
|
28
|
+
pub fn delivery_pct(delivery_volume: f64, total_volume: f64) -> f64 {
|
|
29
|
+
if total_volume < f64::EPSILON {
|
|
30
|
+
return 0.0;
|
|
31
|
+
}
|
|
32
|
+
round(delivery_volume / total_volume * 100.0, 2)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/// Delivery trend analysis: compares recent delivery % with price movement.
|
|
36
|
+
/// High delivery + price up = genuine buying (accumulation).
|
|
37
|
+
/// High delivery + price down = genuine selling (distribution).
|
|
38
|
+
#[must_use]
|
|
39
|
+
pub fn delivery_trend(
|
|
40
|
+
delivery_pcts: &[f64],
|
|
41
|
+
closes: &[f64],
|
|
42
|
+
short_period: usize,
|
|
43
|
+
long_period: usize,
|
|
44
|
+
) -> Option<DeliveryTrend> {
|
|
45
|
+
if delivery_pcts.len() < long_period
|
|
46
|
+
|| closes.len() < long_period
|
|
47
|
+
|| short_period == 0
|
|
48
|
+
|| long_period == 0
|
|
49
|
+
|| short_period >= long_period
|
|
50
|
+
{
|
|
51
|
+
return None;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
let len = delivery_pcts.len();
|
|
55
|
+
let short_avg: f64 =
|
|
56
|
+
delivery_pcts[len - short_period..].iter().sum::<f64>() / short_period as f64;
|
|
57
|
+
let long_avg: f64 = delivery_pcts[len - long_period..].iter().sum::<f64>() / long_period as f64;
|
|
58
|
+
|
|
59
|
+
let delivery_ratio = if long_avg < f64::EPSILON {
|
|
60
|
+
1.0
|
|
61
|
+
} else {
|
|
62
|
+
short_avg / long_avg
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
let clen = closes.len();
|
|
66
|
+
let price_change = if closes[clen - short_period] > f64::EPSILON {
|
|
67
|
+
(closes[clen - 1] - closes[clen - short_period]) / closes[clen - short_period]
|
|
68
|
+
} else {
|
|
69
|
+
0.0
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
Some(match (delivery_ratio, price_change) {
|
|
73
|
+
(r, p) if r > 1.3 && p > 0.02 => DeliveryTrend::StrongAccumulation,
|
|
74
|
+
(r, p) if r > 1.1 && p > 0.0 => DeliveryTrend::Accumulation,
|
|
75
|
+
(r, p) if r > 1.3 && p < -0.02 => DeliveryTrend::StrongDistribution,
|
|
76
|
+
(r, p) if r > 1.1 && p < 0.0 => DeliveryTrend::Distribution,
|
|
77
|
+
_ => DeliveryTrend::Neutral,
|
|
78
|
+
})
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
#[cfg(test)]
|
|
82
|
+
mod tests {
|
|
83
|
+
use super::*;
|
|
84
|
+
|
|
85
|
+
#[test]
|
|
86
|
+
fn delivery_pct_basic() {
|
|
87
|
+
assert_eq!(delivery_pct(500_000.0, 1_000_000.0), 50.0);
|
|
88
|
+
assert_eq!(delivery_pct(0.0, 0.0), 0.0);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
#[test]
|
|
92
|
+
fn accumulation() {
|
|
93
|
+
// High delivery + price up
|
|
94
|
+
let delivery_pcts = vec![40.0, 42.0, 45.0, 50.0, 55.0, 60.0, 65.0, 70.0, 72.0, 75.0];
|
|
95
|
+
let closes = vec![
|
|
96
|
+
100.0, 101.0, 102.0, 103.0, 104.0, 105.0, 106.0, 107.0, 108.0, 110.0,
|
|
97
|
+
];
|
|
98
|
+
let result = delivery_trend(&delivery_pcts, &closes, 3, 10).unwrap();
|
|
99
|
+
assert!(
|
|
100
|
+
result == DeliveryTrend::Accumulation || result == DeliveryTrend::StrongAccumulation
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
#[test]
|
|
105
|
+
fn distribution() {
|
|
106
|
+
// High delivery + price down
|
|
107
|
+
let delivery_pcts = vec![40.0, 42.0, 45.0, 50.0, 55.0, 60.0, 65.0, 70.0, 72.0, 75.0];
|
|
108
|
+
let closes = vec![
|
|
109
|
+
110.0, 109.0, 108.0, 107.0, 106.0, 105.0, 104.0, 103.0, 102.0, 100.0,
|
|
110
|
+
];
|
|
111
|
+
let result = delivery_trend(&delivery_pcts, &closes, 3, 10).unwrap();
|
|
112
|
+
assert!(
|
|
113
|
+
result == DeliveryTrend::Distribution || result == DeliveryTrend::StrongDistribution
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
use crate::core::utils::round;
|
|
2
|
+
use crate::indicators::trend::ema::ema_series;
|
|
3
|
+
|
|
4
|
+
/// MACD crossover direction.
|
|
5
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
6
|
+
pub enum Crossover {
|
|
7
|
+
Bullish,
|
|
8
|
+
Bearish,
|
|
9
|
+
Neutral,
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
impl std::fmt::Display for Crossover {
|
|
13
|
+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
14
|
+
match self {
|
|
15
|
+
Self::Bullish => write!(f, "bullish"),
|
|
16
|
+
Self::Bearish => write!(f, "bearish"),
|
|
17
|
+
Self::Neutral => write!(f, "none"),
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/// MACD computation result.
|
|
23
|
+
#[derive(Debug, Clone)]
|
|
24
|
+
pub struct MacdResult {
|
|
25
|
+
pub value: f64,
|
|
26
|
+
pub signal: f64,
|
|
27
|
+
pub histogram: f64,
|
|
28
|
+
pub crossover: Crossover,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/// MACD (Moving Average Convergence Divergence).
|
|
32
|
+
#[must_use]
|
|
33
|
+
pub fn macd(
|
|
34
|
+
closes: &[f64],
|
|
35
|
+
fast_period: usize,
|
|
36
|
+
slow_period: usize,
|
|
37
|
+
signal_period: usize,
|
|
38
|
+
) -> Option<MacdResult> {
|
|
39
|
+
if closes.len() < slow_period + signal_period
|
|
40
|
+
|| fast_period == 0
|
|
41
|
+
|| slow_period == 0
|
|
42
|
+
|| signal_period == 0
|
|
43
|
+
|| fast_period >= slow_period
|
|
44
|
+
{
|
|
45
|
+
return None;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let ema_fast = ema_series(closes, fast_period)?;
|
|
49
|
+
let ema_slow = ema_series(closes, slow_period)?;
|
|
50
|
+
|
|
51
|
+
let offset = slow_period - fast_period;
|
|
52
|
+
if ema_fast.len() <= offset {
|
|
53
|
+
return None;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
let macd_line: Vec<f64> = ema_fast[offset..]
|
|
57
|
+
.iter()
|
|
58
|
+
.zip(ema_slow.iter())
|
|
59
|
+
.map(|(f, s)| f - s)
|
|
60
|
+
.collect();
|
|
61
|
+
|
|
62
|
+
if macd_line.len() < signal_period {
|
|
63
|
+
return None;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
let signal_series = ema_series(&macd_line, signal_period)?;
|
|
67
|
+
let current_signal = *signal_series.last()?;
|
|
68
|
+
let current_macd = *macd_line.last()?;
|
|
69
|
+
let histogram = current_macd - current_signal;
|
|
70
|
+
|
|
71
|
+
let prev_signal = if signal_series.len() >= 2 {
|
|
72
|
+
signal_series[signal_series.len() - 2]
|
|
73
|
+
} else {
|
|
74
|
+
current_signal
|
|
75
|
+
};
|
|
76
|
+
let prev_macd = if macd_line.len() >= 2 {
|
|
77
|
+
macd_line[macd_line.len() - 2]
|
|
78
|
+
} else {
|
|
79
|
+
current_macd
|
|
80
|
+
};
|
|
81
|
+
let prev_histogram = prev_macd - prev_signal;
|
|
82
|
+
|
|
83
|
+
let crossover = if prev_histogram <= 0.0 && histogram > 0.0 {
|
|
84
|
+
Crossover::Bullish
|
|
85
|
+
} else if prev_histogram >= 0.0 && histogram < 0.0 {
|
|
86
|
+
Crossover::Bearish
|
|
87
|
+
} else {
|
|
88
|
+
Crossover::Neutral
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
Some(MacdResult {
|
|
92
|
+
value: round(current_macd, 2),
|
|
93
|
+
signal: round(current_signal, 2),
|
|
94
|
+
histogram: round(histogram, 2),
|
|
95
|
+
crossover,
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
#[cfg(test)]
|
|
100
|
+
mod tests {
|
|
101
|
+
use super::*;
|
|
102
|
+
|
|
103
|
+
#[test]
|
|
104
|
+
fn macd_uptrend() {
|
|
105
|
+
let closes: Vec<f64> = (0..50).map(|i| 100.0 + i as f64 * 0.5).collect();
|
|
106
|
+
let result = macd(&closes, 12, 26, 9).unwrap();
|
|
107
|
+
assert!(result.value > 0.0);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
#[test]
|
|
111
|
+
fn macd_downtrend() {
|
|
112
|
+
let closes: Vec<f64> = (0..50).map(|i| 150.0 - i as f64 * 0.5).collect();
|
|
113
|
+
let result = macd(&closes, 12, 26, 9).unwrap();
|
|
114
|
+
assert!(result.value < 0.0);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
#[test]
|
|
118
|
+
fn macd_invalid_params() {
|
|
119
|
+
let data = vec![1.0; 50];
|
|
120
|
+
assert!(macd(&data, 0, 26, 9).is_none());
|
|
121
|
+
assert!(macd(&data, 26, 12, 9).is_none());
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
use crate::core::traits::Indicator;
|
|
2
|
+
use crate::core::types::Candle;
|
|
3
|
+
use crate::core::utils::wilders_step;
|
|
4
|
+
|
|
5
|
+
/// Streaming RSI (Wilder's smoothing).
|
|
6
|
+
pub struct Rsi {
|
|
7
|
+
period: usize,
|
|
8
|
+
count: usize,
|
|
9
|
+
prev_close: f64,
|
|
10
|
+
gain_sum: f64,
|
|
11
|
+
loss_sum: f64,
|
|
12
|
+
avg_gain: f64,
|
|
13
|
+
avg_loss: f64,
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
impl Rsi {
|
|
17
|
+
pub fn new(period: usize) -> Self {
|
|
18
|
+
Self {
|
|
19
|
+
period,
|
|
20
|
+
count: 0,
|
|
21
|
+
prev_close: 0.0,
|
|
22
|
+
gain_sum: 0.0,
|
|
23
|
+
loss_sum: 0.0,
|
|
24
|
+
avg_gain: 0.0,
|
|
25
|
+
avg_loss: 0.0,
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
impl Indicator for Rsi {
|
|
31
|
+
type Output = f64;
|
|
32
|
+
|
|
33
|
+
fn update(&mut self, candle: &Candle) -> Option<f64> {
|
|
34
|
+
self.count += 1;
|
|
35
|
+
|
|
36
|
+
if self.count == 1 {
|
|
37
|
+
self.prev_close = candle.close;
|
|
38
|
+
return None;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let change = candle.close - self.prev_close;
|
|
42
|
+
let gain = change.max(0.0);
|
|
43
|
+
let loss = (-change).max(0.0);
|
|
44
|
+
self.prev_close = candle.close;
|
|
45
|
+
|
|
46
|
+
let idx = self.count - 1;
|
|
47
|
+
|
|
48
|
+
if idx <= self.period {
|
|
49
|
+
self.gain_sum += gain;
|
|
50
|
+
self.loss_sum += loss;
|
|
51
|
+
if idx == self.period {
|
|
52
|
+
self.avg_gain = self.gain_sum / self.period as f64;
|
|
53
|
+
self.avg_loss = self.loss_sum / self.period as f64;
|
|
54
|
+
} else {
|
|
55
|
+
return None;
|
|
56
|
+
}
|
|
57
|
+
} else {
|
|
58
|
+
self.avg_gain = wilders_step(self.avg_gain, gain, self.period);
|
|
59
|
+
self.avg_loss = wilders_step(self.avg_loss, loss, self.period);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if self.avg_loss < f64::EPSILON {
|
|
63
|
+
return Some(100.0);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
let rs = self.avg_gain / self.avg_loss;
|
|
67
|
+
Some(100.0 - 100.0 / (1.0 + rs))
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
fn reset(&mut self) {
|
|
71
|
+
*self = Self::new(self.period);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/// Convenience function: RSI from close values.
|
|
76
|
+
#[must_use]
|
|
77
|
+
pub fn rsi(closes: &[f64], period: usize) -> Option<f64> {
|
|
78
|
+
if closes.len() < period + 1 || period == 0 {
|
|
79
|
+
return None;
|
|
80
|
+
}
|
|
81
|
+
let candles: Vec<Candle> = closes.iter().map(|&c| Candle::from_close(c)).collect();
|
|
82
|
+
let mut ind = Rsi::new(period);
|
|
83
|
+
ind.compute_last(&candles)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
#[cfg(test)]
|
|
87
|
+
mod tests {
|
|
88
|
+
use super::*;
|
|
89
|
+
|
|
90
|
+
#[test]
|
|
91
|
+
fn rsi_all_gains() {
|
|
92
|
+
let closes: Vec<f64> = (0..20).map(|i| 100.0 + i as f64).collect();
|
|
93
|
+
assert_eq!(rsi(&closes, 14), Some(100.0));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
#[test]
|
|
97
|
+
fn rsi_all_losses() {
|
|
98
|
+
let closes: Vec<f64> = (0..20).map(|i| 100.0 - i as f64).collect();
|
|
99
|
+
let result = rsi(&closes, 14).unwrap();
|
|
100
|
+
assert!(result < 1.0);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
#[test]
|
|
104
|
+
fn rsi_midrange() {
|
|
105
|
+
let closes = vec![
|
|
106
|
+
100.0, 102.0, 100.0, 102.0, 100.0, 102.0, 100.0, 102.0, 100.0, 102.0, 100.0, 102.0,
|
|
107
|
+
100.0, 102.0, 100.0, 102.0,
|
|
108
|
+
];
|
|
109
|
+
let result = rsi(&closes, 14).unwrap();
|
|
110
|
+
assert!((result - 50.0).abs() < 5.0);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
#[test]
|
|
114
|
+
fn rsi_insufficient() {
|
|
115
|
+
assert_eq!(rsi(&[100.0, 101.0], 14), None);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
#[test]
|
|
119
|
+
fn rsi_streaming() {
|
|
120
|
+
let closes: Vec<f64> = (0..20).map(|i| 100.0 + i as f64).collect();
|
|
121
|
+
let fn_result = rsi(&closes, 14).unwrap();
|
|
122
|
+
|
|
123
|
+
let mut ind = Rsi::new(14);
|
|
124
|
+
let mut last = None;
|
|
125
|
+
for &c in &closes {
|
|
126
|
+
last = ind.update(&Candle::from_close(c));
|
|
127
|
+
}
|
|
128
|
+
assert!((last.unwrap() - fn_result).abs() < 0.01);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
use crate::core::utils::round;
|
|
2
|
+
use std::collections::VecDeque;
|
|
3
|
+
|
|
4
|
+
/// Stochastic Oscillator result.
|
|
5
|
+
#[derive(Debug, Clone, Copy)]
|
|
6
|
+
pub struct StochasticResult {
|
|
7
|
+
pub k: f64, // Fast line (0-100)
|
|
8
|
+
pub d: f64, // Slow line (SMA of K)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/// Stochastic Oscillator (%K, %D).
|
|
12
|
+
/// %K = (close - lowest_low) / (highest_high - lowest_low) * 100
|
|
13
|
+
/// %D = SMA of %K over d_period
|
|
14
|
+
#[must_use]
|
|
15
|
+
pub fn stochastic(
|
|
16
|
+
highs: &[f64],
|
|
17
|
+
lows: &[f64],
|
|
18
|
+
closes: &[f64],
|
|
19
|
+
k_period: usize,
|
|
20
|
+
d_period: usize,
|
|
21
|
+
) -> Option<StochasticResult> {
|
|
22
|
+
let len = closes.len();
|
|
23
|
+
if len < k_period + d_period - 1
|
|
24
|
+
|| highs.len() < len
|
|
25
|
+
|| lows.len() < len
|
|
26
|
+
|| k_period == 0
|
|
27
|
+
|| d_period == 0
|
|
28
|
+
{
|
|
29
|
+
return None;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Compute %K values
|
|
33
|
+
let mut k_values = VecDeque::with_capacity(d_period);
|
|
34
|
+
|
|
35
|
+
for i in (k_period - 1)..len {
|
|
36
|
+
let window_highs = &highs[i + 1 - k_period..=i];
|
|
37
|
+
let window_lows = &lows[i + 1 - k_period..=i];
|
|
38
|
+
|
|
39
|
+
let highest = window_highs
|
|
40
|
+
.iter()
|
|
41
|
+
.cloned()
|
|
42
|
+
.fold(f64::NEG_INFINITY, f64::max);
|
|
43
|
+
let lowest = window_lows.iter().cloned().fold(f64::INFINITY, f64::min);
|
|
44
|
+
|
|
45
|
+
let range = highest - lowest;
|
|
46
|
+
let k = if range < f64::EPSILON {
|
|
47
|
+
50.0
|
|
48
|
+
} else {
|
|
49
|
+
(closes[i] - lowest) / range * 100.0
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
k_values.push_back(k);
|
|
53
|
+
if k_values.len() > d_period {
|
|
54
|
+
k_values.pop_front();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if k_values.len() < d_period {
|
|
59
|
+
return None;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
let k = *k_values.back()?;
|
|
63
|
+
let d = k_values.iter().sum::<f64>() / d_period as f64;
|
|
64
|
+
|
|
65
|
+
Some(StochasticResult {
|
|
66
|
+
k: round(k, 2),
|
|
67
|
+
d: round(d, 2),
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
#[cfg(test)]
|
|
72
|
+
mod tests {
|
|
73
|
+
use super::*;
|
|
74
|
+
|
|
75
|
+
#[test]
|
|
76
|
+
fn stochastic_basic() {
|
|
77
|
+
let highs = vec![10.0, 12.0, 14.0, 13.0, 15.0, 14.0, 16.0, 15.0, 17.0, 16.0];
|
|
78
|
+
let lows = vec![8.0, 9.0, 11.0, 10.0, 12.0, 11.0, 13.0, 12.0, 14.0, 13.0];
|
|
79
|
+
let closes = vec![9.0, 11.0, 13.0, 12.0, 14.0, 13.0, 15.0, 14.0, 16.0, 15.0];
|
|
80
|
+
let result = stochastic(&highs, &lows, &closes, 5, 3).unwrap();
|
|
81
|
+
assert!(result.k >= 0.0 && result.k <= 100.0);
|
|
82
|
+
assert!(result.d >= 0.0 && result.d <= 100.0);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
#[test]
|
|
86
|
+
fn stochastic_overbought() {
|
|
87
|
+
// Price at highs = %K near 100
|
|
88
|
+
let highs: Vec<f64> = (0..20).map(|i| 100.0 + i as f64).collect();
|
|
89
|
+
let lows: Vec<f64> = highs.iter().map(|h| h - 5.0).collect();
|
|
90
|
+
let closes = highs.clone(); // Close at high
|
|
91
|
+
let result = stochastic(&highs, &lows, &closes, 14, 3).unwrap();
|
|
92
|
+
assert!(result.k > 80.0);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
#[test]
|
|
96
|
+
fn stochastic_insufficient() {
|
|
97
|
+
assert!(stochastic(&[1.0; 5], &[1.0; 5], &[1.0; 5], 14, 3).is_none());
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pub mod pivot;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
use crate::core::utils::round;
|
|
2
|
+
|
|
3
|
+
#[derive(Debug, Clone)]
|
|
4
|
+
pub struct PivotPointsResult {
|
|
5
|
+
pub r3: f64,
|
|
6
|
+
pub r2: f64,
|
|
7
|
+
pub r1: f64,
|
|
8
|
+
pub pivot: f64,
|
|
9
|
+
pub s1: f64,
|
|
10
|
+
pub s2: f64,
|
|
11
|
+
pub s3: f64,
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/// Classic Pivot Points from a single candle.
|
|
15
|
+
#[must_use]
|
|
16
|
+
pub fn pivot_points(high: f64, low: f64, close: f64) -> PivotPointsResult {
|
|
17
|
+
let pivot = (high + low + close) / 3.0;
|
|
18
|
+
PivotPointsResult {
|
|
19
|
+
r3: round(high + 2.0 * (pivot - low), 2),
|
|
20
|
+
r2: round(pivot + (high - low), 2),
|
|
21
|
+
r1: round(2.0 * pivot - low, 2),
|
|
22
|
+
pivot: round(pivot, 2),
|
|
23
|
+
s1: round(2.0 * pivot - high, 2),
|
|
24
|
+
s2: round(pivot - (high - low), 2),
|
|
25
|
+
s3: round(low - 2.0 * (high - pivot), 2),
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
#[cfg(test)]
|
|
30
|
+
mod tests {
|
|
31
|
+
use super::*;
|
|
32
|
+
|
|
33
|
+
#[test]
|
|
34
|
+
fn pivot_basic() {
|
|
35
|
+
let r = pivot_points(110.0, 90.0, 100.0);
|
|
36
|
+
assert_eq!(r.pivot, 100.0);
|
|
37
|
+
assert!(r.r1 > r.pivot);
|
|
38
|
+
assert!(r.s1 < r.pivot);
|
|
39
|
+
}
|
|
40
|
+
}
|