use scirs2_core::ndarray::{s, Array1};
use scirs2_core::numeric::Float;
use crate::error::{Result, TimeSeriesError};
pub fn sma<F: Float + Clone>(data: &Array1<F>, window: usize) -> Result<Array1<F>> {
if window == 0 {
return Err(TimeSeriesError::InvalidInput(
"Window size must be positive".to_string(),
));
}
if data.len() < window {
return Err(TimeSeriesError::InsufficientData {
message: "Not enough data for SMA calculation".to_string(),
required: window,
actual: data.len(),
});
}
let mut result = Array1::zeros(data.len() - window + 1);
for i in 0..result.len() {
let sum = data.slice(s![i..i + window]).sum();
let window_f = F::from(window).expect("Failed to convert to float");
result[i] = sum / window_f;
}
Ok(result)
}
pub fn ema<F: Float + Clone>(data: &Array1<F>, alpha: F) -> Result<Array1<F>> {
if data.is_empty() {
return Err(TimeSeriesError::InvalidInput(
"Data cannot be empty".to_string(),
));
}
let zero = F::zero();
let one = F::one();
if alpha <= zero || alpha > one {
return Err(TimeSeriesError::InvalidParameter {
name: "alpha".to_string(),
message: "Alpha must be between 0 and 1".to_string(),
});
}
let mut result = Array1::zeros(data.len());
result[0] = data[0];
let one_minus_alpha = one - alpha;
for i in 1..data.len() {
result[i] = alpha * data[i] + one_minus_alpha * result[i - 1];
}
Ok(result)
}
pub fn bollinger_bands<F: Float + Clone>(
data: &Array1<F>,
window: usize,
num_std: F,
) -> Result<(Array1<F>, Array1<F>, Array1<F>)> {
let sma_values = sma(data, window)?;
let mut upper = Array1::zeros(sma_values.len());
let mut lower = Array1::zeros(sma_values.len());
for i in 0..sma_values.len() {
let slice = data.slice(s![i..i + window]);
let mean = sma_values[i];
let variance = slice
.mapv(|x| {
let diff = x - mean;
diff * diff
})
.sum()
/ F::from(window).expect("Failed to convert to float");
let std_dev = variance.sqrt();
upper[i] = mean + num_std * std_dev;
lower[i] = mean - num_std * std_dev;
}
Ok((upper, sma_values, lower))
}
pub fn rsi<F: Float + Clone>(data: &Array1<F>, period: usize) -> Result<Array1<F>> {
if period == 0 {
return Err(TimeSeriesError::InvalidInput(
"Period must be positive".to_string(),
));
}
if data.len() < period + 1 {
return Err(TimeSeriesError::InsufficientData {
message: "Not enough data for RSI calculation".to_string(),
required: period + 1,
actual: data.len(),
});
}
let mut changes = Array1::zeros(data.len() - 1);
for i in 0..changes.len() {
changes[i] = data[i + 1] - data[i];
}
let gains = changes.mapv(|x| if x > F::zero() { x } else { F::zero() });
let losses = changes.mapv(|x| if x < F::zero() { -x } else { F::zero() });
let avg_gain = sma(&gains, period)?;
let avg_loss = sma(&losses, period)?;
let mut rsi = Array1::zeros(avg_gain.len());
let hundred = F::from(100).expect("Failed to convert constant to float");
for i in 0..rsi.len() {
if avg_loss[i] == F::zero() {
rsi[i] = hundred;
} else {
let rs = avg_gain[i] / avg_loss[i];
rsi[i] = hundred - (hundred / (F::one() + rs));
}
}
Ok(rsi)
}
pub fn macd<F: Float + Clone>(
data: &Array1<F>,
fast_period: usize,
slow_period: usize,
signal_period: usize,
) -> Result<(Array1<F>, Array1<F>, Array1<F>)> {
if fast_period >= slow_period {
return Err(TimeSeriesError::InvalidInput(
"Fast period must be less than slow period".to_string(),
));
}
let fast_alpha = F::from(2.0).expect("Failed to convert constant to float")
/ F::from(fast_period + 1).expect("Failed to convert to float");
let slow_alpha = F::from(2.0).expect("Failed to convert constant to float")
/ F::from(slow_period + 1).expect("Failed to convert to float");
let signal_alpha = F::from(2.0).expect("Failed to convert constant to float")
/ F::from(signal_period + 1).expect("Failed to convert to float");
let fast_ema = ema(data, fast_alpha)?;
let slow_ema = ema(data, slow_alpha)?;
let macd_line = &fast_ema - &slow_ema;
let signal_line = ema(&macd_line, signal_alpha)?;
let histogram = &macd_line - &signal_line;
Ok((macd_line, signal_line, histogram))
}
pub fn stochastic<F: Float + Clone>(
high: &Array1<F>,
low: &Array1<F>,
close: &Array1<F>,
k_period: usize,
d_period: usize,
) -> Result<(Array1<F>, Array1<F>)> {
if high.len() != low.len() || low.len() != close.len() {
return Err(TimeSeriesError::DimensionMismatch {
expected: high.len(),
actual: close.len(),
});
}
if high.len() < k_period {
return Err(TimeSeriesError::InsufficientData {
message: "Not enough data for stochastic calculation".to_string(),
required: k_period,
actual: high.len(),
});
}
let mut k_percent = Array1::zeros(high.len() - k_period + 1);
let hundred = F::from(100).expect("Failed to convert constant to float");
for i in 0..k_percent.len() {
let period_high = high
.slice(s![i..i + k_period])
.iter()
.cloned()
.fold(F::neg_infinity(), F::max);
let period_low = low
.slice(s![i..i + k_period])
.iter()
.cloned()
.fold(F::infinity(), F::min);
let current_close = close[i + k_period - 1];
if period_high == period_low {
k_percent[i] = hundred;
} else {
k_percent[i] = hundred * (current_close - period_low) / (period_high - period_low);
}
}
let d_percent = sma(&k_percent, d_period)?;
Ok((k_percent, d_percent))
}
pub fn atr<F: Float + Clone>(
high: &Array1<F>,
low: &Array1<F>,
close: &Array1<F>,
period: usize,
) -> Result<Array1<F>> {
if high.len() != low.len() || low.len() != close.len() {
return Err(TimeSeriesError::DimensionMismatch {
expected: high.len(),
actual: close.len(),
});
}
if high.len() < period + 1 {
return Err(TimeSeriesError::InsufficientData {
message: "Not enough data for ATR calculation".to_string(),
required: period + 1,
actual: high.len(),
});
}
let mut true_ranges = Array1::zeros(high.len() - 1);
for i in 1..high.len() {
let tr1 = high[i] - low[i];
let tr2 = (high[i] - close[i - 1]).abs();
let tr3 = (low[i] - close[i - 1]).abs();
true_ranges[i - 1] = tr1.max(tr2).max(tr3);
}
sma(&true_ranges, period)
}
pub fn williams_r<F: Float + Clone>(
high: &Array1<F>,
low: &Array1<F>,
close: &Array1<F>,
period: usize,
) -> Result<Array1<F>> {
if high.len() != low.len() || low.len() != close.len() {
return Err(TimeSeriesError::DimensionMismatch {
expected: high.len(),
actual: close.len(),
});
}
if high.len() < period {
return Err(TimeSeriesError::InsufficientData {
message: "Not enough data for Williams %R calculation".to_string(),
required: period,
actual: high.len(),
});
}
let mut williams_r = Array1::zeros(high.len() - period + 1);
let hundred = F::from(100).expect("Failed to convert constant to float");
for i in 0..williams_r.len() {
let period_high = high
.slice(s![i..i + period])
.iter()
.cloned()
.fold(F::neg_infinity(), F::max);
let period_low = low
.slice(s![i..i + period])
.iter()
.cloned()
.fold(F::infinity(), F::min);
let current_close = close[i + period - 1];
if period_high == period_low {
williams_r[i] = F::zero();
} else {
williams_r[i] =
((period_high - current_close) / (period_high - period_low)) * (-hundred);
}
}
Ok(williams_r)
}
pub fn cci<F: Float + Clone>(
high: &Array1<F>,
low: &Array1<F>,
close: &Array1<F>,
period: usize,
) -> Result<Array1<F>> {
if high.len() != low.len() || low.len() != close.len() {
return Err(TimeSeriesError::DimensionMismatch {
expected: high.len(),
actual: close.len(),
});
}
if high.len() < period {
return Err(TimeSeriesError::InsufficientData {
message: "Not enough data for CCI calculation".to_string(),
required: period,
actual: high.len(),
});
}
let mut typical_price = Array1::zeros(high.len());
let three = F::from(3).expect("Failed to convert constant to float");
for i in 0..high.len() {
typical_price[i] = (high[i] + low[i] + close[i]) / three;
}
let sma_tp = sma(&typical_price, period)?;
let mut cci = Array1::zeros(sma_tp.len());
let constant = F::from(0.015).expect("Failed to convert constant to float");
for i in 0..cci.len() {
let slice = typical_price.slice(s![i..i + period]);
let mean = sma_tp[i];
let mean_deviation = slice.mapv(|x| (x - mean).abs()).sum()
/ F::from(period).expect("Failed to convert to float");
if mean_deviation != F::zero() {
cci[i] = (typical_price[i + period - 1] - mean) / (constant * mean_deviation);
}
}
Ok(cci)
}
pub fn obv<F: Float + Clone>(close: &Array1<F>, volume: &Array1<F>) -> Result<Array1<F>> {
if close.len() != volume.len() {
return Err(TimeSeriesError::DimensionMismatch {
expected: close.len(),
actual: volume.len(),
});
}
if close.len() < 2 {
return Err(TimeSeriesError::InsufficientData {
message: "Need at least 2 data points for OBV".to_string(),
required: 2,
actual: close.len(),
});
}
let mut obv = Array1::zeros(close.len());
obv[0] = volume[0];
for i in 1..close.len() {
if close[i] > close[i - 1] {
obv[i] = obv[i - 1] + volume[i];
} else if close[i] < close[i - 1] {
obv[i] = obv[i - 1] - volume[i];
} else {
obv[i] = obv[i - 1];
}
}
Ok(obv)
}
#[cfg(test)]
mod tests {
use super::*;
use scirs2_core::ndarray::arr1;
#[test]
fn test_sma() {
let data = arr1(&[1.0, 2.0, 3.0, 4.0, 5.0]);
let result = sma(&data, 3).expect("Operation failed");
let expected = arr1(&[2.0, 3.0, 4.0]);
for (actual, expected) in result.iter().zip(expected.iter()) {
assert!((actual - expected).abs() < 1e-10);
}
}
#[test]
fn test_ema() {
let data = arr1(&[1.0, 2.0, 3.0, 4.0, 5.0]);
let result = ema(&data, 0.5).expect("Operation failed");
assert_eq!(result[0], 1.0);
assert!((result[1] - 1.5).abs() < 1e-10); assert!(result.len() == data.len());
}
#[test]
fn test_rsi() {
let data = arr1(&[44.0, 44.25, 44.5, 43.75, 44.5, 45.0, 45.25, 45.5]);
let result = rsi(&data, 3);
assert!(result.is_ok());
let rsi_values = result.expect("Operation failed");
for &value in rsi_values.iter() {
assert!(value >= 0.0 && value <= 100.0);
}
}
#[test]
fn test_bollinger_bands() {
let data = arr1(&[20.0, 21.0, 19.5, 22.0, 21.5]);
let (upper, middle, lower) = bollinger_bands(&data, 3, 2.0).expect("Operation failed");
for i in 0..upper.len() {
assert!(upper[i] > middle[i]);
assert!(middle[i] > lower[i]);
}
}
#[test]
fn test_macd() {
let data = arr1(&[12.0, 13.0, 14.0, 13.5, 15.0, 16.0, 15.5, 17.0]);
let result = macd(&data, 3, 6, 2);
assert!(result.is_ok());
let (macd_line, signal_line, histogram) = result.expect("Operation failed");
assert_eq!(macd_line.len(), data.len());
assert_eq!(signal_line.len(), data.len());
assert_eq!(histogram.len(), data.len());
}
#[test]
fn test_atr() {
let high = arr1(&[15.0, 16.0, 14.5, 17.0, 16.5]);
let low = arr1(&[13.0, 14.0, 13.5, 15.0, 15.5]);
let close = arr1(&[14.5, 15.5, 14.0, 16.0, 16.0]);
let result = atr(&high, &low, &close, 3);
assert!(result.is_ok());
let atr_values = result.expect("Operation failed");
for &value in atr_values.iter() {
assert!(value >= 0.0);
}
}
#[test]
fn test_insufficient_data() {
let data = arr1(&[1.0, 2.0]);
let result = sma(&data, 5);
assert!(result.is_err());
}
#[test]
fn test_invalid_parameters() {
let data = arr1(&[1.0, 2.0, 3.0]);
let result = sma(&data, 0);
assert!(result.is_err());
let result = ema(&data, 1.5);
assert!(result.is_err());
}
}