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:
39
migrations/041_create_strategy_configs_table.sql
Normal file
39
migrations/041_create_strategy_configs_table.sql
Normal 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)';
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
447
services/trading_agent_service/tests/strategy_tests.rs
Normal file
447
services/trading_agent_service/tests/strategy_tests.rs
Normal 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));
|
||||
}
|
||||
Reference in New Issue
Block a user