feat(trading-agent): implement strategy coordination module (Wave 12.2.2)

Implements strategy registration and lifecycle management with TDD approach.

**Implementation**:
- StrategyCoordinator: Manages strategy configuration and status
- StrategyConfig: Strategy metadata with JSONB parameters
- Strategy types: Equal Weight, Risk Parity, ML Optimized, Mean Variance, Momentum, Mean Reversion
- Status management: Active, Paused, Stopped
- Database persistence with PostgreSQL + JSONB

**Database**:
- Migration 041: strategy_configs table
- UUID primary keys, unique strategy names
- JSONB parameters for flexible configuration
- Trigger for automatic updated_at timestamps

**Tests** (14/14 passing):
- Strategy registration with validation
- Duplicate name prevention
- List/filter strategies (all, active only)
- Status updates with validation
- Performance benchmarks (<50ms per operation)
- All 6 strategy types supported
- Empty and complex parameters

**Performance**:
- Registration: <50ms
- List: <50ms
- Update: <50ms
- All operations meet <50ms target

**Production Ready**:
- Proper error handling with thiserror
- Tracing instrumentation
- NO stubs or placeholders
- Real PostgreSQL integration

Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
jgrusewski
2025-10-16 07:49:04 +02:00
parent 27dad268db
commit 54c6756345
10 changed files with 1177 additions and 4 deletions

View File

@@ -0,0 +1,39 @@
-- Create strategy_configs table for trading agent service
-- Manages strategy registration, configuration, and lifecycle
CREATE TABLE IF NOT EXISTS strategy_configs (
strategy_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
strategy_name TEXT NOT NULL,
strategy_type TEXT NOT NULL CHECK (strategy_type IN ('EqualWeight', 'RiskParity', 'MLOptimized', 'MeanVariance', 'Momentum', 'MeanReversion')),
parameters JSONB NOT NULL DEFAULT '{}',
status TEXT NOT NULL DEFAULT 'Active' CHECK (status IN ('Active', 'Paused', 'Stopped')),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE(strategy_name)
);
-- Indexes for performance
CREATE INDEX IF NOT EXISTS idx_strategy_configs_type ON strategy_configs(strategy_type);
CREATE INDEX IF NOT EXISTS idx_strategy_configs_status ON strategy_configs(status);
CREATE INDEX IF NOT EXISTS idx_strategy_configs_created ON strategy_configs(created_at DESC);
CREATE INDEX IF NOT EXISTS idx_strategy_configs_parameters ON strategy_configs USING GIN (parameters);
-- Updated timestamp trigger
CREATE OR REPLACE FUNCTION update_strategy_configs_updated_at()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trigger_update_strategy_configs_updated_at
BEFORE UPDATE ON strategy_configs
FOR EACH ROW
EXECUTE FUNCTION update_strategy_configs_updated_at();
-- Comments
COMMENT ON TABLE strategy_configs IS 'Strategy configuration and lifecycle management for trading agent';
COMMENT ON COLUMN strategy_configs.strategy_type IS 'Type of allocation strategy (EqualWeight, RiskParity, MLOptimized, MeanVariance, Momentum, MeanReversion)';
COMMENT ON COLUMN strategy_configs.parameters IS 'JSONB configuration parameters specific to strategy type';
COMMENT ON COLUMN strategy_configs.status IS 'Strategy status (Active, Paused, Stopped)';

View File

@@ -0,0 +1,58 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n strategy_id,\n strategy_name,\n strategy_type,\n parameters,\n status,\n created_at,\n updated_at\n FROM strategy_configs\n WHERE strategy_id = $1\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "strategy_id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "strategy_name",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "strategy_type",
"type_info": "Text"
},
{
"ordinal": 3,
"name": "parameters",
"type_info": "Jsonb"
},
{
"ordinal": 4,
"name": "status",
"type_info": "Text"
},
{
"ordinal": 5,
"name": "created_at",
"type_info": "Timestamptz"
},
{
"ordinal": 6,
"name": "updated_at",
"type_info": "Timestamptz"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
false,
false,
false,
false,
false,
false,
false
]
},
"hash": "2a88bd43a5df2a9f9c5bbcfadf6c869f0d273f8063411e49f6691c4d20655a14"
}

View File

@@ -0,0 +1,26 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO agent_orders (\n order_id, allocation_id, symbol, side, quantity, price,\n order_type, status, time_in_force, filled_quantity,\n client_order_id, created_at, metadata\n )\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Text",
"Text",
"Text",
"Numeric",
"Numeric",
"Text",
"Text",
"Text",
"Numeric",
"Text",
"Timestamptz",
"Jsonb"
]
},
"nullable": []
},
"hash": "91de6a60159626c5cb3ee1c3d8c66c0e2ec33c32bdc6510c235931b3cd509f2d"
}

View File

@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE strategy_configs\n SET status = $1\n WHERE strategy_id = $2\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Uuid"
]
},
"nullable": []
},
"hash": "e88a192d1ad771838a473e16a677146f3fef9346ea88d294a97352282384fd5c"
}

View File

@@ -0,0 +1,56 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n strategy_id,\n strategy_name,\n strategy_type,\n parameters,\n status,\n created_at,\n updated_at\n FROM strategy_configs\n ORDER BY created_at DESC\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "strategy_id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "strategy_name",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "strategy_type",
"type_info": "Text"
},
{
"ordinal": 3,
"name": "parameters",
"type_info": "Jsonb"
},
{
"ordinal": 4,
"name": "status",
"type_info": "Text"
},
{
"ordinal": 5,
"name": "created_at",
"type_info": "Timestamptz"
},
{
"ordinal": 6,
"name": "updated_at",
"type_info": "Timestamptz"
}
],
"parameters": {
"Left": []
},
"nullable": [
false,
false,
false,
false,
false,
false,
false
]
},
"hash": "f218fc76780ca39d146674eae4bab7707bedc41d641545d8ea96df69ee5a36e9"
}

View File

@@ -0,0 +1,56 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n strategy_id,\n strategy_name,\n strategy_type,\n parameters,\n status,\n created_at,\n updated_at\n FROM strategy_configs\n WHERE status = 'Active'\n ORDER BY created_at DESC\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "strategy_id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "strategy_name",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "strategy_type",
"type_info": "Text"
},
{
"ordinal": 3,
"name": "parameters",
"type_info": "Jsonb"
},
{
"ordinal": 4,
"name": "status",
"type_info": "Text"
},
{
"ordinal": 5,
"name": "created_at",
"type_info": "Timestamptz"
},
{
"ordinal": 6,
"name": "updated_at",
"type_info": "Timestamptz"
}
],
"parameters": {
"Left": []
},
"nullable": [
false,
false,
false,
false,
false,
false,
false
]
},
"hash": "f63e4cd5aff2bf33961383e53c5bfb559b5a3fcf75ee897e38ca024bdf1ce745"
}

View File

@@ -0,0 +1,25 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO strategy_configs (strategy_name, strategy_type, parameters, status)\n VALUES ($1, $2, $3, $4)\n RETURNING strategy_id\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "strategy_id",
"type_info": "Uuid"
}
],
"parameters": {
"Left": [
"Text",
"Text",
"Jsonb",
"Text"
]
},
"nullable": [
false
]
},
"hash": "f6e6de13c5202107e4d26e19af7a80dd5186eac2ac77e0a6cb7edd4ea0e792d3"
}

View File

@@ -11,8 +11,8 @@ pub mod proto {
pub mod service;
pub mod universe;
pub mod orders;
pub mod strategies;
pub mod monitoring;
// pub mod assets; // TODO: Implement in Phase 2
// pub mod allocation; // TODO: Implement in Phase 3
// pub mod orders; // TODO: Implement in Phase 4
// pub mod strategies; // TODO: Implement in Phase 5
// pub mod monitoring; // TODO: Implement in Phase 6

View File

@@ -2,4 +2,455 @@
//!
//! Manages multiple trading strategies and their lifecycle.
// Stub implementation - to be filled in future agents
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use sqlx::PgPool;
use std::collections::HashMap;
use thiserror::Error;
use tracing::{debug, error, info, instrument, warn};
/// Strategy coordinator for managing strategy lifecycle
#[derive(Debug, Clone)]
pub struct StrategyCoordinator {
pool: PgPool,
}
/// Strategy types supported by the trading agent
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::Type)]
#[sqlx(type_name = "text")]
pub enum StrategyType {
EqualWeight,
RiskParity,
MLOptimized,
MeanVariance,
Momentum,
MeanReversion,
}
impl std::fmt::Display for StrategyType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
StrategyType::EqualWeight => write!(f, "EqualWeight"),
StrategyType::RiskParity => write!(f, "RiskParity"),
StrategyType::MLOptimized => write!(f, "MLOptimized"),
StrategyType::MeanVariance => write!(f, "MeanVariance"),
StrategyType::Momentum => write!(f, "Momentum"),
StrategyType::MeanReversion => write!(f, "MeanReversion"),
}
}
}
impl std::str::FromStr for StrategyType {
type Err = StrategyError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"EqualWeight" => Ok(StrategyType::EqualWeight),
"RiskParity" => Ok(StrategyType::RiskParity),
"MLOptimized" => Ok(StrategyType::MLOptimized),
"MeanVariance" => Ok(StrategyType::MeanVariance),
"Momentum" => Ok(StrategyType::Momentum),
"MeanReversion" => Ok(StrategyType::MeanReversion),
_ => Err(StrategyError::InvalidStrategyType(s.to_string())),
}
}
}
/// Strategy status
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::Type)]
#[sqlx(type_name = "text")]
pub enum StrategyStatus {
Active,
Paused,
Stopped,
}
impl std::fmt::Display for StrategyStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
StrategyStatus::Active => write!(f, "Active"),
StrategyStatus::Paused => write!(f, "Paused"),
StrategyStatus::Stopped => write!(f, "Stopped"),
}
}
}
impl std::str::FromStr for StrategyStatus {
type Err = StrategyError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"Active" => Ok(StrategyStatus::Active),
"Paused" => Ok(StrategyStatus::Paused),
"Stopped" => Ok(StrategyStatus::Stopped),
_ => Err(StrategyError::InvalidStatus(s.to_string())),
}
}
}
/// Strategy configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StrategyConfig {
pub strategy_id: String,
pub strategy_name: String,
pub strategy_type: StrategyType,
pub parameters: HashMap<String, f64>,
pub status: StrategyStatus,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
/// Strategy-related errors
#[derive(Debug, Error)]
pub enum StrategyError {
#[error("Database error: {0}")]
Database(#[from] sqlx::Error),
#[error("Strategy not found: {0}")]
NotFound(String),
#[error("Invalid strategy type: {0}")]
InvalidStrategyType(String),
#[error("Invalid status: {0}")]
InvalidStatus(String),
#[error("Strategy name already exists: {0}")]
DuplicateName(String),
#[error("JSON serialization error: {0}")]
JsonError(#[from] serde_json::Error),
}
impl StrategyCoordinator {
/// Create a new strategy coordinator
pub fn new(pool: PgPool) -> Self {
Self { pool }
}
/// Register a new strategy
#[instrument(skip(self, config), fields(strategy_name = %config.strategy_name, strategy_type = ?config.strategy_type))]
pub async fn register_strategy(&self, config: StrategyConfig) -> Result<String, StrategyError> {
info!(
strategy_name = %config.strategy_name,
strategy_type = ?config.strategy_type,
"Registering new strategy"
);
// Serialize parameters to JSONB
let parameters_json = serde_json::to_value(&config.parameters)?;
// Insert strategy into database
let result = sqlx::query!(
r#"
INSERT INTO strategy_configs (strategy_name, strategy_type, parameters, status)
VALUES ($1, $2, $3, $4)
RETURNING strategy_id
"#,
config.strategy_name,
config.strategy_type.to_string(),
parameters_json,
config.status.to_string()
)
.fetch_one(&self.pool)
.await
.map_err(|e| {
if let sqlx::Error::Database(ref db_err) = e {
if db_err.is_unique_violation() {
error!(
strategy_name = %config.strategy_name,
"Strategy name already exists"
);
return StrategyError::DuplicateName(config.strategy_name.clone());
}
}
error!(error = ?e, "Failed to register strategy");
StrategyError::Database(e)
})?;
let strategy_id = result.strategy_id.to_string();
info!(
strategy_id = %strategy_id,
strategy_name = %config.strategy_name,
"Strategy registered successfully"
);
Ok(strategy_id)
}
/// List all strategies
#[instrument(skip(self))]
pub async fn list_strategies(&self) -> Result<Vec<StrategyConfig>, StrategyError> {
debug!("Listing all strategies");
let rows = sqlx::query!(
r#"
SELECT
strategy_id,
strategy_name,
strategy_type,
parameters,
status,
created_at,
updated_at
FROM strategy_configs
ORDER BY created_at DESC
"#
)
.fetch_all(&self.pool)
.await
.map_err(|e| {
error!(error = ?e, "Failed to list strategies");
StrategyError::Database(e)
})?;
let strategies: Vec<StrategyConfig> = rows
.into_iter()
.map(|row| {
let parameters: HashMap<String, f64> =
serde_json::from_value(row.parameters).unwrap_or_default();
let strategy_type = row.strategy_type.parse().unwrap_or_else(|e| {
warn!(
error = ?e,
strategy_type = %row.strategy_type,
"Failed to parse strategy type, defaulting to EqualWeight"
);
StrategyType::EqualWeight
});
let status = row.status.parse().unwrap_or_else(|e| {
warn!(
error = ?e,
status = %row.status,
"Failed to parse status, defaulting to Active"
);
StrategyStatus::Active
});
StrategyConfig {
strategy_id: row.strategy_id.to_string(),
strategy_name: row.strategy_name,
strategy_type,
parameters,
status,
created_at: row.created_at,
updated_at: row.updated_at,
}
})
.collect();
debug!(count = strategies.len(), "Retrieved strategies");
Ok(strategies)
}
/// Update strategy status
#[instrument(skip(self), fields(strategy_id = %strategy_id, new_status = ?status))]
pub async fn update_status(
&self,
strategy_id: &str,
status: StrategyStatus,
) -> Result<(), StrategyError> {
info!(
strategy_id = %strategy_id,
new_status = ?status,
"Updating strategy status"
);
// Parse UUID
let uuid = uuid::Uuid::parse_str(strategy_id).map_err(|e| {
error!(
strategy_id = %strategy_id,
error = ?e,
"Invalid strategy ID format"
);
StrategyError::NotFound(strategy_id.to_string())
})?;
let result = sqlx::query!(
r#"
UPDATE strategy_configs
SET status = $1
WHERE strategy_id = $2
"#,
status.to_string(),
uuid
)
.execute(&self.pool)
.await
.map_err(|e| {
error!(error = ?e, "Failed to update strategy status");
StrategyError::Database(e)
})?;
if result.rows_affected() == 0 {
error!(
strategy_id = %strategy_id,
"Strategy not found for status update"
);
return Err(StrategyError::NotFound(strategy_id.to_string()));
}
info!(
strategy_id = %strategy_id,
new_status = ?status,
"Strategy status updated successfully"
);
Ok(())
}
/// Get active strategies only
#[instrument(skip(self))]
pub async fn get_active_strategies(&self) -> Result<Vec<StrategyConfig>, StrategyError> {
debug!("Fetching active strategies");
let rows = sqlx::query!(
r#"
SELECT
strategy_id,
strategy_name,
strategy_type,
parameters,
status,
created_at,
updated_at
FROM strategy_configs
WHERE status = 'Active'
ORDER BY created_at DESC
"#
)
.fetch_all(&self.pool)
.await
.map_err(|e| {
error!(error = ?e, "Failed to fetch active strategies");
StrategyError::Database(e)
})?;
let strategies: Vec<StrategyConfig> = rows
.into_iter()
.map(|row| {
let parameters: HashMap<String, f64> =
serde_json::from_value(row.parameters).unwrap_or_default();
let strategy_type = row.strategy_type.parse().unwrap_or(StrategyType::EqualWeight);
let status = row.status.parse().unwrap_or(StrategyStatus::Active);
StrategyConfig {
strategy_id: row.strategy_id.to_string(),
strategy_name: row.strategy_name,
strategy_type,
parameters,
status,
created_at: row.created_at,
updated_at: row.updated_at,
}
})
.collect();
debug!(count = strategies.len(), "Retrieved active strategies");
Ok(strategies)
}
/// Get strategy by ID
#[instrument(skip(self), fields(strategy_id = %strategy_id))]
pub async fn get_strategy(&self, strategy_id: &str) -> Result<StrategyConfig, StrategyError> {
debug!(strategy_id = %strategy_id, "Fetching strategy by ID");
// Parse UUID
let uuid = uuid::Uuid::parse_str(strategy_id).map_err(|e| {
error!(
strategy_id = %strategy_id,
error = ?e,
"Invalid strategy ID format"
);
StrategyError::NotFound(strategy_id.to_string())
})?;
let row = sqlx::query!(
r#"
SELECT
strategy_id,
strategy_name,
strategy_type,
parameters,
status,
created_at,
updated_at
FROM strategy_configs
WHERE strategy_id = $1
"#,
uuid
)
.fetch_optional(&self.pool)
.await
.map_err(|e| {
error!(error = ?e, "Failed to fetch strategy");
StrategyError::Database(e)
})?;
match row {
Some(row) => {
let parameters: HashMap<String, f64> =
serde_json::from_value(row.parameters).unwrap_or_default();
let strategy_type = row.strategy_type.parse().unwrap_or(StrategyType::EqualWeight);
let status = row.status.parse().unwrap_or(StrategyStatus::Active);
let config = StrategyConfig {
strategy_id: row.strategy_id.to_string(),
strategy_name: row.strategy_name,
strategy_type,
parameters,
status,
created_at: row.created_at,
updated_at: row.updated_at,
};
debug!(strategy_id = %strategy_id, "Strategy retrieved successfully");
Ok(config)
}
None => {
error!(strategy_id = %strategy_id, "Strategy not found");
Err(StrategyError::NotFound(strategy_id.to_string()))
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_strategy_type_display() {
assert_eq!(StrategyType::EqualWeight.to_string(), "EqualWeight");
assert_eq!(StrategyType::RiskParity.to_string(), "RiskParity");
assert_eq!(StrategyType::MLOptimized.to_string(), "MLOptimized");
}
#[test]
fn test_strategy_type_from_str() {
assert_eq!("EqualWeight".parse::<StrategyType>().unwrap(), StrategyType::EqualWeight);
assert_eq!("RiskParity".parse::<StrategyType>().unwrap(), StrategyType::RiskParity);
assert!("Invalid".parse::<StrategyType>().is_err());
}
#[test]
fn test_strategy_status_display() {
assert_eq!(StrategyStatus::Active.to_string(), "Active");
assert_eq!(StrategyStatus::Paused.to_string(), "Paused");
assert_eq!(StrategyStatus::Stopped.to_string(), "Stopped");
}
#[test]
fn test_strategy_status_from_str() {
assert_eq!("Active".parse::<StrategyStatus>().unwrap(), StrategyStatus::Active);
assert_eq!("Paused".parse::<StrategyStatus>().unwrap(), StrategyStatus::Paused);
assert!("Invalid".parse::<StrategyStatus>().is_err());
}
}

View File

@@ -0,0 +1,447 @@
//! Integration tests for strategy coordination module
use trading_agent_service::strategies::{
StrategyCoordinator, StrategyConfig, StrategyType, StrategyStatus, StrategyError,
};
use std::collections::HashMap;
fn get_test_database_url() -> String {
std::env::var("DATABASE_URL")
.unwrap_or_else(|_| "postgresql://foxhunt:foxhunt_dev_password@localhost:5432/foxhunt".to_string())
}
async fn setup_test_db() -> sqlx::PgPool {
let pool = sqlx::PgPool::connect(&get_test_database_url())
.await
.expect("Failed to connect to database");
// Run migrations
sqlx::migrate!("../../migrations")
.run(&pool)
.await
.expect("Failed to run migrations");
pool
}
#[tokio::test]
async fn test_register_strategy() {
let pool = setup_test_db().await;
let coordinator = StrategyCoordinator::new(pool);
// Create a strategy config
let mut parameters = HashMap::new();
parameters.insert("risk_limit".to_string(), 0.05);
parameters.insert("rebalance_threshold".to_string(), 0.02);
let config = StrategyConfig {
strategy_id: String::new(), // Will be generated
strategy_name: "test_equal_weight_001".to_string(),
strategy_type: StrategyType::EqualWeight,
parameters,
status: StrategyStatus::Active,
created_at: chrono::Utc::now(),
updated_at: chrono::Utc::now(),
};
let result = coordinator.register_strategy(config).await;
assert!(result.is_ok(), "Strategy registration should succeed");
let strategy_id = result.expect("Strategy ID should be returned");
assert!(!strategy_id.is_empty(), "Strategy ID should not be empty");
// Verify it's a valid UUID
assert!(uuid::Uuid::parse_str(&strategy_id).is_ok(), "Strategy ID should be valid UUID");
}
#[tokio::test]
async fn test_register_duplicate_strategy_name() {
let pool = setup_test_db().await;
let coordinator = StrategyCoordinator::new(pool);
let mut parameters = HashMap::new();
parameters.insert("risk_limit".to_string(), 0.05);
let config1 = StrategyConfig {
strategy_id: String::new(),
strategy_name: "duplicate_test_strategy".to_string(),
strategy_type: StrategyType::RiskParity,
parameters: parameters.clone(),
status: StrategyStatus::Active,
created_at: chrono::Utc::now(),
updated_at: chrono::Utc::now(),
};
// Register first strategy
let result1 = coordinator.register_strategy(config1).await;
assert!(result1.is_ok());
// Try to register duplicate name
let config2 = StrategyConfig {
strategy_id: String::new(),
strategy_name: "duplicate_test_strategy".to_string(),
strategy_type: StrategyType::MLOptimized,
parameters,
status: StrategyStatus::Active,
created_at: chrono::Utc::now(),
updated_at: chrono::Utc::now(),
};
let result2 = coordinator.register_strategy(config2).await;
assert!(result2.is_err(), "Duplicate strategy name should fail");
}
#[tokio::test]
async fn test_list_strategies() {
let pool = setup_test_db().await;
let coordinator = StrategyCoordinator::new(pool);
// Register multiple strategies
for i in 0..3 {
let mut parameters = HashMap::new();
parameters.insert("risk_limit".to_string(), 0.05);
let config = StrategyConfig {
strategy_id: String::new(),
strategy_name: format!("test_strategy_list_{}", i),
strategy_type: StrategyType::EqualWeight,
parameters,
status: StrategyStatus::Active,
created_at: chrono::Utc::now(),
updated_at: chrono::Utc::now(),
};
coordinator.register_strategy(config).await.expect("Failed to register strategy");
}
// List strategies
let result = coordinator.list_strategies().await;
assert!(result.is_ok(), "List strategies should succeed");
let strategies = result.expect("Strategies list should be returned");
assert!(strategies.len() >= 3, "Should have at least 3 strategies");
// Verify all strategies have required fields
for strategy in &strategies {
assert!(!strategy.strategy_id.is_empty());
assert!(!strategy.strategy_name.is_empty());
}
}
#[tokio::test]
async fn test_update_strategy_status() {
let pool = setup_test_db().await;
let coordinator = StrategyCoordinator::new(pool);
// Register a strategy
let mut parameters = HashMap::new();
parameters.insert("risk_limit".to_string(), 0.05);
let config = StrategyConfig {
strategy_id: String::new(),
strategy_name: "test_status_update".to_string(),
strategy_type: StrategyType::Momentum,
parameters,
status: StrategyStatus::Active,
created_at: chrono::Utc::now(),
updated_at: chrono::Utc::now(),
};
let strategy_id = coordinator.register_strategy(config).await.expect("Failed to register");
// Update status to Paused
let result = coordinator.update_status(&strategy_id, StrategyStatus::Paused).await;
assert!(result.is_ok(), "Status update should succeed");
// Verify status changed
let strategies = coordinator.list_strategies().await.expect("Failed to list");
let updated_strategy = strategies.iter().find(|s| s.strategy_id == strategy_id);
assert!(updated_strategy.is_some());
assert_eq!(updated_strategy.unwrap().status, StrategyStatus::Paused);
}
#[tokio::test]
async fn test_update_nonexistent_strategy_status() {
let pool = setup_test_db().await;
let coordinator = StrategyCoordinator::new(pool);
// Try to update non-existent strategy
let fake_id = uuid::Uuid::new_v4().to_string();
let result = coordinator.update_status(&fake_id, StrategyStatus::Stopped).await;
assert!(result.is_err(), "Updating non-existent strategy should fail");
}
#[tokio::test]
async fn test_get_active_strategies() {
let pool = setup_test_db().await;
let coordinator = StrategyCoordinator::new(pool);
// Register strategies with different statuses
let statuses = vec![
StrategyStatus::Active,
StrategyStatus::Paused,
StrategyStatus::Active,
StrategyStatus::Stopped,
StrategyStatus::Active,
];
for (i, status) in statuses.iter().enumerate() {
let mut parameters = HashMap::new();
parameters.insert("risk_limit".to_string(), 0.05);
let config = StrategyConfig {
strategy_id: String::new(),
strategy_name: format!("test_active_filter_{}", i),
strategy_type: StrategyType::EqualWeight,
parameters,
status: status.clone(),
created_at: chrono::Utc::now(),
updated_at: chrono::Utc::now(),
};
coordinator.register_strategy(config).await.expect("Failed to register");
}
// Get only active strategies
let result = coordinator.get_active_strategies().await;
assert!(result.is_ok(), "Get active strategies should succeed");
let active_strategies = result.expect("Active strategies should be returned");
// Should have at least 3 active strategies from this test
let test_active_count = active_strategies
.iter()
.filter(|s| s.strategy_name.starts_with("test_active_filter_"))
.count();
assert_eq!(test_active_count, 3, "Should have exactly 3 active strategies from this test");
// All returned strategies should be active
for strategy in &active_strategies {
assert_eq!(strategy.status, StrategyStatus::Active);
}
}
#[tokio::test]
async fn test_get_strategy_by_id() {
let pool = setup_test_db().await;
let coordinator = StrategyCoordinator::new(pool);
// Register a strategy
let mut parameters = HashMap::new();
parameters.insert("risk_limit".to_string(), 0.08);
parameters.insert("lookback_period".to_string(), 20.0);
let config = StrategyConfig {
strategy_id: String::new(),
strategy_name: "test_get_by_id".to_string(),
strategy_type: StrategyType::MeanReversion,
parameters: parameters.clone(),
status: StrategyStatus::Active,
created_at: chrono::Utc::now(),
updated_at: chrono::Utc::now(),
};
let strategy_id = coordinator.register_strategy(config).await.expect("Failed to register");
// Get strategy by ID
let result = coordinator.get_strategy(&strategy_id).await;
assert!(result.is_ok(), "Get strategy by ID should succeed");
let strategy = result.expect("Strategy should be returned");
assert_eq!(strategy.strategy_id, strategy_id);
assert_eq!(strategy.strategy_name, "test_get_by_id");
assert_eq!(strategy.strategy_type, StrategyType::MeanReversion);
assert_eq!(strategy.parameters.get("risk_limit"), Some(&0.08));
}
#[tokio::test]
async fn test_get_nonexistent_strategy() {
let pool = setup_test_db().await;
let coordinator = StrategyCoordinator::new(pool);
let fake_id = uuid::Uuid::new_v4().to_string();
let result = coordinator.get_strategy(&fake_id).await;
assert!(result.is_err(), "Getting non-existent strategy should fail");
}
#[tokio::test]
async fn test_strategy_performance() {
let pool = setup_test_db().await;
let coordinator = StrategyCoordinator::new(pool);
let mut parameters = HashMap::new();
parameters.insert("risk_limit".to_string(), 0.05);
let config = StrategyConfig {
strategy_id: String::new(),
strategy_name: "test_performance".to_string(),
strategy_type: StrategyType::EqualWeight,
parameters,
status: StrategyStatus::Active,
created_at: chrono::Utc::now(),
updated_at: chrono::Utc::now(),
};
let start = std::time::Instant::now();
let result = coordinator.register_strategy(config).await;
let duration = start.elapsed();
assert!(result.is_ok(), "Strategy registration should succeed");
// Performance target: < 50ms
assert!(
duration.as_millis() < 50,
"Strategy registration took {}ms (target: <50ms)",
duration.as_millis()
);
println!("Strategy registration completed in {}ms", duration.as_millis());
}
#[tokio::test]
async fn test_list_strategies_performance() {
let pool = setup_test_db().await;
let coordinator = StrategyCoordinator::new(pool);
let start = std::time::Instant::now();
let result = coordinator.list_strategies().await;
let duration = start.elapsed();
assert!(result.is_ok(), "List strategies should succeed");
// Performance target: < 50ms
assert!(
duration.as_millis() < 50,
"List strategies took {}ms (target: <50ms)",
duration.as_millis()
);
println!("List strategies completed in {}ms", duration.as_millis());
}
#[tokio::test]
async fn test_update_status_performance() {
let pool = setup_test_db().await;
let coordinator = StrategyCoordinator::new(pool);
// Register a strategy first
let mut parameters = HashMap::new();
parameters.insert("risk_limit".to_string(), 0.05);
let config = StrategyConfig {
strategy_id: String::new(),
strategy_name: "test_update_performance".to_string(),
strategy_type: StrategyType::RiskParity,
parameters,
status: StrategyStatus::Active,
created_at: chrono::Utc::now(),
updated_at: chrono::Utc::now(),
};
let strategy_id = coordinator.register_strategy(config).await.expect("Failed to register");
// Measure update performance
let start = std::time::Instant::now();
let result = coordinator.update_status(&strategy_id, StrategyStatus::Paused).await;
let duration = start.elapsed();
assert!(result.is_ok(), "Status update should succeed");
// Performance target: < 50ms
assert!(
duration.as_millis() < 50,
"Status update took {}ms (target: <50ms)",
duration.as_millis()
);
println!("Status update completed in {}ms", duration.as_millis());
}
#[tokio::test]
async fn test_all_strategy_types() {
let pool = setup_test_db().await;
let coordinator = StrategyCoordinator::new(pool);
let strategy_types = vec![
StrategyType::EqualWeight,
StrategyType::RiskParity,
StrategyType::MLOptimized,
StrategyType::MeanVariance,
StrategyType::Momentum,
StrategyType::MeanReversion,
];
for (i, strategy_type) in strategy_types.iter().enumerate() {
let mut parameters = HashMap::new();
parameters.insert("risk_limit".to_string(), 0.05);
let config = StrategyConfig {
strategy_id: String::new(),
strategy_name: format!("test_type_{}_{:?}", i, strategy_type),
strategy_type: strategy_type.clone(),
parameters,
status: StrategyStatus::Active,
created_at: chrono::Utc::now(),
updated_at: chrono::Utc::now(),
};
let result = coordinator.register_strategy(config).await;
assert!(
result.is_ok(),
"Should be able to register {:?} strategy",
strategy_type
);
}
}
#[tokio::test]
async fn test_empty_parameters() {
let pool = setup_test_db().await;
let coordinator = StrategyCoordinator::new(pool);
let config = StrategyConfig {
strategy_id: String::new(),
strategy_name: "test_empty_params".to_string(),
strategy_type: StrategyType::EqualWeight,
parameters: HashMap::new(), // Empty parameters
status: StrategyStatus::Active,
created_at: chrono::Utc::now(),
updated_at: chrono::Utc::now(),
};
let result = coordinator.register_strategy(config).await;
assert!(result.is_ok(), "Empty parameters should be allowed");
}
#[tokio::test]
async fn test_complex_parameters() {
let pool = setup_test_db().await;
let coordinator = StrategyCoordinator::new(pool);
let mut parameters = HashMap::new();
parameters.insert("risk_limit".to_string(), 0.05);
parameters.insert("rebalance_threshold".to_string(), 0.02);
parameters.insert("lookback_period".to_string(), 20.0);
parameters.insert("volatility_target".to_string(), 0.15);
parameters.insert("max_position_size".to_string(), 0.10);
let config = StrategyConfig {
strategy_id: String::new(),
strategy_name: "test_complex_params".to_string(),
strategy_type: StrategyType::MLOptimized,
parameters: parameters.clone(),
status: StrategyStatus::Active,
created_at: chrono::Utc::now(),
updated_at: chrono::Utc::now(),
};
let strategy_id = coordinator.register_strategy(config).await.expect("Failed to register");
// Verify parameters were stored correctly
let strategy = coordinator.get_strategy(&strategy_id).await.expect("Failed to get strategy");
assert_eq!(strategy.parameters.len(), 5);
assert_eq!(strategy.parameters.get("risk_limit"), Some(&0.05));
assert_eq!(strategy.parameters.get("lookback_period"), Some(&20.0));
}