use crate::strategy_lib::strategy::{Signal, Strategy, StrategyConfig, StrategyError};
use crate::trade_math::moving_averages::{ExponentialMovingAverage, SimpleMovingAverage};
use polars::prelude::*;
#[derive(Debug, Clone)]
pub struct MovingAverageCrossover {
config: StrategyConfig,
name: String,
description: String,
fast_period: usize,
slow_period: usize,
ma_type: String,
price_column: String,
}
impl MovingAverageCrossover {
pub fn sma_crossover(fast_period: usize, slow_period: usize) -> Self {
let config = StrategyConfig::new()
.with_parameter("fast_period", fast_period)
.with_parameter("slow_period", slow_period)
.with_parameter("ma_type", "sma")
.with_parameter("price_column", "close");
Self::new(config)
}
pub fn ema_crossover(fast_period: usize, slow_period: usize) -> Self {
let config = StrategyConfig::new()
.with_parameter("fast_period", fast_period)
.with_parameter("slow_period", slow_period)
.with_parameter("ma_type", "ema")
.with_parameter("price_column", "close");
Self::new(config)
}
fn calculate_sma(
&self,
prices: &[f64],
period: usize,
) -> Result<Vec<Option<f64>>, StrategyError> {
let mut sma = SimpleMovingAverage::new(period)
.map_err(|e| StrategyError::IndicatorError(e.to_string()))?;
let mut results = Vec::new();
for &price in prices {
sma.update(price)
.map_err(|e| StrategyError::IndicatorError(e.to_string()))?;
if sma.is_ready() {
let value = sma
.value()
.map_err(|e| StrategyError::IndicatorError(e.to_string()))?;
results.push(Some(value));
} else {
results.push(None);
}
}
Ok(results)
}
fn calculate_ema(
&self,
prices: &[f64],
period: usize,
) -> Result<Vec<Option<f64>>, StrategyError> {
let mut ema = ExponentialMovingAverage::new(period)
.map_err(|e| StrategyError::IndicatorError(e.to_string()))?;
let mut results = Vec::new();
for &price in prices {
ema.update(price)
.map_err(|e| StrategyError::IndicatorError(e.to_string()))?;
if ema.is_ready() {
let value = ema
.value()
.map_err(|e| StrategyError::IndicatorError(e.to_string()))?;
results.push(Some(value));
} else {
results.push(None);
}
}
Ok(results)
}
fn detect_crossovers(&self, fast_ma: &[Option<f64>], slow_ma: &[Option<f64>]) -> Vec<Signal> {
let mut signals = Vec::new();
let mut prev_fast: Option<f64> = None;
let mut prev_slow: Option<f64> = None;
for i in 0..fast_ma.len() {
let current_fast = fast_ma[i];
let current_slow = slow_ma[i];
let signal = if let (Some(cf), Some(cs), Some(pf), Some(ps)) =
(current_fast, current_slow, prev_fast, prev_slow)
{
if pf <= ps && cf > cs {
Signal::Buy
} else if pf >= ps && cf < cs {
Signal::Sell
} else {
Signal::Hold
}
} else {
Signal::Hold
};
signals.push(signal);
if current_fast.is_some() {
prev_fast = current_fast;
}
if current_slow.is_some() {
prev_slow = current_slow;
}
}
signals
}
}
impl Strategy for MovingAverageCrossover {
fn new(config: StrategyConfig) -> Self {
let fast_period = config.get_int("fast_period").unwrap_or(10) as usize;
let slow_period = config.get_int("slow_period").unwrap_or(30) as usize;
let ma_type = config.get_string("ma_type").unwrap_or("ema").to_string();
let price_column = config
.get_string("price_column")
.unwrap_or("close")
.to_string();
let name = format!(
"MA_Crossover_{}_{}_{}",
fast_period,
slow_period,
ma_type.to_uppercase()
);
let description = format!(
"Moving Average Crossover strategy using {} and {} period {}",
fast_period,
slow_period,
ma_type.to_uppercase()
);
Self {
config,
name,
description,
fast_period,
slow_period,
ma_type,
price_column,
}
}
fn generate_signals(&self, data: &DataFrame) -> Result<Series, StrategyError> {
self.validate_data(data)?;
let prices = data.column(&self.price_column)?.f64().map_err(|_| {
StrategyError::InvalidParameter("Cannot convert price column to f64".to_string())
})?;
let price_vec: Vec<f64> = prices
.into_iter()
.map(|opt_price| opt_price.unwrap_or(0.0))
.collect();
let fast_ma = match self.ma_type.as_str() {
"sma" => self.calculate_sma(&price_vec, self.fast_period)?,
"ema" => self.calculate_ema(&price_vec, self.fast_period)?,
_ => {
return Err(StrategyError::InvalidParameter(format!(
"Unknown MA type: {}",
self.ma_type
)))
}
};
let slow_ma = match self.ma_type.as_str() {
"sma" => self.calculate_sma(&price_vec, self.slow_period)?,
"ema" => self.calculate_ema(&price_vec, self.slow_period)?,
_ => {
return Err(StrategyError::InvalidParameter(format!(
"Unknown MA type: {}",
self.ma_type
)))
}
};
let signals = self.detect_crossovers(&fast_ma, &slow_ma);
let signal_ints: Vec<i32> = signals.iter().map(|s| s.to_int()).collect();
Ok(Series::new("signals".into(), signal_ints))
}
fn name(&self) -> &str {
&self.name
}
fn description(&self) -> &str {
&self.description
}
fn required_columns(&self) -> Vec<&str> {
vec![&self.price_column]
}
fn config(&self) -> &StrategyConfig {
&self.config
}
fn min_data_points(&self) -> usize {
self.slow_period + 10 }
}
#[cfg(test)]
mod tests {
use super::*;
fn create_test_data() -> DataFrame {
let close = Series::new(
"close".into(),
&[
100.0, 101.0, 102.0, 103.0, 104.0, 105.0, 106.0, 107.0, 108.0, 109.0, 110.0, 111.0,
112.0, 113.0, 114.0, 115.0, 116.0, 117.0, 118.0, 119.0, 120.0, 121.0, 122.0, 123.0,
124.0,
],
);
DataFrame::new(vec![close.into()]).unwrap()
}
#[test]
fn test_strategy_creation() {
let strategy = MovingAverageCrossover::sma_crossover(5, 10);
assert_eq!(strategy.name(), "MA_Crossover_5_10_SMA");
assert!(strategy.description().contains("5 and 10 period SMA"));
assert_eq!(strategy.required_columns(), vec!["close"]);
assert_eq!(strategy.min_data_points(), 20); }
#[test]
fn test_ema_strategy_creation() {
let strategy = MovingAverageCrossover::ema_crossover(8, 21);
assert_eq!(strategy.name(), "MA_Crossover_8_21_EMA");
assert!(strategy.description().contains("8 and 21 period EMA"));
}
#[test]
fn test_generate_signals() {
let df = create_test_data();
let strategy = MovingAverageCrossover::sma_crossover(5, 10);
let signals = strategy.generate_signals(&df).unwrap();
assert_eq!(signals.len(), df.height());
let signal_vals = signals.i32().unwrap();
for i in 0..signal_vals.len() {
let val = signal_vals.get(i).unwrap_or(0);
assert!(val >= 0 && val <= 2); }
}
#[test]
fn test_missing_price_column() {
let wrong_col = Series::new("wrong_col".into(), &[1.0, 2.0, 3.0]);
let df = DataFrame::new(vec![wrong_col.into()]).unwrap();
let strategy = MovingAverageCrossover::sma_crossover(5, 10);
let result = strategy.generate_signals(&df);
assert!(result.is_err());
match result.unwrap_err() {
StrategyError::MissingData(msg) => {
assert!(msg.contains("close"));
}
_ => panic!("Expected MissingData error"),
}
}
#[test]
fn test_insufficient_data() {
let config = StrategyConfig::new()
.with_parameter("fast_period", 5_i64)
.with_parameter("slow_period", 10_i64);
let strategy = MovingAverageCrossover::new(config);
let close = Series::new("close".into(), &[100.0, 101.0]); let df = DataFrame::new(vec![close.into()]).unwrap();
let result = strategy.generate_signals(&df);
assert!(result.is_err());
if let Err(StrategyError::ValidationError(msg)) = result {
assert!(msg.contains("Insufficient data"));
} else {
panic!("Expected ValidationError");
}
}
#[test]
fn test_config_access() {
let strategy = MovingAverageCrossover::sma_crossover(12, 26);
let config = strategy.config();
assert_eq!(config.get_int("fast_period").unwrap(), 12);
assert_eq!(config.get_int("slow_period").unwrap(), 26);
assert_eq!(config.get_string("ma_type").unwrap(), "sma");
}
}