use crate::strategy_lib::strategy::{Signal, Strategy, StrategyConfig, StrategyError};
use polars::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MovingAverageCrossoverConfig {
pub fast_period: u32,
pub slow_period: u32,
pub ma_type: String,
pub price_col: String,
}
impl Default for MovingAverageCrossoverConfig {
fn default() -> Self {
Self {
fast_period: 10,
slow_period: 30,
ma_type: "ema".to_string(),
price_col: "close".to_string(),
}
}
}
#[derive(Debug)]
pub struct MovingAverageCrossover {
config: MovingAverageCrossoverConfig,
name: String,
description: String,
}
impl Strategy for MovingAverageCrossover {
fn new(_config: StrategyConfig) -> Self {
let strategy_config = MovingAverageCrossoverConfig::default();
let name = format!(
"MA_Crossover_{}_{}",
strategy_config.fast_period, strategy_config.slow_period
);
let description = format!(
"Moving Average Crossover strategy using {} and {} period {}",
strategy_config.fast_period,
strategy_config.slow_period,
strategy_config.ma_type.to_uppercase()
);
Self {
config: strategy_config,
name,
description,
}
}
fn generate_signals(&self, data: &DataFrame) -> Result<Series, StrategyError> {
if data.column(&self.config.price_col).is_err() {
return Err(StrategyError::MissingData(format!(
"Price column '{}' not found in data",
self.config.price_col
)));
}
let length = data.height();
let signals = vec![Signal::Hold as i32; length];
let signal_series = Series::new("signal".into(), signals);
Ok(signal_series)
}
fn name(&self) -> &str {
&self.name
}
fn description(&self) -> &str {
&self.description
}
fn required_columns(&self) -> Vec<&str> {
vec![&self.config.price_col]
}
}
#[cfg(test)]
mod tests {
use super::*;
fn create_test_data() -> DataFrame {
let close = Series::new("close".into(), &[100.0, 101.0, 99.0]);
DataFrame::new(vec![close.into()]).unwrap()
}
#[test]
fn test_strategy_creation() {
let config = StrategyConfig {
parameters: Series::new("params".into(), [0i32]), };
let strategy = MovingAverageCrossover::new(config);
assert_eq!(strategy.name(), "MA_Crossover_10_30");
assert_eq!(
strategy.description(),
"Moving Average Crossover strategy using 10 and 30 period EMA"
);
assert_eq!(strategy.required_columns(), vec!["close"]);
}
#[test]
fn test_generate_signals() {
let df = create_test_data();
let config = StrategyConfig {
parameters: Series::new("params".into(), [0i32]), };
let strategy = MovingAverageCrossover::new(config);
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() {
assert_eq!(signal_vals.get(i), Some(Signal::Hold as i32));
}
}
#[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 config = StrategyConfig {
parameters: Series::new("params".into(), [0i32]), };
let strategy = MovingAverageCrossover::new(config);
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"),
}
}
}