Files
Ely-GPUI-Components/src/finance/indicators.rs
T

254 lines
8.3 KiB
Rust

use super::candles::Candle;
/// A reading per candle; none until an indicator has seen enough.
pub(crate) type Reading = Vec<Option<f64>>;
/// A simple moving average of `period` values.
pub(crate) fn sma(values: &[f64], period: usize) -> Reading {
assert!(period > 0, "an average needs a period");
(0..values.len())
.map(|ix| {
(ix + 1 >= period)
.then(|| values[ix + 1 - period..=ix].iter().sum::<f64>() / period as f64)
})
.collect()
}
/// An exponential moving average, seeded with the first period's simple average.
pub(crate) fn ema(values: &[f64], period: usize) -> Reading {
assert!(period > 0, "an average needs a period");
let weight = 2.0 / (period as f64 + 1.0);
let mut out = vec![None; values.len()];
if values.len() < period {
return out;
}
let mut last = values[..period].iter().sum::<f64>() / period as f64;
out[period - 1] = Some(last);
for ix in period..values.len() {
last = values[ix] * weight + last * (1.0 - weight);
out[ix] = Some(last);
}
out
}
/// An average over only the readings there are, in place: leading gaps stay gaps.
fn ema_of(reading: &Reading, period: usize) -> Reading {
let start = reading
.iter()
.position(Option::is_some)
.unwrap_or(reading.len());
let values: Vec<f64> = reading[start..]
.iter()
.map(|value| value.expect("readings run on once they start"))
.collect();
let mut out = vec![None; start];
out.extend(ema(&values, period));
out
}
/// Bollinger bands: the moving average, and `width` standard deviations below and above it.
pub(crate) fn bollinger(values: &[f64], period: usize, width: f64) -> Vec<Option<(f64, f64, f64)>> {
sma(values, period)
.into_iter()
.enumerate()
.map(|(ix, mean)| {
let mean = mean?;
let window = &values[ix + 1 - period..=ix];
let spread = (window
.iter()
.map(|value| (value - mean).powi(2))
.sum::<f64>()
/ period as f64)
.sqrt();
Some((mean - width * spread, mean, mean + width * spread))
})
.collect()
}
/// The volume-weighted average of each candle's typical price, from the first candle on.
pub(crate) fn vwap(candles: &[Candle]) -> Reading {
let (mut weighted, mut volume) = (0.0, 0.0);
candles
.iter()
.map(|candle| {
weighted += (candle.high + candle.low + candle.close) / 3.0 * candle.volume;
volume += candle.volume;
(volume > 0.0).then(|| weighted / volume)
})
.collect()
}
/// MACD: a fast average less a slow one, its own average as the signal, and the gap between them.
pub(crate) struct Macd {
pub line: Reading,
pub signal: Reading,
pub histogram: Reading,
}
pub(crate) fn macd(values: &[f64], (fast, slow, signal): (usize, usize, usize)) -> Macd {
let (quick, steady) = (ema(values, fast), ema(values, slow));
let line: Reading = quick
.iter()
.zip(&steady)
.map(|(a, b)| Some((*a)? - (*b)?))
.collect();
let signal = ema_of(&line, signal);
let histogram = line
.iter()
.zip(&signal)
.map(|(a, b)| Some((*a)? - (*b)?))
.collect();
Macd {
line,
signal,
histogram,
}
}
/// The relative strength index with Wilder's smoothing: 100 when every change rose, 0 when every one fell.
pub(crate) fn rsi(values: &[f64], period: usize) -> Reading {
assert!(period > 0, "an index needs a period");
let mut out = vec![None; values.len()];
if values.len() <= period {
return out;
}
let change = |ix: usize| values[ix] - values[ix - 1];
let (mut gain, mut loss) = (1..=period).fold((0.0, 0.0), |(gain, loss), ix| {
(gain + change(ix).max(0.0), loss + (-change(ix)).max(0.0))
});
(gain, loss) = (gain / period as f64, loss / period as f64);
let index = |gain: f64, loss: f64| {
if loss == 0.0 {
100.0
} else {
100.0 - 100.0 / (1.0 + gain / loss)
}
};
out[period] = Some(index(gain, loss));
for (ix, slot) in out.iter_mut().enumerate().skip(period + 1) {
let smoothing = (period - 1) as f64;
gain = (gain * smoothing + change(ix).max(0.0)) / period as f64;
loss = (loss * smoothing + (-change(ix)).max(0.0)) / period as f64;
*slot = Some(index(gain, loss));
}
out
}
/// The KDJ stochastic: where each close sits in the last `period` candles' range as K, K smoothed as D, and J pulling ahead of both.
pub(crate) fn kdj(candles: &[Candle], period: usize) -> (Reading, Reading, Reading) {
assert!(period > 0, "a stochastic needs a period");
let (mut k, mut d) = (50.0, 50.0);
let (mut ks, mut ds, mut js) = (
vec![None; candles.len()],
vec![None; candles.len()],
vec![None; candles.len()],
);
for ix in period.saturating_sub(1)..candles.len() {
let window = &candles[ix + 1 - period..=ix];
let (low, high) = window
.iter()
.fold((f64::MAX, f64::MIN), |(low, high), candle| {
(low.min(candle.low), high.max(candle.high))
});
let raw = if high > low {
(candles[ix].close - low) / (high - low) * 100.0
} else {
50.0
};
k = k * 2.0 / 3.0 + raw / 3.0;
d = d * 2.0 / 3.0 + k / 3.0;
(ks[ix], ds[ix], js[ix]) = (Some(k), Some(d), Some(3.0 * k - 2.0 * d));
}
(ks, ds, js)
}
/// On-balance volume: the running total of volume, added on a rising close and taken on a falling one.
pub(crate) fn obv(candles: &[Candle]) -> Vec<f64> {
let mut total = 0.0;
candles
.iter()
.enumerate()
.map(|(ix, candle)| {
if let Some(last) = ix.checked_sub(1).map(|before| candles[before].close) {
total += if candle.close > last {
candle.volume
} else if candle.close < last {
-candle.volume
} else {
0.0
};
}
total
})
.collect()
}
#[cfg(test)]
mod tests {
use jiff::Timestamp;
use super::*;
fn candle(close: f64, volume: f64) -> Candle {
Candle::new(
Timestamp::UNIX_EPOCH,
(close, close + 1.0, close - 1.0, close),
volume,
)
}
#[test]
fn averages_start_once_they_have_a_period() {
assert_eq!(
sma(&[1.0, 2.0, 3.0, 4.0], 2),
[None, Some(1.5), Some(2.5), Some(3.5)]
);
assert_eq!(
ema(&[1.0, 2.0, 3.0, 4.0, 5.0], 3),
[None, None, Some(2.0), Some(3.0), Some(4.0)]
);
}
#[test]
fn bands_sit_two_deviations_either_side() {
let bands = bollinger(&[2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0], 8, 2.0);
assert_eq!(bands[7], Some((1.0, 5.0, 9.0)));
assert_eq!(bands[6], None);
}
#[test]
fn momentum_reads_rises_and_falls() {
let rising: Vec<f64> = (0..20).map(f64::from).collect();
assert_eq!(rsi(&rising, 14)[19], Some(100.0), "nothing fell");
let falling: Vec<f64> = rising.iter().rev().copied().collect();
assert_eq!(rsi(&falling, 14)[19], Some(0.0), "nothing rose");
let trend = macd(&rising, (3, 6, 2));
assert_eq!(trend.line[4], None);
assert!(trend.line[5].is_some() && trend.signal[5].is_none() && trend.signal[6].is_some());
assert!(
trend.line[10].expect("a line") > 0.0,
"a rise holds the fast average above the slow"
);
}
#[test]
fn volume_follows_the_close() {
let candles = [
candle(10.0, 100.0),
candle(11.0, 200.0),
candle(10.5, 50.0),
candle(10.5, 70.0),
];
assert_eq!(obv(&candles), [0.0, 200.0, 150.0, 150.0]);
let mean = vwap(&candles);
assert_eq!(mean[0], Some(10.0));
assert!((mean[1].expect("a price") - (10.0 * 100.0 + 11.0 * 200.0) / 300.0).abs() < 1e-9);
let (k, d, j) = kdj(&candles, 2);
assert_eq!(k[0], None);
assert!(k[1].is_some() && d[1].is_some());
assert!(
(j[1].expect("j") - (3.0 * k[1].expect("k") - 2.0 * d[1].expect("d"))).abs() < 1e-9
);
}
}