use polars::prelude::*;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use thiserror::Error;
pub mod mean_reversion;
pub mod momentum;
pub mod multi_indicator;
pub mod trend_following;
pub mod volatility;
pub mod volume;
#[derive(Error, Debug)]
pub enum StrategyError {
#[error("Missing required data: {0}")]
MissingData(String),
#[error("Invalid parameter: {0}")]
InvalidParameter(String),
#[error("Polars error: {0}")]
PolarsError(#[from] PolarsError),
#[error("Indicator error: {0}")]
IndicatorError(String),
#[error("Configuration error: {0}")]
ConfigurationError(String),
#[error("Validation error: {0}")]
ValidationError(String),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum Signal {
Buy,
Sell,
Hold,
}
impl Signal {
pub fn to_int(&self) -> i32 {
match self {
Signal::Hold => 0,
Signal::Buy => 1,
Signal::Sell => 2,
}
}
pub fn from_int(value: i32) -> Self {
match value {
1 => Signal::Buy,
2 => Signal::Sell,
_ => Signal::Hold,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct StrategyConfig {
pub parameters: HashMap<String, ParameterValue>,
pub risk_settings: Option<RiskSettings>,
pub optimization: Option<OptimizationSettings>,
}
impl StrategyConfig {
pub fn new() -> Self {
Self::default()
}
pub fn with_parameter<T: Into<ParameterValue>>(mut self, key: &str, value: T) -> Self {
self.parameters.insert(key.to_string(), value.into());
self
}
pub fn get_parameter(&self, key: &str) -> Option<&ParameterValue> {
self.parameters.get(key)
}
pub fn get_int(&self, key: &str) -> Result<i64, StrategyError> {
match self.get_parameter(key) {
Some(ParameterValue::Integer(val)) => Ok(*val),
Some(_) => Err(StrategyError::InvalidParameter(format!(
"Parameter '{}' is not an integer",
key
))),
None => Err(StrategyError::MissingData(format!(
"Parameter '{}' not found",
key
))),
}
}
pub fn get_float(&self, key: &str) -> Result<f64, StrategyError> {
match self.get_parameter(key) {
Some(ParameterValue::Float(val)) => Ok(*val),
Some(ParameterValue::Integer(val)) => Ok(*val as f64),
Some(_) => Err(StrategyError::InvalidParameter(format!(
"Parameter '{}' is not a number",
key
))),
None => Err(StrategyError::MissingData(format!(
"Parameter '{}' not found",
key
))),
}
}
pub fn get_string(&self, key: &str) -> Result<&str, StrategyError> {
match self.get_parameter(key) {
Some(ParameterValue::String(val)) => Ok(val),
Some(_) => Err(StrategyError::InvalidParameter(format!(
"Parameter '{}' is not a string",
key
))),
None => Err(StrategyError::MissingData(format!(
"Parameter '{}' not found",
key
))),
}
}
pub fn get_bool(&self, key: &str) -> Result<bool, StrategyError> {
match self.get_parameter(key) {
Some(ParameterValue::Boolean(val)) => Ok(*val),
Some(_) => Err(StrategyError::InvalidParameter(format!(
"Parameter '{}' is not a boolean",
key
))),
None => Err(StrategyError::MissingData(format!(
"Parameter '{}' not found",
key
))),
}
}
pub fn validate(&self, required_params: &[&str]) -> Result<(), StrategyError> {
for ¶m in required_params {
if !self.parameters.contains_key(param) {
return Err(StrategyError::ValidationError(format!(
"Required parameter '{}' is missing",
param
)));
}
}
Ok(())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ParameterValue {
Integer(i64),
Float(f64),
String(String),
Boolean(bool),
Array(Vec<ParameterValue>),
}
impl From<i64> for ParameterValue {
fn from(value: i64) -> Self {
ParameterValue::Integer(value)
}
}
impl From<f64> for ParameterValue {
fn from(value: f64) -> Self {
ParameterValue::Float(value)
}
}
impl From<String> for ParameterValue {
fn from(value: String) -> Self {
ParameterValue::String(value)
}
}
impl From<&str> for ParameterValue {
fn from(value: &str) -> Self {
ParameterValue::String(value.to_string())
}
}
impl From<bool> for ParameterValue {
fn from(value: bool) -> Self {
ParameterValue::Boolean(value)
}
}
impl From<usize> for ParameterValue {
fn from(value: usize) -> Self {
ParameterValue::Integer(value as i64)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RiskSettings {
pub max_position_size: f64,
pub stop_loss: Option<f64>,
pub take_profit: Option<f64>,
pub max_positions: Option<usize>,
}
impl Default for RiskSettings {
fn default() -> Self {
Self {
max_position_size: 0.1, stop_loss: Some(0.05), take_profit: Some(0.15), max_positions: Some(1), }
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OptimizationSettings {
pub use_parallel: bool,
pub use_cache: bool,
pub batch_size: Option<usize>,
}
impl Default for OptimizationSettings {
fn default() -> Self {
Self {
use_parallel: true,
use_cache: true,
batch_size: Some(1000),
}
}
}
pub trait Strategy: Send + Sync {
fn new(config: StrategyConfig) -> Self
where
Self: Sized;
fn generate_signals(&self, data: &DataFrame) -> Result<Series, StrategyError>;
fn name(&self) -> &str;
fn description(&self) -> &str;
fn required_columns(&self) -> Vec<&str>;
fn config(&self) -> &StrategyConfig;
fn validate_data(&self, data: &DataFrame) -> Result<(), StrategyError> {
for &col in self.required_columns().iter() {
if data.column(col).is_err() {
return Err(StrategyError::MissingData(format!(
"Required column '{}' not found",
col
)));
}
}
if data.height() < self.min_data_points() {
return Err(StrategyError::ValidationError(format!(
"Insufficient data: need at least {} rows, got {}",
self.min_data_points(),
data.height()
)));
}
Ok(())
}
fn min_data_points(&self) -> usize {
50 }
fn supports_realtime(&self) -> bool {
true
}
fn metadata(&self) -> StrategyMetadata {
StrategyMetadata {
name: self.name().to_string(),
description: self.description().to_string(),
required_columns: self
.required_columns()
.iter()
.map(|s| s.to_string())
.collect(),
min_data_points: self.min_data_points(),
supports_realtime: self.supports_realtime(),
version: "1.0.0".to_string(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StrategyMetadata {
pub name: String,
pub description: String,
pub required_columns: Vec<String>,
pub min_data_points: usize,
pub supports_realtime: bool,
pub version: String,
}
pub mod utils {
use super::*;
pub fn signals_to_int_series(signals: &[Signal]) -> Series {
let int_signals: Vec<i32> = signals.iter().map(|s| s.to_int()).collect();
Series::new("signals".into(), int_signals)
}
pub fn int_series_to_signals(series: &Series) -> Result<Vec<Signal>, StrategyError> {
let int_values = series.i32().map_err(|_| {
StrategyError::InvalidParameter("Cannot convert series to integers".to_string())
})?;
Ok(int_values
.into_iter()
.map(|opt_val| Signal::from_int(opt_val.unwrap_or(0)))
.collect())
}
pub fn create_ma_config(fast_period: usize, slow_period: usize) -> StrategyConfig {
StrategyConfig::new()
.with_parameter("fast_period", fast_period)
.with_parameter("slow_period", slow_period)
}
pub fn create_rsi_config(period: usize, oversold: f64, overbought: f64) -> StrategyConfig {
StrategyConfig::new()
.with_parameter("period", period)
.with_parameter("oversold_threshold", oversold)
.with_parameter("overbought_threshold", overbought)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_signal_conversion() {
assert_eq!(Signal::Buy.to_int(), 1);
assert_eq!(Signal::Sell.to_int(), 2);
assert_eq!(Signal::Hold.to_int(), 0);
assert_eq!(Signal::from_int(1), Signal::Buy);
assert_eq!(Signal::from_int(2), Signal::Sell);
assert_eq!(Signal::from_int(0), Signal::Hold);
assert_eq!(Signal::from_int(99), Signal::Hold); }
#[test]
fn test_strategy_config() {
let config = StrategyConfig::new()
.with_parameter("period", 20_i64)
.with_parameter("threshold", 0.5)
.with_parameter("enabled", true)
.with_parameter("name", "test_strategy");
assert_eq!(config.get_int("period").unwrap(), 20);
assert_eq!(config.get_float("threshold").unwrap(), 0.5);
assert_eq!(config.get_bool("enabled").unwrap(), true);
assert_eq!(config.get_string("name").unwrap(), "test_strategy");
}
#[test]
fn test_config_validation() {
let config = StrategyConfig::new().with_parameter("period", 20_i64);
assert!(config.validate(&["period"]).is_ok());
assert!(config.validate(&["period", "missing_param"]).is_err());
}
}