work on new engine
This commit is contained in:
parent
44476da13f
commit
a1e5a21847
126 changed files with 3425 additions and 6695 deletions
250
apps/stock/engine/src/indicators/atr.rs
Normal file
250
apps/stock/engine/src/indicators/atr.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
256
apps/stock/engine/src/indicators/bollinger_bands.rs
Normal file
256
apps/stock/engine/src/indicators/bollinger_bands.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
142
apps/stock/engine/src/indicators/common.rs
Normal file
142
apps/stock/engine/src/indicators/common.rs
Normal 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();
|
||||
}
|
||||
}
|
||||
213
apps/stock/engine/src/indicators/ema.rs
Normal file
213
apps/stock/engine/src/indicators/ema.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
229
apps/stock/engine/src/indicators/macd.rs
Normal file
229
apps/stock/engine/src/indicators/macd.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
40
apps/stock/engine/src/indicators/mod.rs
Normal file
40
apps/stock/engine/src/indicators/mod.rs
Normal 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>;
|
||||
}
|
||||
223
apps/stock/engine/src/indicators/rsi.rs
Normal file
223
apps/stock/engine/src/indicators/rsi.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
139
apps/stock/engine/src/indicators/sma.rs
Normal file
139
apps/stock/engine/src/indicators/sma.rs
Normal 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));
|
||||
}
|
||||
}
|
||||
297
apps/stock/engine/src/indicators/stochastic.rs
Normal file
297
apps/stock/engine/src/indicators/stochastic.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue