work on new engine

This commit is contained in:
Boki 2025-07-04 11:24:27 -04:00
parent 44476da13f
commit a1e5a21847
126 changed files with 3425 additions and 6695 deletions

View file

@ -0,0 +1,250 @@
use super::{Indicator, IncrementalIndicator, IndicatorResult, IndicatorError, PriceData};
use super::common::RollingWindow;
/// Average True Range (ATR) Indicator
///
/// Measures volatility by calculating the average of true ranges over a period
/// True Range = max(High - Low, |High - Previous Close|, |Low - Previous Close|)
pub struct ATR {
period: usize,
atr_value: Option<f64>,
prev_close: Option<f64>,
true_ranges: RollingWindow<f64>,
sum: f64,
initialized: bool,
}
impl ATR {
pub fn new(period: usize) -> Result<Self, IndicatorError> {
if period == 0 {
return Err(IndicatorError::InvalidParameter(
"Period must be greater than 0".to_string()
));
}
Ok(Self {
period,
atr_value: None,
prev_close: None,
true_ranges: RollingWindow::new(period),
sum: 0.0,
initialized: false,
})
}
/// Calculate True Range
fn calculate_true_range(high: f64, low: f64, prev_close: Option<f64>) -> f64 {
let high_low = high - low;
match prev_close {
Some(prev) => {
let high_close = (high - prev).abs();
let low_close = (low - prev).abs();
high_low.max(high_close).max(low_close)
}
None => high_low,
}
}
/// Calculate ATR for a series of price data
pub fn calculate_series(
high: &[f64],
low: &[f64],
close: &[f64],
period: usize
) -> Result<Vec<f64>, IndicatorError> {
if high.len() != low.len() || high.len() != close.len() {
return Err(IndicatorError::InvalidParameter(
"High, low, and close arrays must have the same length".to_string()
));
}
if high.len() < period + 1 {
return Err(IndicatorError::InsufficientData {
required: period + 1,
actual: high.len(),
});
}
let mut true_ranges = Vec::with_capacity(high.len() - 1);
// Calculate true ranges
for i in 1..high.len() {
let tr = Self::calculate_true_range(high[i], low[i], Some(close[i - 1]));
true_ranges.push(tr);
}
let mut atr_values = Vec::with_capacity(true_ranges.len() - period + 1);
// Calculate initial ATR as SMA of first period true ranges
let initial_atr: f64 = true_ranges[0..period].iter().sum::<f64>() / period as f64;
atr_values.push(initial_atr);
// Calculate subsequent ATRs using Wilder's smoothing
let mut atr = initial_atr;
for i in period..true_ranges.len() {
// Wilder's smoothing: ATR = ((n-1) * ATR + TR) / n
atr = ((period - 1) as f64 * atr + true_ranges[i]) / period as f64;
atr_values.push(atr);
}
Ok(atr_values)
}
}
impl Indicator for ATR {
fn calculate(&mut self, data: &PriceData) -> Result<IndicatorResult, IndicatorError> {
if data.high.len() != data.low.len() || data.high.len() != data.close.len() {
return Err(IndicatorError::InvalidParameter(
"Price data arrays must have the same length".to_string()
));
}
let atr_values = Self::calculate_series(
&data.high,
&data.low,
&data.close,
self.period
)?;
Ok(IndicatorResult::Series(atr_values))
}
fn reset(&mut self) {
self.atr_value = None;
self.prev_close = None;
self.true_ranges.clear();
self.sum = 0.0;
self.initialized = false;
}
fn is_ready(&self) -> bool {
self.initialized
}
}
impl IncrementalIndicator for ATR {
fn update(&mut self, value: f64) -> Result<Option<f64>, IndicatorError> {
// For single value update, we assume it's the close price
// In real usage, you'd need to provide high/low/close separately
self.update_hlc(value, value, value)
}
fn current(&self) -> Option<f64> {
self.atr_value
}
}
impl ATR {
/// Update with high, low, close values
pub fn update_hlc(&mut self, high: f64, low: f64, close: f64) -> Result<Option<f64>, IndicatorError> {
if let Some(prev_close) = self.prev_close {
let tr = Self::calculate_true_range(high, low, Some(prev_close));
if !self.initialized {
// Still building initial window
self.true_ranges.push(tr);
self.sum += tr;
if self.true_ranges.is_full() {
// Calculate initial ATR
let initial_atr = self.sum / self.period as f64;
self.atr_value = Some(initial_atr);
self.initialized = true;
}
} else {
// Update ATR using Wilder's smoothing
if let Some(current_atr) = self.atr_value {
let new_atr = ((self.period - 1) as f64 * current_atr + tr) / self.period as f64;
self.atr_value = Some(new_atr);
}
}
}
self.prev_close = Some(close);
Ok(self.atr_value)
}
/// Get ATR as a percentage of price
pub fn atr_percent(&self, price: f64) -> Option<f64> {
self.atr_value.map(|atr| (atr / price) * 100.0)
}
/// Calculate stop loss based on ATR multiple
pub fn calculate_stop_loss(&self, entry_price: f64, is_long: bool, atr_multiple: f64) -> Option<f64> {
self.atr_value.map(|atr| {
if is_long {
entry_price - (atr * atr_multiple)
} else {
entry_price + (atr * atr_multiple)
}
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_atr_calculation() {
let high = vec![
48.70, 48.72, 48.90, 48.87, 48.82,
49.05, 49.20, 49.35, 49.92, 50.19,
50.12, 49.66, 49.88, 50.19, 50.36
];
let low = vec![
47.79, 48.14, 48.39, 48.37, 48.24,
48.64, 48.94, 48.86, 49.50, 49.87,
49.20, 48.90, 49.43, 49.73, 49.26
];
let close = vec![
48.16, 48.61, 48.75, 48.63, 48.74,
49.03, 49.07, 49.32, 49.91, 50.13,
49.53, 49.50, 49.75, 50.03, 50.29
];
let atr_values = ATR::calculate_series(&high, &low, &close, 14).unwrap();
assert!(!atr_values.is_empty());
// ATR should always be positive
for atr in &atr_values {
assert!(*atr > 0.0);
}
}
#[test]
fn test_true_range_calculation() {
// Test case where high-low is largest
let tr = ATR::calculate_true_range(50.0, 45.0, Some(47.0));
assert_eq!(tr, 5.0);
// Test case where high-prev_close is largest
let tr = ATR::calculate_true_range(50.0, 48.0, Some(45.0));
assert_eq!(tr, 5.0);
// Test case where prev_close-low is largest
let tr = ATR::calculate_true_range(48.0, 45.0, Some(50.0));
assert_eq!(tr, 5.0);
}
#[test]
fn test_incremental_atr() {
let mut atr = ATR::new(5).unwrap();
// Need at least one previous close
assert_eq!(atr.update_hlc(10.0, 9.0, 9.5).unwrap(), None);
// Build up window
assert_eq!(atr.update_hlc(10.2, 9.1, 9.8).unwrap(), None);
assert_eq!(atr.update_hlc(10.5, 9.3, 10.0).unwrap(), None);
assert_eq!(atr.update_hlc(10.3, 9.5, 9.7).unwrap(), None);
assert_eq!(atr.update_hlc(10.1, 9.2, 9.6).unwrap(), None);
// Should have ATR value now
let atr_value = atr.update_hlc(10.0, 9.0, 9.5).unwrap();
assert!(atr_value.is_some());
assert!(atr_value.unwrap() > 0.0);
}
}

View file

@ -0,0 +1,256 @@
use super::{Indicator, IncrementalIndicator, IndicatorResult, IndicatorError, PriceData};
use super::sma::SMA;
use super::common::RollingWindow;
/// Bollinger Bands Indicator
///
/// Middle Band = SMA(n)
/// Upper Band = SMA(n) + (k × σ)
/// Lower Band = SMA(n) - (k × σ)
/// where σ is the standard deviation and k is typically 2
pub struct BollingerBands {
period: usize,
std_dev_multiplier: f64,
sma: SMA,
window: RollingWindow<f64>,
}
impl BollingerBands {
pub fn new(period: usize, std_dev_multiplier: f64) -> Result<Self, IndicatorError> {
if period == 0 {
return Err(IndicatorError::InvalidParameter(
"Period must be greater than 0".to_string()
));
}
if std_dev_multiplier <= 0.0 {
return Err(IndicatorError::InvalidParameter(
"Standard deviation multiplier must be positive".to_string()
));
}
Ok(Self {
period,
std_dev_multiplier,
sma: SMA::new(period)?,
window: RollingWindow::new(period),
})
}
/// Standard Bollinger Bands with 20 period and 2 standard deviations
pub fn standard() -> Result<Self, IndicatorError> {
Self::new(20, 2.0)
}
/// Calculate standard deviation
fn calculate_std_dev(values: &[f64], mean: f64) -> f64 {
if values.is_empty() {
return 0.0;
}
let variance = values.iter()
.map(|x| (*x - mean).powi(2))
.sum::<f64>() / values.len() as f64;
variance.sqrt()
}
/// Calculate Bollinger Bands for a series of values
pub fn calculate_series(
values: &[f64],
period: usize,
std_dev_multiplier: f64
) -> Result<(Vec<f64>, Vec<f64>, Vec<f64>), IndicatorError> {
if period == 0 {
return Err(IndicatorError::InvalidParameter(
"Period must be greater than 0".to_string()
));
}
if values.len() < period {
return Err(IndicatorError::InsufficientData {
required: period,
actual: values.len(),
});
}
// Calculate SMA (middle band)
let middle_band = SMA::calculate_series(values, period)?;
let mut upper_band = Vec::with_capacity(middle_band.len());
let mut lower_band = Vec::with_capacity(middle_band.len());
// Calculate bands
for i in 0..middle_band.len() {
// Get the window of values for this position
let start_idx = i;
let end_idx = i + period;
let window = &values[start_idx..end_idx];
// Calculate standard deviation
let std_dev = Self::calculate_std_dev(window, middle_band[i]);
let band_width = std_dev * std_dev_multiplier;
upper_band.push(middle_band[i] + band_width);
lower_band.push(middle_band[i] - band_width);
}
Ok((middle_band, upper_band, lower_band))
}
/// Calculate the current bandwidth (distance between upper and lower bands)
pub fn bandwidth(&self) -> Option<f64> {
if let Some(values) = self.get_current_bands() {
Some(values.upper - values.lower)
} else {
None
}
}
/// Calculate %B (percent b) - position of price relative to bands
/// %B = (Price - Lower Band) / (Upper Band - Lower Band)
pub fn percent_b(&self, price: f64) -> Option<f64> {
if let Some(values) = self.get_current_bands() {
let width = values.upper - values.lower;
if width > 0.0 {
Some((price - values.lower) / width)
} else {
None
}
} else {
None
}
}
fn get_current_bands(&self) -> Option<BollingerBandsValues> {
if let Some(middle) = self.sma.current() {
if self.window.is_full() {
let values = self.window.as_slice();
let std_dev = Self::calculate_std_dev(&values, middle);
let band_width = std_dev * self.std_dev_multiplier;
Some(BollingerBandsValues {
middle,
upper: middle + band_width,
lower: middle - band_width,
})
} else {
None
}
} else {
None
}
}
}
impl Indicator for BollingerBands {
fn calculate(&mut self, data: &PriceData) -> Result<IndicatorResult, IndicatorError> {
let values = &data.close;
let (middle, upper, lower) = Self::calculate_series(
values,
self.period,
self.std_dev_multiplier
)?;
Ok(IndicatorResult::BollingerBands {
middle,
upper,
lower,
})
}
fn reset(&mut self) {
self.sma.reset();
self.window.clear();
}
fn is_ready(&self) -> bool {
self.sma.is_ready() && self.window.is_full()
}
}
impl IncrementalIndicator for BollingerBands {
fn update(&mut self, value: f64) -> Result<Option<f64>, IndicatorError> {
// Update window
self.window.push(value);
// Update SMA
let _sma_result = self.sma.update(value)?;
// Return bandwidth if ready
if self.is_ready() {
Ok(self.bandwidth())
} else {
Ok(None)
}
}
fn current(&self) -> Option<f64> {
self.bandwidth()
}
}
/// Structure to hold all Bollinger Bands values
pub struct BollingerBandsValues {
pub middle: f64,
pub upper: f64,
pub lower: f64,
}
impl BollingerBands {
/// Get all current band values
pub fn current_values(&self) -> Option<BollingerBandsValues> {
self.get_current_bands()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_bollinger_bands_calculation() {
let values = vec![
20.0, 21.0, 22.0, 23.0, 24.0,
25.0, 24.0, 23.0, 22.0, 21.0,
20.0, 21.0, 22.0, 23.0, 24.0,
25.0, 24.0, 23.0, 22.0, 21.0
];
let (middle, upper, lower) = BollingerBands::calculate_series(&values, 5, 2.0).unwrap();
assert_eq!(middle.len(), 16);
assert_eq!(upper.len(), 16);
assert_eq!(lower.len(), 16);
// Upper band should always be above middle
for i in 0..middle.len() {
assert!(upper[i] > middle[i]);
assert!(lower[i] < middle[i]);
}
}
#[test]
fn test_percent_b() {
let mut bb = BollingerBands::standard().unwrap();
// Create some test data
for i in 0..25 {
let _ = bb.update(20.0 + (i as f64 % 5.0));
}
// Price at upper band should give %B ≈ 1.0
if let Some(bands) = bb.current_values() {
let percent_b = bb.percent_b(bands.upper).unwrap();
assert!((percent_b - 1.0).abs() < 1e-10);
// Price at lower band should give %B ≈ 0.0
let percent_b = bb.percent_b(bands.lower).unwrap();
assert!(percent_b.abs() < 1e-10);
// Price at middle band should give %B ≈ 0.5
let percent_b = bb.percent_b(bands.middle).unwrap();
assert!((percent_b - 0.5).abs() < 0.1);
}
}
}

View file

@ -0,0 +1,142 @@
use serde::{Serialize, Deserialize};
use std::collections::VecDeque;
/// Common types and utilities for indicators
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PriceData {
pub open: Vec<f64>,
pub high: Vec<f64>,
pub low: Vec<f64>,
pub close: Vec<f64>,
pub volume: Vec<f64>,
}
impl PriceData {
pub fn new() -> Self {
Self {
open: Vec::new(),
high: Vec::new(),
low: Vec::new(),
close: Vec::new(),
volume: Vec::new(),
}
}
pub fn len(&self) -> usize {
self.close.len()
}
pub fn is_empty(&self) -> bool {
self.close.is_empty()
}
/// Get typical price (high + low + close) / 3
pub fn typical_prices(&self) -> Vec<f64> {
self.high.iter()
.zip(&self.low)
.zip(&self.close)
.map(|((h, l), c)| (h + l + c) / 3.0)
.collect()
}
/// Get true range for ATR calculation
pub fn true_ranges(&self) -> Vec<f64> {
if self.len() < 2 {
return vec![];
}
let mut ranges = Vec::with_capacity(self.len() - 1);
for i in 1..self.len() {
let high_low = self.high[i] - self.low[i];
let high_close = (self.high[i] - self.close[i - 1]).abs();
let low_close = (self.low[i] - self.close[i - 1]).abs();
ranges.push(high_low.max(high_close).max(low_close));
}
ranges
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum IndicatorResult {
Single(f64),
Multiple(Vec<f64>),
Series(Vec<f64>),
MACD {
macd: Vec<f64>,
signal: Vec<f64>,
histogram: Vec<f64>,
},
BollingerBands {
middle: Vec<f64>,
upper: Vec<f64>,
lower: Vec<f64>,
},
Stochastic {
k: Vec<f64>,
d: Vec<f64>,
},
}
#[derive(Debug, Clone)]
pub enum IndicatorError {
InsufficientData { required: usize, actual: usize },
InvalidParameter(String),
CalculationError(String),
}
impl std::fmt::Display for IndicatorError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
IndicatorError::InsufficientData { required, actual } => {
write!(f, "Insufficient data: required {}, got {}", required, actual)
}
IndicatorError::InvalidParameter(msg) => write!(f, "Invalid parameter: {}", msg),
IndicatorError::CalculationError(msg) => write!(f, "Calculation error: {}", msg),
}
}
}
impl std::error::Error for IndicatorError {}
/// Rolling window for incremental calculations
pub struct RollingWindow<T> {
window: VecDeque<T>,
capacity: usize,
}
impl<T: Clone> RollingWindow<T> {
pub fn new(capacity: usize) -> Self {
Self {
window: VecDeque::with_capacity(capacity),
capacity,
}
}
pub fn push(&mut self, value: T) {
if self.window.len() >= self.capacity {
self.window.pop_front();
}
self.window.push_back(value);
}
pub fn is_full(&self) -> bool {
self.window.len() >= self.capacity
}
pub fn len(&self) -> usize {
self.window.len()
}
pub fn iter(&self) -> impl Iterator<Item = &T> {
self.window.iter()
}
pub fn as_slice(&self) -> Vec<T> {
self.window.iter().cloned().collect()
}
pub fn clear(&mut self) {
self.window.clear();
}
}

View file

@ -0,0 +1,213 @@
use super::{Indicator, IncrementalIndicator, IndicatorResult, IndicatorError, PriceData};
/// Exponential Moving Average (EMA) Indicator
///
/// Calculates the exponentially weighted moving average
/// giving more weight to recent prices
pub struct EMA {
period: usize,
alpha: f64,
value: Option<f64>,
initialized: bool,
}
impl EMA {
pub fn new(period: usize) -> Result<Self, IndicatorError> {
if period == 0 {
return Err(IndicatorError::InvalidParameter(
"Period must be greater than 0".to_string()
));
}
// Calculate smoothing factor (alpha)
// Common formula: 2 / (period + 1)
let alpha = 2.0 / (period as f64 + 1.0);
Ok(Self {
period,
alpha,
value: None,
initialized: false,
})
}
/// Create EMA with custom smoothing factor
pub fn with_alpha(alpha: f64) -> Result<Self, IndicatorError> {
if alpha <= 0.0 || alpha > 1.0 {
return Err(IndicatorError::InvalidParameter(
"Alpha must be between 0 and 1".to_string()
));
}
// Calculate equivalent period for reference
let period = ((2.0 / alpha) - 1.0) as usize;
Ok(Self {
period,
alpha,
value: None,
initialized: false,
})
}
/// Calculate EMA for a series of values
pub fn calculate_series(values: &[f64], period: usize) -> Result<Vec<f64>, IndicatorError> {
if period == 0 {
return Err(IndicatorError::InvalidParameter(
"Period must be greater than 0".to_string()
));
}
if values.is_empty() {
return Ok(vec![]);
}
let alpha = 2.0 / (period as f64 + 1.0);
let mut result = Vec::with_capacity(values.len());
// Start with first value as initial EMA
let mut ema = values[0];
result.push(ema);
// Calculate EMA for remaining values
for i in 1..values.len() {
ema = alpha * values[i] + (1.0 - alpha) * ema;
result.push(ema);
}
Ok(result)
}
/// Alternative initialization using SMA of first N values
pub fn calculate_series_sma_init(values: &[f64], period: usize) -> Result<Vec<f64>, IndicatorError> {
if period == 0 {
return Err(IndicatorError::InvalidParameter(
"Period must be greater than 0".to_string()
));
}
if values.len() < period {
return Err(IndicatorError::InsufficientData {
required: period,
actual: values.len(),
});
}
let alpha = 2.0 / (period as f64 + 1.0);
let mut result = Vec::with_capacity(values.len() - period + 1);
// Calculate initial SMA
let initial_sma: f64 = values[0..period].iter().sum::<f64>() / period as f64;
let mut ema = initial_sma;
result.push(ema);
// Calculate EMA for remaining values
for i in period..values.len() {
ema = alpha * values[i] + (1.0 - alpha) * ema;
result.push(ema);
}
Ok(result)
}
}
impl Indicator for EMA {
fn calculate(&mut self, data: &PriceData) -> Result<IndicatorResult, IndicatorError> {
let values = &data.close;
if values.is_empty() {
return Err(IndicatorError::InsufficientData {
required: 1,
actual: 0,
});
}
// Reset and calculate from scratch
self.reset();
let ema_values = Self::calculate_series(values, self.period)?;
// Update internal state with last value
if let Some(&last) = ema_values.last() {
self.value = Some(last);
self.initialized = true;
}
Ok(IndicatorResult::Series(ema_values))
}
fn reset(&mut self) {
self.value = None;
self.initialized = false;
}
fn is_ready(&self) -> bool {
self.initialized && self.value.is_some()
}
}
impl IncrementalIndicator for EMA {
fn update(&mut self, value: f64) -> Result<Option<f64>, IndicatorError> {
match self.value {
Some(prev_ema) => {
// Update EMA: EMA = α × Price + (1 - α) × Previous EMA
let new_ema = self.alpha * value + (1.0 - self.alpha) * prev_ema;
self.value = Some(new_ema);
self.initialized = true;
Ok(Some(new_ema))
}
None => {
// First value becomes the initial EMA
self.value = Some(value);
self.initialized = true;
Ok(Some(value))
}
}
}
fn current(&self) -> Option<f64> {
self.value
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ema_calculation() {
let values = vec![10.0, 11.0, 12.0, 13.0, 14.0, 15.0];
let result = EMA::calculate_series(&values, 3).unwrap();
assert_eq!(result.len(), 6);
assert!((result[0] - 10.0).abs() < 1e-10); // First value
// Verify EMA calculation
let alpha = 2.0 / 4.0; // 0.5
let expected_ema2 = alpha * 11.0 + (1.0 - alpha) * 10.0; // 10.5
assert!((result[1] - expected_ema2).abs() < 1e-10);
}
#[test]
fn test_incremental_ema() {
let mut ema = EMA::new(3).unwrap();
// First value
assert_eq!(ema.update(10.0).unwrap(), Some(10.0));
// Second value: EMA = 0.5 * 12 + 0.5 * 10 = 11
assert_eq!(ema.update(12.0).unwrap(), Some(11.0));
// Third value: EMA = 0.5 * 14 + 0.5 * 11 = 12.5
assert_eq!(ema.update(14.0).unwrap(), Some(12.5));
}
#[test]
fn test_ema_with_sma_init() {
let values = vec![2.0, 4.0, 6.0, 8.0, 10.0, 12.0];
let result = EMA::calculate_series_sma_init(&values, 3).unwrap();
// Initial SMA = (2 + 4 + 6) / 3 = 4
assert_eq!(result.len(), 4);
assert!((result[0] - 4.0).abs() < 1e-10);
}
}

View file

@ -0,0 +1,229 @@
use super::{Indicator, IncrementalIndicator, IndicatorResult, IndicatorError, PriceData};
use super::ema::EMA;
/// Moving Average Convergence Divergence (MACD) Indicator
///
/// MACD = Fast EMA - Slow EMA
/// Signal = EMA of MACD
/// Histogram = MACD - Signal
pub struct MACD {
fast_period: usize,
slow_period: usize,
signal_period: usize,
fast_ema: EMA,
slow_ema: EMA,
signal_ema: EMA,
macd_value: Option<f64>,
}
impl MACD {
pub fn new(fast_period: usize, slow_period: usize, signal_period: usize) -> Result<Self, IndicatorError> {
if fast_period == 0 || slow_period == 0 || signal_period == 0 {
return Err(IndicatorError::InvalidParameter(
"All periods must be greater than 0".to_string()
));
}
if fast_period >= slow_period {
return Err(IndicatorError::InvalidParameter(
"Fast period must be less than slow period".to_string()
));
}
Ok(Self {
fast_period,
slow_period,
signal_period,
fast_ema: EMA::new(fast_period)?,
slow_ema: EMA::new(slow_period)?,
signal_ema: EMA::new(signal_period)?,
macd_value: None,
})
}
/// Standard MACD with 12, 26, 9 periods
pub fn standard() -> Result<Self, IndicatorError> {
Self::new(12, 26, 9)
}
/// Calculate MACD for a series of values
pub fn calculate_series(
values: &[f64],
fast_period: usize,
slow_period: usize,
signal_period: usize
) -> Result<(Vec<f64>, Vec<f64>, Vec<f64>), IndicatorError> {
if fast_period >= slow_period {
return Err(IndicatorError::InvalidParameter(
"Fast period must be less than slow period".to_string()
));
}
if values.len() < slow_period {
return Err(IndicatorError::InsufficientData {
required: slow_period,
actual: values.len(),
});
}
// Calculate EMAs
let fast_ema = EMA::calculate_series(values, fast_period)?;
let slow_ema = EMA::calculate_series(values, slow_period)?;
// Calculate MACD line
let mut macd_line = Vec::with_capacity(slow_ema.len());
for i in 0..slow_ema.len() {
// Align indices - slow EMA starts later
let fast_idx = i + (slow_period - fast_period);
macd_line.push(fast_ema[fast_idx] - slow_ema[i]);
}
// Calculate signal line (EMA of MACD)
let signal_line = if macd_line.len() >= signal_period {
EMA::calculate_series(&macd_line, signal_period)?
} else {
vec![]
};
// Calculate histogram
let mut histogram = Vec::with_capacity(signal_line.len());
for i in 0..signal_line.len() {
// Align indices
let macd_idx = i + (macd_line.len() - signal_line.len());
histogram.push(macd_line[macd_idx] - signal_line[i]);
}
Ok((macd_line, signal_line, histogram))
}
}
impl Indicator for MACD {
fn calculate(&mut self, data: &PriceData) -> Result<IndicatorResult, IndicatorError> {
let values = &data.close;
let (macd, signal, histogram) = Self::calculate_series(
values,
self.fast_period,
self.slow_period,
self.signal_period
)?;
Ok(IndicatorResult::MACD {
macd,
signal,
histogram,
})
}
fn reset(&mut self) {
self.fast_ema.reset();
self.slow_ema.reset();
self.signal_ema.reset();
self.macd_value = None;
}
fn is_ready(&self) -> bool {
self.fast_ema.is_ready() && self.slow_ema.is_ready() && self.signal_ema.is_ready()
}
}
impl IncrementalIndicator for MACD {
fn update(&mut self, value: f64) -> Result<Option<f64>, IndicatorError> {
// Update both EMAs
let fast_result = self.fast_ema.update(value)?;
let slow_result = self.slow_ema.update(value)?;
// Calculate MACD if both EMAs are ready
if let (Some(fast), Some(slow)) = (fast_result, slow_result) {
let macd = fast - slow;
self.macd_value = Some(macd);
// Update signal line
if let Some(signal) = self.signal_ema.update(macd)? {
// Return histogram value
Ok(Some(macd - signal))
} else {
Ok(None)
}
} else {
Ok(None)
}
}
fn current(&self) -> Option<f64> {
// Return current histogram value
if let (Some(fast), Some(slow), Some(signal)) = (
self.fast_ema.current(),
self.slow_ema.current(),
self.signal_ema.current()
) {
let macd = fast - slow;
Some(macd - signal)
} else {
None
}
}
}
/// Structure to hold all MACD values for incremental updates
pub struct MACDValues {
pub macd: f64,
pub signal: f64,
pub histogram: f64,
}
impl MACD {
/// Get all current MACD values
pub fn current_values(&self) -> Option<MACDValues> {
if let (Some(fast), Some(slow), Some(signal)) = (
self.fast_ema.current(),
self.slow_ema.current(),
self.signal_ema.current()
) {
let macd = fast - slow;
Some(MACDValues {
macd,
signal,
histogram: macd - signal,
})
} else {
None
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_macd_calculation() {
let values = vec![
10.0, 10.5, 11.0, 11.5, 12.0, 12.5, 13.0, 13.5,
14.0, 14.5, 15.0, 14.5, 14.0, 13.5, 13.0, 12.5,
12.0, 11.5, 11.0, 10.5, 10.0, 10.5, 11.0, 11.5,
12.0, 12.5, 13.0, 13.5, 14.0, 14.5
];
let (macd, signal, histogram) = MACD::calculate_series(&values, 12, 26, 9).unwrap();
// Should have values after slow period
assert!(!macd.is_empty());
assert!(!signal.is_empty());
assert!(!histogram.is_empty());
// Histogram should equal MACD - Signal
for i in 0..histogram.len() {
let expected = macd[macd.len() - histogram.len() + i] - signal[i];
assert!((histogram[i] - expected).abs() < 1e-10);
}
}
#[test]
fn test_standard_macd() {
let macd = MACD::standard().unwrap();
assert_eq!(macd.fast_period, 12);
assert_eq!(macd.slow_period, 26);
assert_eq!(macd.signal_period, 9);
}
}

View file

@ -0,0 +1,40 @@
// Technical Analysis Indicators Library
pub mod sma;
pub mod ema;
pub mod rsi;
pub mod macd;
pub mod bollinger_bands;
pub mod stochastic;
pub mod atr;
pub mod common;
// Re-export commonly used types and traits
pub use common::{IndicatorResult, IndicatorError, PriceData};
pub use sma::SMA;
pub use ema::EMA;
pub use rsi::RSI;
pub use macd::MACD;
pub use bollinger_bands::BollingerBands;
pub use stochastic::Stochastic;
pub use atr::ATR;
/// Trait that all indicators must implement
pub trait Indicator {
/// Calculate the indicator value(s) for the given data
fn calculate(&mut self, data: &PriceData) -> Result<IndicatorResult, IndicatorError>;
/// Reset the indicator state
fn reset(&mut self);
/// Check if the indicator has enough data to produce valid results
fn is_ready(&self) -> bool;
}
/// Trait for indicators that can be calculated incrementally
pub trait IncrementalIndicator: Indicator {
/// Update the indicator with a new data point
fn update(&mut self, value: f64) -> Result<Option<f64>, IndicatorError>;
/// Get the current value without updating
fn current(&self) -> Option<f64>;
}

View file

@ -0,0 +1,223 @@
use super::{Indicator, IncrementalIndicator, IndicatorResult, IndicatorError, PriceData};
use super::common::RollingWindow;
/// Relative Strength Index (RSI) Indicator
///
/// Measures momentum by comparing the magnitude of recent gains to recent losses
/// RSI = 100 - (100 / (1 + RS))
/// where RS = Average Gain / Average Loss
pub struct RSI {
period: usize,
avg_gain: f64,
avg_loss: f64,
prev_value: Option<f64>,
window: RollingWindow<f64>,
initialized: bool,
}
impl RSI {
pub fn new(period: usize) -> Result<Self, IndicatorError> {
if period == 0 {
return Err(IndicatorError::InvalidParameter(
"Period must be greater than 0".to_string()
));
}
Ok(Self {
period,
avg_gain: 0.0,
avg_loss: 0.0,
prev_value: None,
window: RollingWindow::new(period + 1),
initialized: false,
})
}
/// Calculate RSI for a series of values
pub fn calculate_series(values: &[f64], period: usize) -> Result<Vec<f64>, IndicatorError> {
if period == 0 {
return Err(IndicatorError::InvalidParameter(
"Period must be greater than 0".to_string()
));
}
if values.len() <= period {
return Err(IndicatorError::InsufficientData {
required: period + 1,
actual: values.len(),
});
}
let mut result = Vec::with_capacity(values.len() - period);
let mut gains = Vec::with_capacity(values.len() - 1);
let mut losses = Vec::with_capacity(values.len() - 1);
// Calculate price changes
for i in 1..values.len() {
let change = values[i] - values[i - 1];
if change > 0.0 {
gains.push(change);
losses.push(0.0);
} else {
gains.push(0.0);
losses.push(-change);
}
}
// Calculate initial averages using SMA
let initial_avg_gain: f64 = gains[0..period].iter().sum::<f64>() / period as f64;
let initial_avg_loss: f64 = losses[0..period].iter().sum::<f64>() / period as f64;
// Calculate first RSI
let rs = if initial_avg_loss > 0.0 {
initial_avg_gain / initial_avg_loss
} else {
100.0 // If no losses, RSI is 100
};
result.push(100.0 - (100.0 / (1.0 + rs)));
// Calculate remaining RSIs using EMA smoothing
let mut avg_gain = initial_avg_gain;
let mut avg_loss = initial_avg_loss;
let alpha = 1.0 / period as f64;
for i in period..gains.len() {
// Wilder's smoothing method
avg_gain = (avg_gain * (period - 1) as f64 + gains[i]) / period as f64;
avg_loss = (avg_loss * (period - 1) as f64 + losses[i]) / period as f64;
let rs = if avg_loss > 0.0 {
avg_gain / avg_loss
} else {
100.0
};
result.push(100.0 - (100.0 / (1.0 + rs)));
}
Ok(result)
}
fn calculate_rsi(&self) -> f64 {
if self.avg_loss == 0.0 {
100.0
} else {
let rs = self.avg_gain / self.avg_loss;
100.0 - (100.0 / (1.0 + rs))
}
}
}
impl Indicator for RSI {
fn calculate(&mut self, data: &PriceData) -> Result<IndicatorResult, IndicatorError> {
let values = &data.close;
if values.len() <= self.period {
return Err(IndicatorError::InsufficientData {
required: self.period + 1,
actual: values.len(),
});
}
let rsi_values = Self::calculate_series(values, self.period)?;
Ok(IndicatorResult::Series(rsi_values))
}
fn reset(&mut self) {
self.avg_gain = 0.0;
self.avg_loss = 0.0;
self.prev_value = None;
self.window.clear();
self.initialized = false;
}
fn is_ready(&self) -> bool {
self.initialized
}
}
impl IncrementalIndicator for RSI {
fn update(&mut self, value: f64) -> Result<Option<f64>, IndicatorError> {
self.window.push(value);
if let Some(prev) = self.prev_value {
let change = value - prev;
let gain = if change > 0.0 { change } else { 0.0 };
let loss = if change < 0.0 { -change } else { 0.0 };
if !self.initialized && self.window.len() > self.period {
// Initialize using first period values
let values = self.window.as_slice();
let mut sum_gain = 0.0;
let mut sum_loss = 0.0;
for i in 1..=self.period {
let change = values[i] - values[i - 1];
if change > 0.0 {
sum_gain += change;
} else {
sum_loss += -change;
}
}
self.avg_gain = sum_gain / self.period as f64;
self.avg_loss = sum_loss / self.period as f64;
self.initialized = true;
} else if self.initialized {
// Update using Wilder's smoothing
self.avg_gain = (self.avg_gain * (self.period - 1) as f64 + gain) / self.period as f64;
self.avg_loss = (self.avg_loss * (self.period - 1) as f64 + loss) / self.period as f64;
}
}
self.prev_value = Some(value);
if self.initialized {
Ok(Some(self.calculate_rsi()))
} else {
Ok(None)
}
}
fn current(&self) -> Option<f64> {
if self.initialized {
Some(self.calculate_rsi())
} else {
None
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_rsi_calculation() {
let values = vec![
44.0, 44.25, 44.38, 44.38, 44.88, 45.05,
45.25, 45.38, 45.75, 46.03, 46.23, 46.08,
46.03, 45.85, 46.25, 46.38, 46.50
];
let result = RSI::calculate_series(&values, 14).unwrap();
assert_eq!(result.len(), 3);
// RSI should be between 0 and 100
for rsi in &result {
assert!(*rsi >= 0.0 && *rsi <= 100.0);
}
}
#[test]
fn test_rsi_extremes() {
// All gains - RSI should be close to 100
let increasing = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
let result = RSI::calculate_series(&increasing, 5).unwrap();
assert!(result.last().unwrap() > &95.0);
// All losses - RSI should be close to 0
let decreasing = vec![8.0, 7.0, 6.0, 5.0, 4.0, 3.0, 2.0, 1.0];
let result = RSI::calculate_series(&decreasing, 5).unwrap();
assert!(result.last().unwrap() < &5.0);
}
}

View file

@ -0,0 +1,139 @@
use super::{Indicator, IncrementalIndicator, IndicatorResult, IndicatorError, PriceData};
use super::common::RollingWindow;
/// Simple Moving Average (SMA) Indicator
///
/// Calculates the arithmetic mean of the last N periods
pub struct SMA {
period: usize,
window: RollingWindow<f64>,
sum: f64,
}
impl SMA {
pub fn new(period: usize) -> Result<Self, IndicatorError> {
if period == 0 {
return Err(IndicatorError::InvalidParameter(
"Period must be greater than 0".to_string()
));
}
Ok(Self {
period,
window: RollingWindow::new(period),
sum: 0.0,
})
}
/// Calculate SMA for a series of values
pub fn calculate_series(values: &[f64], period: usize) -> Result<Vec<f64>, IndicatorError> {
if period == 0 {
return Err(IndicatorError::InvalidParameter(
"Period must be greater than 0".to_string()
));
}
if values.len() < period {
return Err(IndicatorError::InsufficientData {
required: period,
actual: values.len(),
});
}
let mut result = Vec::with_capacity(values.len() - period + 1);
// Calculate first SMA
let mut sum: f64 = values[0..period].iter().sum();
result.push(sum / period as f64);
// Calculate remaining SMAs using sliding window
for i in period..values.len() {
sum = sum - values[i - period] + values[i];
result.push(sum / period as f64);
}
Ok(result)
}
}
impl Indicator for SMA {
fn calculate(&mut self, data: &PriceData) -> Result<IndicatorResult, IndicatorError> {
let values = &data.close;
if values.len() < self.period {
return Err(IndicatorError::InsufficientData {
required: self.period,
actual: values.len(),
});
}
let sma_values = Self::calculate_series(values, self.period)?;
Ok(IndicatorResult::Series(sma_values))
}
fn reset(&mut self) {
self.window.clear();
self.sum = 0.0;
}
fn is_ready(&self) -> bool {
self.window.is_full()
}
}
impl IncrementalIndicator for SMA {
fn update(&mut self, value: f64) -> Result<Option<f64>, IndicatorError> {
// If window is full, subtract the oldest value from sum
if self.window.is_full() {
if let Some(oldest) = self.window.iter().next() {
self.sum -= oldest;
}
}
// Add new value
self.window.push(value);
self.sum += value;
// Calculate SMA if we have enough data
if self.window.is_full() {
Ok(Some(self.sum / self.period as f64))
} else {
Ok(None)
}
}
fn current(&self) -> Option<f64> {
if self.window.is_full() {
Some(self.sum / self.period as f64)
} else {
None
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sma_calculation() {
let values = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
let result = SMA::calculate_series(&values, 3).unwrap();
assert_eq!(result.len(), 8);
assert!((result[0] - 2.0).abs() < 1e-10); // (1+2+3)/3 = 2
assert!((result[1] - 3.0).abs() < 1e-10); // (2+3+4)/3 = 3
assert!((result[7] - 9.0).abs() < 1e-10); // (8+9+10)/3 = 9
}
#[test]
fn test_incremental_sma() {
let mut sma = SMA::new(3).unwrap();
assert_eq!(sma.update(1.0).unwrap(), None);
assert_eq!(sma.update(2.0).unwrap(), None);
assert_eq!(sma.update(3.0).unwrap(), Some(2.0));
assert_eq!(sma.update(4.0).unwrap(), Some(3.0));
assert_eq!(sma.update(5.0).unwrap(), Some(4.0));
}
}

View file

@ -0,0 +1,297 @@
use super::{Indicator, IncrementalIndicator, IndicatorResult, IndicatorError, PriceData};
use super::sma::SMA;
use super::common::RollingWindow;
/// Stochastic Oscillator Indicator
///
/// %K = 100 × (Close - Lowest Low) / (Highest High - Lowest Low)
/// %D = SMA of %K
pub struct Stochastic {
k_period: usize,
d_period: usize,
smooth_k: usize,
high_window: RollingWindow<f64>,
low_window: RollingWindow<f64>,
close_window: RollingWindow<f64>,
k_sma: SMA,
d_sma: SMA,
k_values: RollingWindow<f64>,
}
impl Stochastic {
pub fn new(k_period: usize, d_period: usize, smooth_k: usize) -> Result<Self, IndicatorError> {
if k_period == 0 || d_period == 0 {
return Err(IndicatorError::InvalidParameter(
"K and D periods must be greater than 0".to_string()
));
}
Ok(Self {
k_period,
d_period,
smooth_k,
high_window: RollingWindow::new(k_period),
low_window: RollingWindow::new(k_period),
close_window: RollingWindow::new(k_period),
k_sma: SMA::new(smooth_k.max(1))?,
d_sma: SMA::new(d_period)?,
k_values: RollingWindow::new(d_period),
})
}
/// Standard Fast Stochastic (14, 3)
pub fn fast() -> Result<Self, IndicatorError> {
Self::new(14, 3, 1)
}
/// Standard Slow Stochastic (14, 3, 3)
pub fn slow() -> Result<Self, IndicatorError> {
Self::new(14, 3, 3)
}
/// Calculate raw %K value
fn calculate_raw_k(high: &[f64], low: &[f64], close: f64) -> Option<f64> {
if high.is_empty() || low.is_empty() {
return None;
}
let highest = high.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
let lowest = low.iter().cloned().fold(f64::INFINITY, f64::min);
let range = highest - lowest;
if range > 0.0 {
Some(100.0 * (close - lowest) / range)
} else {
Some(50.0) // If no range, return middle value
}
}
/// Calculate Stochastic for a series of price data
pub fn calculate_series(
high: &[f64],
low: &[f64],
close: &[f64],
k_period: usize,
d_period: usize,
smooth_k: usize,
) -> Result<(Vec<f64>, Vec<f64>), IndicatorError> {
if high.len() != low.len() || high.len() != close.len() {
return Err(IndicatorError::InvalidParameter(
"High, low, and close arrays must have the same length".to_string()
));
}
if high.len() < k_period {
return Err(IndicatorError::InsufficientData {
required: k_period,
actual: high.len(),
});
}
// Calculate raw %K values
let mut raw_k_values = Vec::with_capacity(high.len() - k_period + 1);
for i in k_period - 1..high.len() {
let start = i + 1 - k_period;
if let Some(k) = Self::calculate_raw_k(
&high[start..=i],
&low[start..=i],
close[i]
) {
raw_k_values.push(k);
}
}
// Smooth %K if requested
let k_values = if smooth_k > 1 {
SMA::calculate_series(&raw_k_values, smooth_k)?
} else {
raw_k_values
};
// Calculate %D (SMA of %K)
let d_values = if k_values.len() >= d_period {
SMA::calculate_series(&k_values, d_period)?
} else {
vec![]
};
Ok((k_values, d_values))
}
}
impl Indicator for Stochastic {
fn calculate(&mut self, data: &PriceData) -> Result<IndicatorResult, IndicatorError> {
if data.high.len() != data.low.len() || data.high.len() != data.close.len() {
return Err(IndicatorError::InvalidParameter(
"Price data arrays must have the same length".to_string()
));
}
let (k_values, d_values) = Self::calculate_series(
&data.high,
&data.low,
&data.close,
self.k_period,
self.d_period,
self.smooth_k,
)?;
Ok(IndicatorResult::Stochastic {
k: k_values,
d: d_values,
})
}
fn reset(&mut self) {
self.high_window.clear();
self.low_window.clear();
self.close_window.clear();
self.k_sma.reset();
self.d_sma.reset();
self.k_values.clear();
}
fn is_ready(&self) -> bool {
self.high_window.is_full() && self.d_sma.is_ready()
}
}
impl IncrementalIndicator for Stochastic {
fn update(&mut self, value: f64) -> Result<Option<f64>, IndicatorError> {
// For incremental updates, we assume value is close price
// In real usage, you'd need to provide high/low/close separately
self.high_window.push(value);
self.low_window.push(value);
self.close_window.push(value);
if self.high_window.is_full() {
// Calculate raw %K
if let Some(raw_k) = Self::calculate_raw_k(
&self.high_window.as_slice(),
&self.low_window.as_slice(),
value
) {
// Smooth %K if needed
let k_value = if self.smooth_k > 1 {
self.k_sma.update(raw_k)?
} else {
Some(raw_k)
};
if let Some(k) = k_value {
self.k_values.push(k);
// Calculate %D
if let Some(d) = self.d_sma.update(k)? {
return Ok(Some(d));
}
}
}
}
Ok(None)
}
fn current(&self) -> Option<f64> {
self.d_sma.current()
}
}
/// Structure to hold Stochastic values
pub struct StochasticValues {
pub k: f64,
pub d: f64,
}
impl Stochastic {
/// Get current %K and %D values
pub fn current_values(&self) -> Option<StochasticValues> {
if let (Some(&k), Some(d)) = (self.k_values.iter().last(), self.d_sma.current()) {
Some(StochasticValues { k, d })
} else {
None
}
}
/// Update with separate high, low, close values
pub fn update_hlc(&mut self, high: f64, low: f64, close: f64) -> Result<Option<StochasticValues>, IndicatorError> {
self.high_window.push(high);
self.low_window.push(low);
self.close_window.push(close);
if self.high_window.is_full() {
if let Some(raw_k) = Self::calculate_raw_k(
&self.high_window.as_slice(),
&self.low_window.as_slice(),
close
) {
let k_value = if self.smooth_k > 1 {
self.k_sma.update(raw_k)?
} else {
Some(raw_k)
};
if let Some(k) = k_value {
self.k_values.push(k);
if let Some(d) = self.d_sma.update(k)? {
return Ok(Some(StochasticValues { k, d }));
}
}
}
}
Ok(None)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_stochastic_calculation() {
let high = vec![
127.01, 127.62, 126.59, 127.35, 128.17,
128.43, 127.37, 126.42, 126.90, 126.85,
125.65, 125.72, 127.16, 127.72, 127.69
];
let low = vec![
125.36, 126.16, 124.93, 126.09, 126.82,
126.48, 126.03, 124.83, 126.39, 125.72,
124.56, 124.57, 125.07, 126.86, 126.63
];
let close = vec![
125.36, 126.16, 124.93, 126.09, 126.82,
126.48, 126.03, 124.83, 126.39, 125.72,
124.56, 124.57, 125.07, 126.86, 126.63
];
let (k_values, d_values) = Stochastic::calculate_series(&high, &low, &close, 14, 3, 1).unwrap();
assert!(!k_values.is_empty());
assert!(!d_values.is_empty());
// %K should be between 0 and 100
for k in &k_values {
assert!(*k >= 0.0 && *k <= 100.0);
}
}
#[test]
fn test_stochastic_extremes() {
// When close is at highest high, %K should be 100
let high = vec![10.0; 14];
let low = vec![5.0; 14];
let mut close = vec![7.5; 14];
close[13] = 10.0; // Last close at high
let (k_values, _) = Stochastic::calculate_series(&high, &low, &close, 14, 3, 1).unwrap();
assert!((k_values[0] - 100.0).abs() < 1e-10);
// When close is at lowest low, %K should be 0
close[13] = 5.0; // Last close at low
let (k_values, _) = Stochastic::calculate_series(&high, &low, &close, 14, 3, 1).unwrap();
assert!(k_values[0].abs() < 1e-10);
}
}