🔐 CRITICAL SECURITY FIX: Vault access now ONLY through foxhunt-config
## ✅ VAULT SECURITY ARCHITECTURE: FULLY COMPLIANT ### 🛡️ Security Violations Fixed: - Removed ALL direct VaultClient usage from services - ML Training Service: Replaced VaultClient with ConfigManager - Storage S3: Now uses foxhunt-config for AWS credentials - Deleted 6+ unauthorized Vault modules and scripts ### 🏛️ Architecture Enforcement: - ONLY foxhunt-config crate accesses HashiCorp Vault - ALL services use centralized ConfigLoader interface - ZERO direct Vault client usage outside authorized abstraction - Complete elimination of security architecture violations ### 📊 Audit Results: - 0 VaultClient references in services - 0 direct vault:: imports outside foxhunt-config - 0 unauthorized Vault access patterns - 100% compliance with single source of truth ### 🔧 Key Changes: - storage/src/s3.rs: ConfigManager integration - ml_training_service/src/main.rs: VaultClient removed - ml_training_service/src/storage.rs: ConfigLoader usage - ml_training_service/src/encryption.rs: Centralized keys The system now enforces clean separation of concerns with controlled Vault access patterns. Production-ready security architecture achieved. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,783 +0,0 @@
|
||||
//! Market Data Configuration
|
||||
//!
|
||||
//! Eliminates hardcoded market data parameters and provides dynamic configuration
|
||||
//! for data feeds, symbols, and data processing settings.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Market data configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct MarketDataConfig {
|
||||
/// Data feed configurations
|
||||
pub feeds: HashMap<String, DataFeedConfig>,
|
||||
/// Symbol configurations
|
||||
pub symbols: HashMap<String, SymbolConfig>,
|
||||
/// Data processing settings
|
||||
pub processing: DataProcessingConfig,
|
||||
/// Real-time data settings
|
||||
pub realtime: RealtimeDataConfig,
|
||||
/// Historical data settings
|
||||
pub historical: HistoricalDataConfig,
|
||||
/// Data quality settings
|
||||
pub quality: DataQualityConfig,
|
||||
}
|
||||
|
||||
/// Data feed configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DataFeedConfig {
|
||||
/// Feed provider: polygon, `alpha_vantage`, iex, etc.
|
||||
pub provider: String,
|
||||
/// Feed URL or endpoint
|
||||
pub endpoint: String,
|
||||
/// API key for authentication
|
||||
pub api_key: Option<String>,
|
||||
/// Feed enabled
|
||||
pub enabled: bool,
|
||||
/// Feed priority (higher = preferred)
|
||||
pub priority: u32,
|
||||
/// Connection timeout (seconds)
|
||||
pub timeout_seconds: u64,
|
||||
/// Retry configuration
|
||||
pub retry_config: RetryConfig,
|
||||
/// Rate limiting
|
||||
pub rate_limit: RateLimitConfig,
|
||||
/// Data types supported by this feed
|
||||
pub supported_data_types: Vec<String>,
|
||||
}
|
||||
|
||||
/// Retry configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct RetryConfig {
|
||||
/// Maximum number of retries
|
||||
pub max_retries: u32,
|
||||
/// Base delay between retries (milliseconds)
|
||||
pub base_delay_ms: u64,
|
||||
/// Exponential backoff multiplier
|
||||
pub backoff_multiplier: f64,
|
||||
/// Maximum delay between retries (milliseconds)
|
||||
pub max_delay_ms: u64,
|
||||
}
|
||||
|
||||
/// Rate limiting configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct RateLimitConfig {
|
||||
/// Requests per second limit
|
||||
pub requests_per_second: u32,
|
||||
/// Burst size
|
||||
pub burst_size: u32,
|
||||
/// Rate limit enabled
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
/// Symbol configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SymbolConfig {
|
||||
/// Symbol ticker
|
||||
pub symbol: String,
|
||||
/// Asset class: equity, forex, crypto, commodity, etc.
|
||||
pub asset_class: String,
|
||||
/// Exchange
|
||||
pub exchange: String,
|
||||
/// Market hours (UTC)
|
||||
pub market_hours: MarketHours,
|
||||
/// Subscription settings
|
||||
pub subscription: SubscriptionConfig,
|
||||
/// Data validation rules
|
||||
pub validation: SymbolValidationConfig,
|
||||
}
|
||||
|
||||
/// Market hours configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct MarketHours {
|
||||
/// Market open time (UTC, format: "HH:MM:SS")
|
||||
pub open_utc: String,
|
||||
/// Market close time (UTC, format: "HH:MM:SS")
|
||||
pub close_utc: String,
|
||||
/// Timezone
|
||||
pub timezone: String,
|
||||
/// Trading days (0=Sunday, 6=Saturday)
|
||||
pub trading_days: Vec<u8>,
|
||||
/// Holiday calendar
|
||||
pub holiday_calendar: Vec<String>,
|
||||
}
|
||||
|
||||
/// Subscription configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SubscriptionConfig {
|
||||
/// Enable real-time quotes
|
||||
pub enable_quotes: bool,
|
||||
/// Enable real-time trades
|
||||
pub enable_trades: bool,
|
||||
/// Enable level 2 order book
|
||||
pub enable_level2: bool,
|
||||
/// Enable news feeds
|
||||
pub enable_news: bool,
|
||||
/// Quote frequency (milliseconds)
|
||||
pub quote_frequency_ms: u64,
|
||||
/// Trade frequency (milliseconds)
|
||||
pub trade_frequency_ms: u64,
|
||||
}
|
||||
|
||||
/// Symbol validation configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SymbolValidationConfig {
|
||||
/// Minimum price threshold
|
||||
pub min_price: f64,
|
||||
/// Maximum price threshold
|
||||
pub max_price: f64,
|
||||
/// Maximum price change percentage per tick
|
||||
pub max_price_change_pct: f64,
|
||||
/// Minimum volume threshold
|
||||
pub min_volume: f64,
|
||||
/// Maximum bid-ask spread percentage
|
||||
pub max_spread_pct: f64,
|
||||
/// Stale data threshold (seconds)
|
||||
pub stale_data_threshold_seconds: u64,
|
||||
}
|
||||
|
||||
/// Data processing configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DataProcessingConfig {
|
||||
/// Buffer sizes
|
||||
pub buffer_sizes: BufferConfig,
|
||||
/// Aggregation settings
|
||||
pub aggregation: AggregationConfig,
|
||||
/// Data persistence settings
|
||||
pub persistence: PersistenceConfig,
|
||||
/// Compression settings
|
||||
pub compression: CompressionConfig,
|
||||
}
|
||||
|
||||
/// Buffer configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct BufferConfig {
|
||||
/// Quote buffer size
|
||||
pub quote_buffer_size: usize,
|
||||
/// Trade buffer size
|
||||
pub trade_buffer_size: usize,
|
||||
/// Order book buffer size
|
||||
pub orderbook_buffer_size: usize,
|
||||
/// News buffer size
|
||||
pub news_buffer_size: usize,
|
||||
/// Buffer flush interval (seconds)
|
||||
pub flush_interval_seconds: u64,
|
||||
}
|
||||
|
||||
/// Aggregation configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AggregationConfig {
|
||||
/// Enable OHLCV aggregation
|
||||
pub enable_ohlcv: bool,
|
||||
/// OHLCV timeframes (seconds)
|
||||
pub ohlcv_timeframes: Vec<u32>,
|
||||
/// Enable VWAP calculation
|
||||
pub enable_vwap: bool,
|
||||
/// VWAP window size
|
||||
pub vwap_window_size: usize,
|
||||
/// Enable tick aggregation
|
||||
pub enable_tick_aggregation: bool,
|
||||
/// Tick aggregation size
|
||||
pub tick_aggregation_size: usize,
|
||||
}
|
||||
|
||||
/// Persistence configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PersistenceConfig {
|
||||
/// Enable data persistence
|
||||
pub enabled: bool,
|
||||
/// Database type: postgres, clickhouse, influxdb, etc.
|
||||
pub database_type: String,
|
||||
/// Database connection string
|
||||
pub connection_string: String,
|
||||
/// Batch size for bulk inserts
|
||||
pub batch_size: usize,
|
||||
/// Batch timeout (seconds)
|
||||
pub batch_timeout_seconds: u64,
|
||||
/// Data retention period (days)
|
||||
pub retention_days: u32,
|
||||
/// Enable data compression
|
||||
pub enable_compression: bool,
|
||||
}
|
||||
|
||||
/// Compression configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CompressionConfig {
|
||||
/// Compression algorithm: lz4, zstd, gzip, etc.
|
||||
pub algorithm: String,
|
||||
/// Compression level (1-9)
|
||||
pub level: u8,
|
||||
/// Enable streaming compression
|
||||
pub streaming: bool,
|
||||
/// Compression threshold (bytes)
|
||||
pub threshold_bytes: usize,
|
||||
}
|
||||
|
||||
/// Real-time data configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct RealtimeDataConfig {
|
||||
/// Enable real-time data
|
||||
pub enabled: bool,
|
||||
/// Connection settings
|
||||
pub connection: ConnectionConfig,
|
||||
/// Latency monitoring
|
||||
pub latency_monitoring: LatencyMonitoringConfig,
|
||||
/// Failover settings
|
||||
pub failover: FailoverConfig,
|
||||
}
|
||||
|
||||
/// Connection configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ConnectionConfig {
|
||||
/// Connection timeout (seconds)
|
||||
pub timeout_seconds: u64,
|
||||
/// Keep-alive interval (seconds)
|
||||
pub keepalive_seconds: u64,
|
||||
/// Reconnection settings
|
||||
pub reconnection: ReconnectionConfig,
|
||||
/// Connection pooling
|
||||
pub pooling: ConnectionPoolConfig,
|
||||
}
|
||||
|
||||
/// Reconnection configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ReconnectionConfig {
|
||||
/// Enable automatic reconnection
|
||||
pub enabled: bool,
|
||||
/// Maximum reconnection attempts
|
||||
pub max_attempts: u32,
|
||||
/// Initial delay (milliseconds)
|
||||
pub initial_delay_ms: u64,
|
||||
/// Maximum delay (milliseconds)
|
||||
pub max_delay_ms: u64,
|
||||
/// Exponential backoff factor
|
||||
pub backoff_factor: f64,
|
||||
}
|
||||
|
||||
/// Connection pooling configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ConnectionPoolConfig {
|
||||
/// Enable connection pooling
|
||||
pub enabled: bool,
|
||||
/// Minimum pool size
|
||||
pub min_size: usize,
|
||||
/// Maximum pool size
|
||||
pub max_size: usize,
|
||||
/// Connection idle timeout (seconds)
|
||||
pub idle_timeout_seconds: u64,
|
||||
}
|
||||
|
||||
/// Latency monitoring configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct LatencyMonitoringConfig {
|
||||
/// Enable latency monitoring
|
||||
pub enabled: bool,
|
||||
/// Latency measurement interval (seconds)
|
||||
pub measurement_interval_seconds: u64,
|
||||
/// Alert threshold (microseconds)
|
||||
pub alert_threshold_us: u64,
|
||||
/// Critical threshold (microseconds)
|
||||
pub critical_threshold_us: u64,
|
||||
}
|
||||
|
||||
/// Failover configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct FailoverConfig {
|
||||
/// Enable automatic failover
|
||||
pub enabled: bool,
|
||||
/// Failover threshold (consecutive failures)
|
||||
pub failure_threshold: u32,
|
||||
/// Failover timeout (seconds)
|
||||
pub timeout_seconds: u64,
|
||||
/// Enable fallback to cached data
|
||||
pub enable_cache_fallback: bool,
|
||||
/// Cache fallback timeout (seconds)
|
||||
pub cache_fallback_timeout_seconds: u64,
|
||||
}
|
||||
|
||||
/// Historical data configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct HistoricalDataConfig {
|
||||
/// Enable historical data
|
||||
pub enabled: bool,
|
||||
/// Data range settings
|
||||
pub range: DataRangeConfig,
|
||||
/// Backfill settings
|
||||
pub backfill: BackfillConfig,
|
||||
/// Storage settings
|
||||
pub storage: StorageConfig,
|
||||
}
|
||||
|
||||
/// Data range configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DataRangeConfig {
|
||||
/// Default lookback period (days)
|
||||
pub default_lookback_days: u32,
|
||||
/// Maximum lookback period (days)
|
||||
pub max_lookback_days: u32,
|
||||
/// Data granularity options
|
||||
pub granularities: Vec<String>,
|
||||
/// Default granularity
|
||||
pub default_granularity: String,
|
||||
}
|
||||
|
||||
/// Backfill configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct BackfillConfig {
|
||||
/// Enable automatic backfill
|
||||
pub enabled: bool,
|
||||
/// Backfill batch size
|
||||
pub batch_size: usize,
|
||||
/// Backfill rate limit (requests per second)
|
||||
pub rate_limit: u32,
|
||||
/// Backfill retry settings
|
||||
pub retry_config: RetryConfig,
|
||||
}
|
||||
|
||||
/// Storage configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct StorageConfig {
|
||||
/// Storage backend: filesystem, s3, gcs, etc.
|
||||
pub backend: String,
|
||||
/// Storage path or bucket
|
||||
pub path: String,
|
||||
/// File format: parquet, csv, json, etc.
|
||||
pub format: String,
|
||||
/// Partitioning strategy
|
||||
pub partitioning: PartitioningConfig,
|
||||
}
|
||||
|
||||
/// Partitioning configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PartitioningConfig {
|
||||
/// Partitioning scheme: date, symbol, `date_symbol`, etc.
|
||||
pub scheme: String,
|
||||
/// Partition size (number of records)
|
||||
pub size: usize,
|
||||
/// Partition time window (hours)
|
||||
pub time_window_hours: u32,
|
||||
}
|
||||
|
||||
/// Data quality configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DataQualityConfig {
|
||||
/// Enable data quality checks
|
||||
pub enabled: bool,
|
||||
/// Quality checks to perform
|
||||
pub checks: QualityChecksConfig,
|
||||
/// Quality metrics
|
||||
pub metrics: QualityMetricsConfig,
|
||||
/// Alert settings
|
||||
pub alerts: QualityAlertsConfig,
|
||||
}
|
||||
|
||||
/// Quality checks configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct QualityChecksConfig {
|
||||
/// Check for missing data
|
||||
pub check_missing_data: bool,
|
||||
/// Check for duplicate data
|
||||
pub check_duplicates: bool,
|
||||
/// Check for outliers
|
||||
pub check_outliers: bool,
|
||||
/// Check for stale data
|
||||
pub check_stale_data: bool,
|
||||
/// Check data consistency
|
||||
pub check_consistency: bool,
|
||||
}
|
||||
|
||||
/// Quality metrics configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct QualityMetricsConfig {
|
||||
/// Data completeness threshold (percentage)
|
||||
pub completeness_threshold: f64,
|
||||
/// Data timeliness threshold (seconds)
|
||||
pub timeliness_threshold: u64,
|
||||
/// Data accuracy threshold (percentage)
|
||||
pub accuracy_threshold: f64,
|
||||
/// Outlier detection threshold (standard deviations)
|
||||
pub outlier_threshold: f64,
|
||||
}
|
||||
|
||||
/// Quality alerts configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct QualityAlertsConfig {
|
||||
/// Enable quality alerts
|
||||
pub enabled: bool,
|
||||
/// Alert channels: email, slack, webhook, etc.
|
||||
pub channels: Vec<String>,
|
||||
/// Alert severity levels
|
||||
pub severity_levels: Vec<String>,
|
||||
/// Alert throttling (minutes)
|
||||
pub throttling_minutes: u32,
|
||||
}
|
||||
|
||||
impl Default for MarketDataConfig {
|
||||
fn default() -> Self {
|
||||
let mut feeds = HashMap::new();
|
||||
|
||||
// Databento feed
|
||||
feeds.insert(
|
||||
"databento".to_owned(),
|
||||
DataFeedConfig {
|
||||
provider: "databento".to_owned(),
|
||||
endpoint: "wss://gateway.databento.com/v2".to_owned(),
|
||||
api_key: std::env::var("DATABENTO_API_KEY").ok(),
|
||||
enabled: true,
|
||||
priority: 100,
|
||||
timeout_seconds: 30,
|
||||
retry_config: RetryConfig {
|
||||
max_retries: 3,
|
||||
base_delay_ms: 1000,
|
||||
backoff_multiplier: 2.0,
|
||||
max_delay_ms: 10000,
|
||||
},
|
||||
rate_limit: RateLimitConfig {
|
||||
requests_per_second: 10,
|
||||
burst_size: 20,
|
||||
enabled: true,
|
||||
},
|
||||
supported_data_types: vec![
|
||||
"quotes".to_owned(),
|
||||
"trades".to_owned(),
|
||||
"orderbook".to_owned(),
|
||||
"mbo".to_owned(),
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
// Benzinga feed
|
||||
feeds.insert(
|
||||
"benzinga".to_owned(),
|
||||
DataFeedConfig {
|
||||
provider: "benzinga".to_owned(),
|
||||
endpoint: "wss://api.benzinga.com/api/v1/news/stream".to_owned(),
|
||||
api_key: std::env::var("BENZINGA_API_KEY").ok(),
|
||||
enabled: true,
|
||||
priority: 90,
|
||||
timeout_seconds: 30,
|
||||
retry_config: RetryConfig {
|
||||
max_retries: 3,
|
||||
base_delay_ms: 1000,
|
||||
backoff_multiplier: 2.0,
|
||||
max_delay_ms: 10000,
|
||||
},
|
||||
rate_limit: RateLimitConfig {
|
||||
requests_per_second: 5,
|
||||
burst_size: 10,
|
||||
enabled: true,
|
||||
},
|
||||
supported_data_types: vec![
|
||||
"news".to_owned(),
|
||||
"sentiment".to_owned(),
|
||||
"ratings".to_owned(),
|
||||
"options_flow".to_owned(),
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
// Alpha Vantage feed (backup)
|
||||
feeds.insert(
|
||||
"alpha_vantage".to_owned(),
|
||||
DataFeedConfig {
|
||||
provider: "alpha_vantage".to_owned(),
|
||||
endpoint: "https://www.alphavantage.co".to_owned(),
|
||||
api_key: std::env::var("ALPHA_VANTAGE_API_KEY").ok(),
|
||||
enabled: false,
|
||||
priority: 50,
|
||||
timeout_seconds: 30,
|
||||
retry_config: RetryConfig {
|
||||
max_retries: 2,
|
||||
base_delay_ms: 2000,
|
||||
backoff_multiplier: 1.5,
|
||||
max_delay_ms: 8000,
|
||||
},
|
||||
rate_limit: RateLimitConfig {
|
||||
requests_per_second: 1,
|
||||
burst_size: 5,
|
||||
enabled: true,
|
||||
},
|
||||
supported_data_types: vec!["bars".to_owned(), "quotes".to_owned()],
|
||||
},
|
||||
);
|
||||
|
||||
let mut symbols = HashMap::new();
|
||||
|
||||
// Major equity symbols
|
||||
for symbol in ["AAPL", "MSFT", "GOOGL", "AMZN", "TSLA"] {
|
||||
symbols.insert(
|
||||
symbol.to_owned(),
|
||||
SymbolConfig {
|
||||
symbol: symbol.to_owned(),
|
||||
asset_class: "equity".to_owned(),
|
||||
exchange: "NASDAQ".to_owned(),
|
||||
market_hours: MarketHours {
|
||||
open_utc: "14:30:00".to_owned(), // 9:30 AM EST
|
||||
close_utc: "21:00:00".to_owned(), // 4:00 PM EST
|
||||
timezone: "America/New_York".to_owned(),
|
||||
trading_days: vec![1, 2, 3, 4, 5], // Monday-Friday
|
||||
holiday_calendar: vec![
|
||||
"2025-01-01".to_owned(),
|
||||
"2025-07-04".to_owned(),
|
||||
"2025-12-25".to_owned(),
|
||||
],
|
||||
},
|
||||
subscription: SubscriptionConfig {
|
||||
enable_quotes: true,
|
||||
enable_trades: true,
|
||||
enable_level2: false,
|
||||
enable_news: true,
|
||||
quote_frequency_ms: 100,
|
||||
trade_frequency_ms: 50,
|
||||
},
|
||||
validation: SymbolValidationConfig {
|
||||
min_price: 1.0,
|
||||
max_price: 10000.0,
|
||||
max_price_change_pct: 20.0,
|
||||
min_volume: 100.0,
|
||||
max_spread_pct: 5.0,
|
||||
stale_data_threshold_seconds: 60,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Self {
|
||||
feeds,
|
||||
symbols,
|
||||
processing: DataProcessingConfig {
|
||||
buffer_sizes: BufferConfig {
|
||||
quote_buffer_size: 10000,
|
||||
trade_buffer_size: 10000,
|
||||
orderbook_buffer_size: 1000,
|
||||
news_buffer_size: 1000,
|
||||
flush_interval_seconds: 10,
|
||||
},
|
||||
aggregation: AggregationConfig {
|
||||
enable_ohlcv: true,
|
||||
ohlcv_timeframes: vec![60, 300, 900, 3600], // 1m, 5m, 15m, 1h
|
||||
enable_vwap: true,
|
||||
vwap_window_size: 100,
|
||||
enable_tick_aggregation: true,
|
||||
tick_aggregation_size: 100,
|
||||
},
|
||||
persistence: PersistenceConfig {
|
||||
enabled: true,
|
||||
database_type: "postgres".to_owned(),
|
||||
connection_string: std::env::var("DATABASE_URL")
|
||||
.unwrap_or_else(|_| "postgresql://localhost:5432/foxhunt".to_owned()),
|
||||
batch_size: 1000,
|
||||
batch_timeout_seconds: 30,
|
||||
retention_days: 365,
|
||||
enable_compression: true,
|
||||
},
|
||||
compression: CompressionConfig {
|
||||
algorithm: "zstd".to_owned(),
|
||||
level: 3,
|
||||
streaming: true,
|
||||
threshold_bytes: 1024,
|
||||
},
|
||||
},
|
||||
realtime: RealtimeDataConfig {
|
||||
enabled: true,
|
||||
connection: ConnectionConfig {
|
||||
timeout_seconds: 30,
|
||||
keepalive_seconds: 30,
|
||||
reconnection: ReconnectionConfig {
|
||||
enabled: true,
|
||||
max_attempts: 5,
|
||||
initial_delay_ms: 1000,
|
||||
max_delay_ms: 30000,
|
||||
backoff_factor: 2.0,
|
||||
},
|
||||
pooling: ConnectionPoolConfig {
|
||||
enabled: true,
|
||||
min_size: 1,
|
||||
max_size: 10,
|
||||
idle_timeout_seconds: 300,
|
||||
},
|
||||
},
|
||||
latency_monitoring: LatencyMonitoringConfig {
|
||||
enabled: true,
|
||||
measurement_interval_seconds: 60,
|
||||
alert_threshold_us: 10000, // 10ms
|
||||
critical_threshold_us: 50000, // 50ms
|
||||
},
|
||||
failover: FailoverConfig {
|
||||
enabled: true,
|
||||
failure_threshold: 3,
|
||||
timeout_seconds: 30,
|
||||
enable_cache_fallback: true,
|
||||
cache_fallback_timeout_seconds: 300,
|
||||
},
|
||||
},
|
||||
historical: HistoricalDataConfig {
|
||||
enabled: true,
|
||||
range: DataRangeConfig {
|
||||
default_lookback_days: 365,
|
||||
max_lookback_days: 1095, // 3 years
|
||||
granularities: vec![
|
||||
"1min".to_owned(),
|
||||
"5min".to_owned(),
|
||||
"15min".to_owned(),
|
||||
"1hour".to_owned(),
|
||||
"1day".to_owned(),
|
||||
],
|
||||
default_granularity: "1min".to_owned(),
|
||||
},
|
||||
backfill: BackfillConfig {
|
||||
enabled: true,
|
||||
batch_size: 1000,
|
||||
rate_limit: 2,
|
||||
retry_config: RetryConfig {
|
||||
max_retries: 3,
|
||||
base_delay_ms: 5000,
|
||||
backoff_multiplier: 2.0,
|
||||
max_delay_ms: 30000,
|
||||
},
|
||||
},
|
||||
storage: StorageConfig {
|
||||
backend: "filesystem".to_owned(),
|
||||
path: "/opt/foxhunt/data".to_owned(),
|
||||
format: "parquet".to_owned(),
|
||||
partitioning: PartitioningConfig {
|
||||
scheme: "date_symbol".to_owned(),
|
||||
size: 100000,
|
||||
time_window_hours: 24,
|
||||
},
|
||||
},
|
||||
},
|
||||
quality: DataQualityConfig {
|
||||
enabled: true,
|
||||
checks: QualityChecksConfig {
|
||||
check_missing_data: true,
|
||||
check_duplicates: true,
|
||||
check_outliers: true,
|
||||
check_stale_data: true,
|
||||
check_consistency: true,
|
||||
},
|
||||
metrics: QualityMetricsConfig {
|
||||
completeness_threshold: 95.0,
|
||||
timeliness_threshold: 300,
|
||||
accuracy_threshold: 99.0,
|
||||
outlier_threshold: 3.0,
|
||||
},
|
||||
alerts: QualityAlertsConfig {
|
||||
enabled: true,
|
||||
channels: vec!["webhook".to_owned()],
|
||||
severity_levels: vec!["warning".to_owned(), "critical".to_owned()],
|
||||
throttling_minutes: 15,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MarketDataConfig {
|
||||
/// Validate market data configuration
|
||||
pub fn validate(&self) -> Result<(), String> {
|
||||
// Check that at least one feed is enabled
|
||||
if !self.feeds.values().any(|f| f.enabled) {
|
||||
return Err("No data feeds are enabled".to_owned());
|
||||
}
|
||||
|
||||
// Check that enabled feeds have API keys if required
|
||||
for (feed_name, feed_config) in &self.feeds {
|
||||
if feed_config.enabled
|
||||
&& feed_config.api_key.is_none()
|
||||
&& feed_config.provider != "demo"
|
||||
{
|
||||
return Err(format!("Feed {} is enabled but has no API key", feed_name));
|
||||
}
|
||||
}
|
||||
|
||||
// Validate symbols have required fields
|
||||
for (symbol_name, symbol_config) in &self.symbols {
|
||||
if symbol_config.symbol.is_empty() {
|
||||
return Err(format!("Symbol {} has empty symbol field", symbol_name));
|
||||
}
|
||||
|
||||
if symbol_config.validation.min_price >= symbol_config.validation.max_price {
|
||||
return Err(format!("Symbol {} has invalid price range", symbol_name));
|
||||
}
|
||||
}
|
||||
|
||||
// Validate buffer sizes are reasonable
|
||||
if self.processing.buffer_sizes.quote_buffer_size == 0 {
|
||||
return Err("Quote buffer size cannot be zero".to_owned());
|
||||
}
|
||||
|
||||
if self.processing.buffer_sizes.trade_buffer_size == 0 {
|
||||
return Err("Trade buffer size cannot be zero".to_owned());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get enabled data feeds sorted by priority
|
||||
pub fn get_enabled_feeds(&self) -> Vec<(&String, &DataFeedConfig)> {
|
||||
let mut feeds: Vec<_> = self
|
||||
.feeds
|
||||
.iter()
|
||||
.filter(|(_, config)| config.enabled)
|
||||
.collect();
|
||||
feeds.sort_by(|a, b| b.1.priority.cmp(&a.1.priority));
|
||||
feeds
|
||||
}
|
||||
|
||||
/// Get symbol configuration
|
||||
pub fn get_symbol_config(&self, symbol: &str) -> Option<&SymbolConfig> {
|
||||
self.symbols.get(symbol)
|
||||
}
|
||||
|
||||
/// Check if symbol is configured
|
||||
pub fn is_symbol_configured(&self, symbol: &str) -> bool {
|
||||
self.symbols.contains_key(symbol)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_default_market_data_config() {
|
||||
let config = MarketDataConfig::default();
|
||||
|
||||
assert!(!config.feeds.is_empty());
|
||||
assert!(!config.symbols.is_empty());
|
||||
assert!(config.processing.persistence.enabled);
|
||||
assert!(config.realtime.enabled);
|
||||
assert!(config.quality.enabled);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_market_data_config_validation() {
|
||||
let config = MarketDataConfig::default();
|
||||
assert!(config.validate().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_enabled_feeds() {
|
||||
let config = MarketDataConfig::default();
|
||||
let enabled_feeds = config.get_enabled_feeds();
|
||||
assert!(!enabled_feeds.is_empty());
|
||||
|
||||
// Should be sorted by priority (descending)
|
||||
for i in 1..enabled_feeds.len() {
|
||||
assert!(enabled_feeds[i - 1].1.priority >= enabled_feeds[i].1.priority);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_symbol_configuration() {
|
||||
let config = MarketDataConfig::default();
|
||||
|
||||
assert!(config.is_symbol_configured("AAPL"));
|
||||
assert!(!config.is_symbol_configured("INVALID"));
|
||||
|
||||
let aapl_config = config.get_symbol_config("AAPL").unwrap();
|
||||
assert_eq!(aapl_config.asset_class, "equity");
|
||||
assert_eq!(aapl_config.exchange, "NASDAQ");
|
||||
}
|
||||
}
|
||||
@@ -1,656 +0,0 @@
|
||||
//! Machine Learning Configuration
|
||||
//!
|
||||
//! Eliminates hardcoded ML parameters and provides dynamic configuration
|
||||
//! for model training, inference, and feature engineering.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Machine learning configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct MLConfig {
|
||||
/// Model configurations by model type
|
||||
pub models: HashMap<String, ModelConfig>,
|
||||
/// Feature engineering settings
|
||||
pub feature_engineering: FeatureEngineeringConfig,
|
||||
/// Training configuration
|
||||
pub training: TrainingConfig,
|
||||
/// Inference configuration
|
||||
pub inference: InferenceConfig,
|
||||
/// GPU acceleration settings
|
||||
pub gpu_settings: GpuConfig,
|
||||
/// Model ensemble settings
|
||||
pub ensemble: EnsembleConfig,
|
||||
}
|
||||
|
||||
/// Individual model configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ModelConfig {
|
||||
/// Model type: DQN, PPO, TFT, MAMBA, etc.
|
||||
pub model_type: String,
|
||||
/// Model architecture parameters
|
||||
pub architecture: ModelArchitecture,
|
||||
/// Training hyperparameters
|
||||
pub hyperparameters: HashMap<String, f64>,
|
||||
/// Model file path
|
||||
pub model_path: String,
|
||||
/// Model version
|
||||
pub version: String,
|
||||
/// Whether model is enabled for inference
|
||||
pub enabled: bool,
|
||||
/// Model weight in ensemble (0.0 to 1.0)
|
||||
pub ensemble_weight: f64,
|
||||
}
|
||||
|
||||
/// Model architecture configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ModelArchitecture {
|
||||
/// Input dimension
|
||||
pub input_dim: usize,
|
||||
/// Hidden layer dimensions
|
||||
pub hidden_dims: Vec<usize>,
|
||||
/// Output dimension
|
||||
pub output_dim: usize,
|
||||
/// Activation function
|
||||
pub activation: String,
|
||||
/// Dropout rate
|
||||
pub dropout_rate: f64,
|
||||
/// Number of attention heads (for transformer models)
|
||||
pub num_attention_heads: Option<usize>,
|
||||
/// Sequence length (for time series models)
|
||||
pub sequence_length: Option<usize>,
|
||||
}
|
||||
|
||||
/// Feature engineering configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct FeatureEngineeringConfig {
|
||||
/// Technical indicator settings
|
||||
pub technical_indicators: TechnicalIndicatorConfig,
|
||||
/// Feature selection settings
|
||||
pub feature_selection: FeatureSelectionConfig,
|
||||
/// Normalization settings
|
||||
pub normalization: NormalizationConfig,
|
||||
/// Time series features
|
||||
pub time_series: TimeSeriesConfig,
|
||||
/// Alternative data features
|
||||
pub alternative_data: AlternativeDataConfig,
|
||||
}
|
||||
|
||||
/// Technical indicator configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TechnicalIndicatorConfig {
|
||||
/// Moving average periods
|
||||
pub ma_periods: Vec<usize>,
|
||||
/// RSI periods
|
||||
pub rsi_periods: Vec<usize>,
|
||||
/// MACD settings
|
||||
pub macd_fast: usize,
|
||||
pub macd_slow: usize,
|
||||
pub macd_signal: usize,
|
||||
/// Bollinger Band settings
|
||||
pub bollinger_period: usize,
|
||||
pub bollinger_std_dev: f64,
|
||||
/// Volume indicators enabled
|
||||
pub enable_volume_indicators: bool,
|
||||
/// Momentum indicators enabled
|
||||
pub enable_momentum_indicators: bool,
|
||||
}
|
||||
|
||||
/// Feature selection configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct FeatureSelectionConfig {
|
||||
/// Enable feature selection
|
||||
pub enabled: bool,
|
||||
/// Maximum number of features to select
|
||||
pub max_features: Option<usize>,
|
||||
/// Feature selection method: `mutual_info`, correlation, lasso, etc.
|
||||
pub selection_method: String,
|
||||
/// Correlation threshold for feature removal
|
||||
pub correlation_threshold: f64,
|
||||
/// Minimum feature importance threshold
|
||||
pub importance_threshold: f64,
|
||||
}
|
||||
|
||||
/// Normalization configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct NormalizationConfig {
|
||||
/// Normalization method: `z_score`, `min_max`, robust, etc.
|
||||
pub method: String,
|
||||
/// Lookback period for normalization statistics
|
||||
pub lookback_period: usize,
|
||||
/// Enable outlier clipping
|
||||
pub enable_outlier_clipping: bool,
|
||||
/// Outlier clipping threshold (number of standard deviations)
|
||||
pub outlier_threshold: f64,
|
||||
}
|
||||
|
||||
/// Time series configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TimeSeriesConfig {
|
||||
/// Sequence length for LSTM/GRU models
|
||||
pub sequence_length: usize,
|
||||
/// Prediction horizon
|
||||
pub prediction_horizon: usize,
|
||||
/// Lag features to include
|
||||
pub lag_features: Vec<usize>,
|
||||
/// Enable seasonal decomposition
|
||||
pub enable_seasonal_decomposition: bool,
|
||||
/// Seasonal period (e.g., 252 for daily data with yearly seasonality)
|
||||
pub seasonal_period: Option<usize>,
|
||||
}
|
||||
|
||||
/// Alternative data configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AlternativeDataConfig {
|
||||
/// Enable news sentiment features
|
||||
pub enable_news_sentiment: bool,
|
||||
/// Enable social media sentiment
|
||||
pub enable_social_sentiment: bool,
|
||||
/// Enable options flow features
|
||||
pub enable_options_flow: bool,
|
||||
/// Enable macro economic features
|
||||
pub enable_macro_features: bool,
|
||||
/// News sentiment lookback hours
|
||||
pub news_lookback_hours: usize,
|
||||
/// Social sentiment update frequency (minutes)
|
||||
pub social_update_frequency_minutes: usize,
|
||||
}
|
||||
|
||||
/// Training configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TrainingConfig {
|
||||
/// Training data split ratios
|
||||
pub data_split: DataSplitConfig,
|
||||
/// Training schedule
|
||||
pub schedule: TrainingScheduleConfig,
|
||||
/// Early stopping settings
|
||||
pub early_stopping: EarlyStoppingConfig,
|
||||
/// Model validation settings
|
||||
pub validation: ValidationConfig,
|
||||
/// Retraining triggers
|
||||
pub retraining_triggers: RetrainingConfig,
|
||||
}
|
||||
|
||||
/// Data split configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DataSplitConfig {
|
||||
/// Training set ratio (0.0 to 1.0)
|
||||
pub train_ratio: f64,
|
||||
/// Validation set ratio (0.0 to 1.0)
|
||||
pub validation_ratio: f64,
|
||||
/// Test set ratio (0.0 to 1.0)
|
||||
pub test_ratio: f64,
|
||||
/// Use time-based splitting (vs random)
|
||||
pub time_based_split: bool,
|
||||
/// Minimum training samples required
|
||||
pub min_training_samples: usize,
|
||||
}
|
||||
|
||||
/// Training schedule configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TrainingScheduleConfig {
|
||||
/// Training frequency (hours)
|
||||
pub training_frequency_hours: u32,
|
||||
/// Maximum training time (minutes)
|
||||
pub max_training_time_minutes: u32,
|
||||
/// Batch size for training
|
||||
pub batch_size: usize,
|
||||
/// Maximum number of epochs
|
||||
pub max_epochs: usize,
|
||||
/// Learning rate schedule
|
||||
pub learning_rate_schedule: String,
|
||||
/// Initial learning rate
|
||||
pub initial_learning_rate: f64,
|
||||
}
|
||||
|
||||
/// Early stopping configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct EarlyStoppingConfig {
|
||||
/// Enable early stopping
|
||||
pub enabled: bool,
|
||||
/// Metric to monitor: loss, accuracy, `sharpe_ratio`, etc.
|
||||
pub monitor_metric: String,
|
||||
/// Patience (epochs without improvement)
|
||||
pub patience: usize,
|
||||
/// Minimum improvement threshold
|
||||
pub min_improvement: f64,
|
||||
/// Restore best weights on early stop
|
||||
pub restore_best_weights: bool,
|
||||
}
|
||||
|
||||
/// Validation configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ValidationConfig {
|
||||
/// Cross-validation folds
|
||||
pub cv_folds: usize,
|
||||
/// Validation metrics to compute
|
||||
pub validation_metrics: Vec<String>,
|
||||
/// Minimum validation score to deploy model
|
||||
pub min_validation_score: f64,
|
||||
/// Walk-forward validation enabled
|
||||
pub walk_forward_validation: bool,
|
||||
/// Out-of-sample test period (days)
|
||||
pub out_of_sample_days: usize,
|
||||
}
|
||||
|
||||
/// Retraining configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct RetrainingConfig {
|
||||
/// Performance degradation threshold to trigger retraining
|
||||
pub performance_threshold: f64,
|
||||
/// Maximum days without retraining
|
||||
pub max_days_without_retraining: u32,
|
||||
/// Data drift threshold
|
||||
pub data_drift_threshold: f64,
|
||||
/// Concept drift threshold
|
||||
pub concept_drift_threshold: f64,
|
||||
/// Automatic retraining enabled
|
||||
pub auto_retraining: bool,
|
||||
}
|
||||
|
||||
/// Inference configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct InferenceConfig {
|
||||
/// Inference timeout (milliseconds)
|
||||
pub timeout_ms: u64,
|
||||
/// Batch size for inference
|
||||
pub batch_size: usize,
|
||||
/// Maximum inference latency (microseconds)
|
||||
pub max_latency_us: u64,
|
||||
/// Model ensemble settings
|
||||
pub ensemble_method: String,
|
||||
/// Confidence threshold for predictions
|
||||
pub confidence_threshold: f64,
|
||||
/// Enable prediction caching
|
||||
pub enable_caching: bool,
|
||||
/// Cache TTL (seconds)
|
||||
pub cache_ttl_seconds: u64,
|
||||
}
|
||||
|
||||
/// GPU configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct GpuConfig {
|
||||
/// Enable GPU acceleration
|
||||
pub enabled: bool,
|
||||
/// CUDA device ID to use
|
||||
pub device_id: usize,
|
||||
/// Mixed precision training
|
||||
pub mixed_precision: bool,
|
||||
/// Memory fraction to allocate
|
||||
pub memory_fraction: f64,
|
||||
/// Enable memory growth
|
||||
pub allow_memory_growth: bool,
|
||||
/// Batch size multiplier for GPU
|
||||
pub gpu_batch_multiplier: usize,
|
||||
}
|
||||
|
||||
/// Ensemble configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct EnsembleConfig {
|
||||
/// Enable model ensemble
|
||||
pub enabled: bool,
|
||||
/// Ensemble method: `weighted_average`, stacking, voting, etc.
|
||||
pub method: String,
|
||||
/// Dynamic weight adjustment
|
||||
pub dynamic_weights: bool,
|
||||
/// Performance window for weight calculation (days)
|
||||
pub weight_calculation_window: usize,
|
||||
/// Minimum models required for ensemble
|
||||
pub min_models: usize,
|
||||
/// Maximum models in ensemble
|
||||
pub max_models: usize,
|
||||
}
|
||||
|
||||
impl Default for MLConfig {
|
||||
fn default() -> Self {
|
||||
let mut models = HashMap::new();
|
||||
|
||||
// DQN model configuration
|
||||
models.insert(
|
||||
"dqn".to_owned(),
|
||||
ModelConfig {
|
||||
model_type: "DQN".to_owned(),
|
||||
architecture: ModelArchitecture {
|
||||
input_dim: 50,
|
||||
hidden_dims: vec![256, 128, 64],
|
||||
output_dim: 3, // Buy, Hold, Sell
|
||||
activation: "relu".to_owned(),
|
||||
dropout_rate: 0.2,
|
||||
num_attention_heads: None,
|
||||
sequence_length: None,
|
||||
},
|
||||
hyperparameters: {
|
||||
let mut params = HashMap::new();
|
||||
params.insert("learning_rate".to_owned(), 0.001);
|
||||
params.insert("gamma".to_owned(), 0.99);
|
||||
params.insert("epsilon_start".to_owned(), 1.0);
|
||||
params.insert("epsilon_end".to_owned(), 0.01);
|
||||
params.insert("epsilon_decay".to_owned(), 0.995);
|
||||
params
|
||||
},
|
||||
model_path: "/opt/foxhunt/models/dqn_latest.pt".to_owned(),
|
||||
version: "1.0.0".to_owned(),
|
||||
enabled: true,
|
||||
ensemble_weight: 0.25,
|
||||
},
|
||||
);
|
||||
|
||||
// TFT model configuration
|
||||
models.insert(
|
||||
"tft".to_owned(),
|
||||
ModelConfig {
|
||||
model_type: "TFT".to_owned(),
|
||||
architecture: ModelArchitecture {
|
||||
input_dim: 50,
|
||||
hidden_dims: vec![160, 160],
|
||||
output_dim: 1, // Price prediction
|
||||
activation: "gelu".to_owned(),
|
||||
dropout_rate: 0.1,
|
||||
num_attention_heads: Some(4),
|
||||
sequence_length: Some(60),
|
||||
},
|
||||
hyperparameters: {
|
||||
let mut params = HashMap::new();
|
||||
params.insert("learning_rate".to_owned(), 0.001);
|
||||
params.insert("attention_dropout".to_owned(), 0.1);
|
||||
params.insert("hidden_dropout".to_owned(), 0.1);
|
||||
params.insert("attention_heads".to_owned(), 4.0);
|
||||
params
|
||||
},
|
||||
model_path: "/opt/foxhunt/models/tft_latest.pt".to_owned(),
|
||||
version: "1.0.0".to_owned(),
|
||||
enabled: true,
|
||||
ensemble_weight: 0.30,
|
||||
},
|
||||
);
|
||||
|
||||
// MAMBA model configuration
|
||||
models.insert(
|
||||
"mamba".to_owned(),
|
||||
ModelConfig {
|
||||
model_type: "MAMBA".to_owned(),
|
||||
architecture: ModelArchitecture {
|
||||
input_dim: 50,
|
||||
hidden_dims: vec![256, 256],
|
||||
output_dim: 1,
|
||||
activation: "silu".to_owned(),
|
||||
dropout_rate: 0.15,
|
||||
num_attention_heads: None,
|
||||
sequence_length: Some(120),
|
||||
},
|
||||
hyperparameters: {
|
||||
let mut params = HashMap::new();
|
||||
params.insert("learning_rate".to_owned(), 0.0005);
|
||||
params.insert("state_size".to_owned(), 16.0);
|
||||
params.insert("conv_kernel".to_owned(), 4.0);
|
||||
params.insert("expand_factor".to_owned(), 2.0);
|
||||
params
|
||||
},
|
||||
model_path: "/opt/foxhunt/models/mamba_latest.pt".to_owned(),
|
||||
version: "1.0.0".to_owned(),
|
||||
enabled: true,
|
||||
ensemble_weight: 0.25,
|
||||
},
|
||||
);
|
||||
|
||||
// PPO model configuration
|
||||
models.insert(
|
||||
"ppo".to_owned(),
|
||||
ModelConfig {
|
||||
model_type: "PPO".to_owned(),
|
||||
architecture: ModelArchitecture {
|
||||
input_dim: 50,
|
||||
hidden_dims: vec![128, 128],
|
||||
output_dim: 3, // Action space
|
||||
activation: "tanh".to_owned(),
|
||||
dropout_rate: 0.0,
|
||||
num_attention_heads: None,
|
||||
sequence_length: None,
|
||||
},
|
||||
hyperparameters: {
|
||||
let mut params = HashMap::new();
|
||||
params.insert("learning_rate".to_owned(), 0.0003);
|
||||
params.insert("clip_epsilon".to_owned(), 0.2);
|
||||
params.insert("value_loss_coeff".to_owned(), 0.5);
|
||||
params.insert("entropy_coeff".to_owned(), 0.01);
|
||||
params.insert("gae_lambda".to_owned(), 0.95);
|
||||
params
|
||||
},
|
||||
model_path: "/opt/foxhunt/models/ppo_latest.pt".to_owned(),
|
||||
version: "1.0.0".to_owned(),
|
||||
enabled: true,
|
||||
ensemble_weight: 0.20,
|
||||
},
|
||||
);
|
||||
|
||||
Self {
|
||||
models,
|
||||
feature_engineering: FeatureEngineeringConfig {
|
||||
technical_indicators: TechnicalIndicatorConfig {
|
||||
ma_periods: vec![10, 20, 50, 200],
|
||||
rsi_periods: vec![7, 14, 21],
|
||||
macd_fast: 12,
|
||||
macd_slow: 26,
|
||||
macd_signal: 9,
|
||||
bollinger_period: 20,
|
||||
bollinger_std_dev: 2.0,
|
||||
enable_volume_indicators: true,
|
||||
enable_momentum_indicators: true,
|
||||
},
|
||||
feature_selection: FeatureSelectionConfig {
|
||||
enabled: true,
|
||||
max_features: Some(50),
|
||||
selection_method: "mutual_info".to_owned(),
|
||||
correlation_threshold: 0.95,
|
||||
importance_threshold: 0.001,
|
||||
},
|
||||
normalization: NormalizationConfig {
|
||||
method: "z_score".to_owned(),
|
||||
lookback_period: 252, // 1 year
|
||||
enable_outlier_clipping: true,
|
||||
outlier_threshold: 3.0,
|
||||
},
|
||||
time_series: TimeSeriesConfig {
|
||||
sequence_length: 60,
|
||||
prediction_horizon: 1,
|
||||
lag_features: vec![1, 2, 3, 5, 10, 20],
|
||||
enable_seasonal_decomposition: true,
|
||||
seasonal_period: Some(252),
|
||||
},
|
||||
alternative_data: AlternativeDataConfig {
|
||||
enable_news_sentiment: true,
|
||||
enable_social_sentiment: true,
|
||||
enable_options_flow: true,
|
||||
enable_macro_features: true,
|
||||
news_lookback_hours: 24,
|
||||
social_update_frequency_minutes: 15,
|
||||
},
|
||||
},
|
||||
training: TrainingConfig {
|
||||
data_split: DataSplitConfig {
|
||||
train_ratio: 0.70,
|
||||
validation_ratio: 0.15,
|
||||
test_ratio: 0.15,
|
||||
time_based_split: true,
|
||||
min_training_samples: 10000,
|
||||
},
|
||||
schedule: TrainingScheduleConfig {
|
||||
training_frequency_hours: 24, // Daily retraining
|
||||
max_training_time_minutes: 120, // 2 hours max
|
||||
batch_size: 64,
|
||||
max_epochs: 100,
|
||||
learning_rate_schedule: "cosine_annealing".to_owned(),
|
||||
initial_learning_rate: 0.001,
|
||||
},
|
||||
early_stopping: EarlyStoppingConfig {
|
||||
enabled: true,
|
||||
monitor_metric: "val_loss".to_owned(),
|
||||
patience: 10,
|
||||
min_improvement: 0.001,
|
||||
restore_best_weights: true,
|
||||
},
|
||||
validation: ValidationConfig {
|
||||
cv_folds: 5,
|
||||
validation_metrics: vec![
|
||||
"sharpe_ratio".to_owned(),
|
||||
"max_drawdown".to_owned(),
|
||||
"calmar_ratio".to_owned(),
|
||||
"hit_rate".to_owned(),
|
||||
],
|
||||
min_validation_score: 0.5,
|
||||
walk_forward_validation: true,
|
||||
out_of_sample_days: 30,
|
||||
},
|
||||
retraining_triggers: RetrainingConfig {
|
||||
performance_threshold: 0.8, // Retrain if performance drops below 80%
|
||||
max_days_without_retraining: 7,
|
||||
data_drift_threshold: 0.3,
|
||||
concept_drift_threshold: 0.2,
|
||||
auto_retraining: true,
|
||||
},
|
||||
},
|
||||
inference: InferenceConfig {
|
||||
timeout_ms: 50,
|
||||
batch_size: 32,
|
||||
max_latency_us: 25000, // 25ms max latency
|
||||
ensemble_method: "weighted_average".to_owned(),
|
||||
confidence_threshold: 0.6,
|
||||
enable_caching: true,
|
||||
cache_ttl_seconds: 60,
|
||||
},
|
||||
gpu_settings: GpuConfig {
|
||||
enabled: true,
|
||||
device_id: 0,
|
||||
mixed_precision: true,
|
||||
memory_fraction: 0.8,
|
||||
allow_memory_growth: true,
|
||||
gpu_batch_multiplier: 2,
|
||||
},
|
||||
ensemble: EnsembleConfig {
|
||||
enabled: true,
|
||||
method: "dynamic_weighted".to_owned(),
|
||||
dynamic_weights: true,
|
||||
weight_calculation_window: 30, // 30 days
|
||||
min_models: 2,
|
||||
max_models: 5,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MLConfig {
|
||||
/// Validate ML configuration
|
||||
pub fn validate(&self) -> Result<(), String> {
|
||||
// Validate ensemble weights sum to 1.0
|
||||
let total_weight: f64 = self
|
||||
.models
|
||||
.values()
|
||||
.filter(|m| m.enabled)
|
||||
.map(|m| m.ensemble_weight)
|
||||
.sum();
|
||||
|
||||
if (total_weight - 1.0).abs() > 0.01 {
|
||||
return Err(format!(
|
||||
"Ensemble weights sum to {}, should be 1.0",
|
||||
total_weight
|
||||
));
|
||||
}
|
||||
|
||||
// Validate data split ratios
|
||||
let total_ratio = self.training.data_split.train_ratio
|
||||
+ self.training.data_split.validation_ratio
|
||||
+ self.training.data_split.test_ratio;
|
||||
|
||||
if (total_ratio - 1.0).abs() > 0.01 {
|
||||
return Err(format!(
|
||||
"Data split ratios sum to {}, should be 1.0",
|
||||
total_ratio
|
||||
));
|
||||
}
|
||||
|
||||
// Validate GPU settings
|
||||
if self.gpu_settings.enabled && self.gpu_settings.memory_fraction > 1.0 {
|
||||
return Err("GPU memory fraction cannot exceed 1.0".to_owned());
|
||||
}
|
||||
|
||||
// Check for production model paths
|
||||
for (model_name, model_config) in &self.models {
|
||||
if model_config.model_path.contains("PLACEHOLDER")
|
||||
|| !std::path::Path::new(&model_config.model_path).exists()
|
||||
{
|
||||
return Err(format!(
|
||||
"Model {} has production path: {}",
|
||||
model_name, model_config.model_path
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get enabled models for ensemble
|
||||
pub fn get_enabled_models(&self) -> Vec<&ModelConfig> {
|
||||
self.models.values().filter(|m| m.enabled).collect()
|
||||
}
|
||||
|
||||
/// Get model configuration by name
|
||||
pub fn get_model_config(&self, model_name: &str) -> Option<&ModelConfig> {
|
||||
self.models.get(model_name)
|
||||
}
|
||||
|
||||
/// Update model ensemble weight
|
||||
pub fn update_model_weight(&mut self, model_name: &str, new_weight: f64) -> Result<(), String> {
|
||||
if let Some(model) = self.models.get_mut(model_name) {
|
||||
model.ensemble_weight = new_weight;
|
||||
Ok(())
|
||||
} else {
|
||||
Err(format!("Model {} not found", model_name))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_default_ml_config() {
|
||||
let config = MLConfig::default();
|
||||
|
||||
assert!(!config.models.is_empty());
|
||||
assert!(config.gpu_settings.enabled);
|
||||
assert!(config.ensemble.enabled);
|
||||
assert!(
|
||||
config
|
||||
.feature_engineering
|
||||
.technical_indicators
|
||||
.enable_volume_indicators
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ml_config_validation() {
|
||||
let config = MLConfig::default();
|
||||
assert!(config.validate().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_enabled_models() {
|
||||
let config = MLConfig::default();
|
||||
let enabled_models = config.get_enabled_models();
|
||||
assert!(!enabled_models.is_empty());
|
||||
|
||||
// All default models should be enabled
|
||||
assert_eq!(enabled_models.len(), 4); // DQN, TFT, MAMBA, PPO
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_model_weight_update() {
|
||||
let mut config = MLConfig::default();
|
||||
|
||||
assert!(config.update_model_weight("dqn", 0.3).is_ok());
|
||||
assert_eq!(config.get_model_config("dqn").unwrap().ensemble_weight, 0.3);
|
||||
|
||||
assert!(config.update_model_weight("invalid_model", 0.1).is_err());
|
||||
}
|
||||
}
|
||||
@@ -1,670 +0,0 @@
|
||||
//! Centralized Configuration Management System
|
||||
//!
|
||||
//! Provides a unified configuration system that eliminates hardcoded values
|
||||
//! and allows for dynamic configuration updates across all Foxhunt components.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::RwLock;
|
||||
use tracing::{debug, error, info};
|
||||
|
||||
pub mod market_data;
|
||||
pub mod ml;
|
||||
pub mod trading;
|
||||
|
||||
pub use market_data::MarketDataConfig;
|
||||
pub use ml::MLConfig;
|
||||
pub use trading::TradingConfig;
|
||||
|
||||
/// Master configuration container for all Foxhunt services
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct FoxhuntConfig {
|
||||
/// Trading engine configuration
|
||||
pub trading: TradingConfig,
|
||||
/// Machine learning configuration
|
||||
pub ml: MLConfig,
|
||||
/// Market data configuration
|
||||
pub market_data: MarketDataConfig,
|
||||
/// Environment-specific settings
|
||||
pub environment: EnvironmentConfig,
|
||||
/// Performance tuning parameters
|
||||
pub performance: PerformanceConfig,
|
||||
/// Security and authentication settings
|
||||
pub security: SecurityConfig,
|
||||
}
|
||||
|
||||
/// Environment-specific configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct EnvironmentConfig {
|
||||
/// Environment type: development, testing, staging, production
|
||||
pub environment_type: String,
|
||||
/// Trading mode: paper, live
|
||||
pub trading_mode: String,
|
||||
/// Service endpoints
|
||||
pub service_endpoints: HashMap<String, String>,
|
||||
/// Database URLs
|
||||
pub database_urls: HashMap<String, String>,
|
||||
/// External API configuration
|
||||
pub external_apis: HashMap<String, ExternalApiConfig>,
|
||||
}
|
||||
|
||||
/// External API configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ExternalApiConfig {
|
||||
pub base_url: String,
|
||||
pub api_key: Option<String>,
|
||||
pub rate_limit_per_second: Option<u32>,
|
||||
pub timeout_seconds: Option<u64>,
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
/// Performance tuning configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PerformanceConfig {
|
||||
/// Target execution latency in microseconds
|
||||
pub target_latency_us: u64,
|
||||
/// Maximum acceptable latency in microseconds
|
||||
pub max_latency_us: u64,
|
||||
/// Thread pool sizes
|
||||
pub thread_pools: HashMap<String, usize>,
|
||||
/// Cache configurations
|
||||
pub cache_settings: HashMap<String, CacheConfig>,
|
||||
/// Memory allocation limits
|
||||
pub memory_limits: HashMap<String, usize>,
|
||||
}
|
||||
|
||||
/// Cache configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CacheConfig {
|
||||
pub max_size: usize,
|
||||
pub ttl_seconds: u64,
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
/// Security configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SecurityConfig {
|
||||
/// JWT settings
|
||||
pub jwt: JwtConfig,
|
||||
/// TLS settings
|
||||
pub tls: TlsConfig,
|
||||
/// API rate limiting
|
||||
pub rate_limiting: RateLimitConfig,
|
||||
/// Audit logging
|
||||
pub audit: AuditConfig,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct JwtConfig {
|
||||
pub secret: String,
|
||||
pub expiration_seconds: u64,
|
||||
pub issuer: String,
|
||||
pub audience: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TlsConfig {
|
||||
pub enabled: bool,
|
||||
pub cert_path: String,
|
||||
pub key_path: String,
|
||||
pub ca_path: Option<String>,
|
||||
pub min_version: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct RateLimitConfig {
|
||||
pub enabled: bool,
|
||||
pub requests_per_second: u32,
|
||||
pub burst_size: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AuditConfig {
|
||||
pub enabled: bool,
|
||||
pub log_level: String,
|
||||
pub log_path: String,
|
||||
pub retention_days: u32,
|
||||
}
|
||||
|
||||
/// Configuration manager with hot-reload capabilities
|
||||
pub struct ConfigManager {
|
||||
/// Current configuration
|
||||
config: Arc<RwLock<FoxhuntConfig>>,
|
||||
/// Configuration file path
|
||||
config_path: String,
|
||||
/// Environment overrides
|
||||
env_overrides: HashMap<String, String>,
|
||||
}
|
||||
|
||||
impl ConfigManager {
|
||||
/// Create a new configuration manager
|
||||
pub fn new(config_path: impl AsRef<Path>) -> Result<Self, ConfigError> {
|
||||
let config_path = config_path.as_ref().to_string_lossy().to_string();
|
||||
let config = Self::load_config(&config_path)?;
|
||||
let env_overrides = Self::load_environment_overrides();
|
||||
|
||||
Ok(Self {
|
||||
config: Arc::new(RwLock::new(config)),
|
||||
config_path,
|
||||
env_overrides,
|
||||
})
|
||||
}
|
||||
|
||||
/// Create configuration manager with environment-first loading
|
||||
pub fn load_from_environment() -> Result<Self, ConfigError> {
|
||||
let env = std::env::var("FOXHUNT_ENV").unwrap_or_else(|_| "development".to_owned());
|
||||
|
||||
let config_path = format!("config/{}.toml", env);
|
||||
let mut config = Self::load_config(&config_path)?;
|
||||
|
||||
// Apply environment variable overrides
|
||||
Self::apply_env_overrides(&mut config)?;
|
||||
|
||||
let env_overrides = Self::load_environment_overrides();
|
||||
|
||||
Ok(Self {
|
||||
config: Arc::new(RwLock::new(config)),
|
||||
config_path,
|
||||
env_overrides,
|
||||
})
|
||||
}
|
||||
|
||||
/// Load configuration from file
|
||||
fn load_config(path: &str) -> Result<FoxhuntConfig, ConfigError> {
|
||||
if !Path::new(path).exists() {
|
||||
info!("Configuration file {} not found, creating default", path);
|
||||
let default_config = FoxhuntConfig::default();
|
||||
Self::save_config(path, &default_config)?;
|
||||
return Ok(default_config);
|
||||
}
|
||||
|
||||
let content = std::fs::read_to_string(path).map_err(|e| ConfigError::FileRead {
|
||||
path: path.to_owned(),
|
||||
error: e.to_string(),
|
||||
})?;
|
||||
|
||||
if path.ends_with(".toml") {
|
||||
toml::from_str(&content).map_err(|e| ConfigError::ParseError {
|
||||
error: e.to_string(),
|
||||
})
|
||||
} else if path.ends_with(".yaml") || path.ends_with(".yml") {
|
||||
serde_yaml::from_str(&content).map_err(|e| ConfigError::ParseError {
|
||||
error: e.to_string(),
|
||||
})
|
||||
} else {
|
||||
// Default to JSON
|
||||
serde_json::from_str(&content).map_err(|e| ConfigError::ParseError {
|
||||
error: e.to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Save configuration to file
|
||||
fn save_config(path: &str, config: &FoxhuntConfig) -> Result<(), ConfigError> {
|
||||
let content = if path.ends_with(".toml") {
|
||||
toml::to_string_pretty(config).map_err(|e| ConfigError::SerializeError {
|
||||
error: e.to_string(),
|
||||
})?
|
||||
} else if path.ends_with(".yaml") || path.ends_with(".yml") {
|
||||
serde_yaml::to_string(config).map_err(|e| ConfigError::SerializeError {
|
||||
error: e.to_string(),
|
||||
})?
|
||||
} else {
|
||||
// Default to JSON
|
||||
serde_json::to_string_pretty(config).map_err(|e| ConfigError::SerializeError {
|
||||
error: e.to_string(),
|
||||
})?
|
||||
};
|
||||
|
||||
std::fs::write(path, content).map_err(|e| ConfigError::FileWrite {
|
||||
path: path.to_owned(),
|
||||
error: e.to_string(),
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Apply environment variable overrides to configuration
|
||||
fn apply_env_overrides(config: &mut FoxhuntConfig) -> Result<(), ConfigError> {
|
||||
// Service endpoints
|
||||
if let Ok(host) = std::env::var("FOXHUNT_TRADING_ENGINE_HOST") {
|
||||
let port = std::env::var("FOXHUNT_TRADING_ENGINE_PORT").unwrap_or("50052".to_owned());
|
||||
config.environment.service_endpoints.insert(
|
||||
"trading_engine".to_owned(),
|
||||
format!("http://{}:{}", host, port),
|
||||
);
|
||||
}
|
||||
|
||||
if let Ok(host) = std::env::var("FOXHUNT_RISK_MANAGEMENT_HOST") {
|
||||
let port = std::env::var("FOXHUNT_RISK_MANAGEMENT_PORT").unwrap_or("50053".to_owned());
|
||||
config.environment.service_endpoints.insert(
|
||||
"risk_management".to_owned(),
|
||||
format!("http://{}:{}", host, port),
|
||||
);
|
||||
}
|
||||
|
||||
if let Ok(host) = std::env::var("FOXHUNT_ML_SIGNALS_HOST") {
|
||||
let port = std::env::var("FOXHUNT_ML_SIGNALS_PORT").unwrap_or("50054".to_owned());
|
||||
config.environment.service_endpoints.insert(
|
||||
"ml_signals".to_owned(),
|
||||
format!("http://{}:{}", host, port),
|
||||
);
|
||||
}
|
||||
|
||||
if let Ok(host) = std::env::var("FOXHUNT_MARKET_DATA_HOST") {
|
||||
let port = std::env::var("FOXHUNT_MARKET_DATA_PORT").unwrap_or("50055".to_owned());
|
||||
config.environment.service_endpoints.insert(
|
||||
"market_data".to_owned(),
|
||||
format!("http://{}:{}", host, port),
|
||||
);
|
||||
}
|
||||
|
||||
if let Ok(host) = std::env::var("FOXHUNT_HEALTH_CHECK_HOST") {
|
||||
let port = std::env::var("FOXHUNT_HEALTH_CHECK_PORT").unwrap_or("50056".to_owned());
|
||||
config.environment.service_endpoints.insert(
|
||||
"health_check".to_owned(),
|
||||
format!("http://{}:{}", host, port),
|
||||
);
|
||||
}
|
||||
|
||||
// Database URLs
|
||||
if let Ok(url) = std::env::var("FOXHUNT_POSTGRES_URL") {
|
||||
config
|
||||
.environment
|
||||
.database_urls
|
||||
.insert("postgres".to_owned(), url);
|
||||
}
|
||||
|
||||
if let Ok(url) = std::env::var("FOXHUNT_REDIS_URL") {
|
||||
config
|
||||
.environment
|
||||
.database_urls
|
||||
.insert("redis".to_owned(), url);
|
||||
}
|
||||
|
||||
if let Ok(url) = std::env::var("FOXHUNT_INFLUXDB_URL") {
|
||||
config
|
||||
.environment
|
||||
.database_urls
|
||||
.insert("influxdb".to_owned(), url);
|
||||
}
|
||||
|
||||
if let Ok(url) = std::env::var("FOXHUNT_CLICKHOUSE_URL") {
|
||||
config
|
||||
.environment
|
||||
.database_urls
|
||||
.insert("clickhouse".to_owned(), url);
|
||||
}
|
||||
|
||||
// Broker configurations
|
||||
if let Ok(_host) = std::env::var("FOXHUNT_IB_HOST") {
|
||||
// Update Interactive Brokers host in broker config
|
||||
// Note: This will be implemented when we update the broker config integration
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Load environment variable overrides
|
||||
fn load_environment_overrides() -> HashMap<String, String> {
|
||||
let mut overrides = HashMap::new();
|
||||
|
||||
// Load Foxhunt-specific environment variables
|
||||
for (key, value) in std::env::vars() {
|
||||
if key.starts_with("FOXHUNT_") {
|
||||
overrides.insert(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
debug!("Loaded {} environment overrides", overrides.len());
|
||||
overrides
|
||||
}
|
||||
|
||||
/// Get current configuration (read-only)
|
||||
pub async fn get_config(&self) -> FoxhuntConfig {
|
||||
self.config.read().await.clone()
|
||||
}
|
||||
|
||||
/// Get specific configuration section
|
||||
pub async fn get_trading_config(&self) -> TradingConfig {
|
||||
self.config.read().await.trading.clone()
|
||||
}
|
||||
|
||||
pub async fn get_ml_config(&self) -> MLConfig {
|
||||
self.config.read().await.ml.clone()
|
||||
}
|
||||
|
||||
pub async fn get_market_data_config(&self) -> MarketDataConfig {
|
||||
self.config.read().await.market_data.clone()
|
||||
}
|
||||
|
||||
/// Update configuration section
|
||||
pub async fn update_trading_config(
|
||||
&self,
|
||||
new_config: TradingConfig,
|
||||
) -> Result<(), ConfigError> {
|
||||
let mut config = self.config.write().await;
|
||||
config.trading = new_config;
|
||||
Self::save_config(&self.config_path, &config)?;
|
||||
info!("Trading configuration updated");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Hot-reload configuration from file
|
||||
pub async fn reload(&self) -> Result<(), ConfigError> {
|
||||
let new_config = Self::load_config(&self.config_path)?;
|
||||
let mut config = self.config.write().await;
|
||||
*config = new_config;
|
||||
info!("Configuration reloaded from {}", self.config_path);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get environment variable with fallback
|
||||
pub fn get_env_var(&self, key: &str, default: Option<&str>) -> Option<String> {
|
||||
// Check environment overrides first
|
||||
if let Some(value) = self.env_overrides.get(key) {
|
||||
return Some(value.clone());
|
||||
}
|
||||
|
||||
// Check system environment
|
||||
if let Ok(value) = std::env::var(key) {
|
||||
return Some(value);
|
||||
}
|
||||
|
||||
// Use default if provided
|
||||
default.map(|s| s.to_owned())
|
||||
}
|
||||
|
||||
/// Validate configuration
|
||||
pub async fn validate(&self) -> Result<Vec<String>, ConfigError> {
|
||||
let config = self.config.read().await;
|
||||
let mut warnings = Vec::new();
|
||||
|
||||
// Validate trading configuration
|
||||
if config.trading.symbols_to_trade.is_empty() {
|
||||
warnings.push("No trading symbols configured".to_owned());
|
||||
}
|
||||
|
||||
// Risk configuration validation moved to risk module
|
||||
|
||||
// Validate environment configuration
|
||||
if config.environment.trading_mode != "paper" && config.environment.trading_mode != "live" {
|
||||
warnings.push("Invalid trading mode, must be 'paper' or 'live'".to_owned());
|
||||
}
|
||||
|
||||
// Validate external API keys are properly configured
|
||||
for (api_name, api_config) in &config.environment.external_apis {
|
||||
if let Some(api_key) = &api_config.api_key {
|
||||
if api_key.contains("PLACEHOLDER") || api_key.is_empty() {
|
||||
warnings.push(format!(
|
||||
"API key for {} is not configured - using placeholder value",
|
||||
api_name
|
||||
));
|
||||
}
|
||||
if api_key.len() < 16 && !api_key.contains("PLACEHOLDER") {
|
||||
warnings.push(format!(
|
||||
"API key for {} appears too short for production use",
|
||||
api_name
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(warnings)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for FoxhuntConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
trading: TradingConfig::default(),
|
||||
ml: MLConfig::default(),
|
||||
market_data: MarketDataConfig::default(),
|
||||
environment: EnvironmentConfig::default(),
|
||||
performance: PerformanceConfig::default(),
|
||||
security: SecurityConfig::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for EnvironmentConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
environment_type: "development".to_owned(),
|
||||
trading_mode: "paper".to_owned(),
|
||||
service_endpoints: {
|
||||
let mut endpoints = HashMap::new();
|
||||
let host = std::env::var("FOXHUNT_SERVICE_HOST")
|
||||
.unwrap_or_else(|_| "localhost".to_owned());
|
||||
endpoints.insert(
|
||||
"trading_engine".to_owned(),
|
||||
std::env::var("FOXHUNT_TRADING_ENGINE_URL")
|
||||
.unwrap_or_else(|_| format!("http://{}:50051", host)),
|
||||
);
|
||||
endpoints.insert(
|
||||
"market_data".to_owned(),
|
||||
std::env::var("FOXHUNT_MARKET_DATA_URL")
|
||||
.unwrap_or_else(|_| format!("http://{}:50052", host)),
|
||||
);
|
||||
endpoints.insert(
|
||||
"risk_management".to_owned(),
|
||||
std::env::var("FOXHUNT_RISK_MANAGEMENT_URL")
|
||||
.unwrap_or_else(|_| format!("http://{}:50053", host)),
|
||||
);
|
||||
endpoints
|
||||
},
|
||||
database_urls: {
|
||||
let mut urls = HashMap::new();
|
||||
let db_host =
|
||||
std::env::var("FOXHUNT_DB_HOST").unwrap_or_else(|_| "localhost".to_owned());
|
||||
urls.insert(
|
||||
"postgres".to_owned(),
|
||||
std::env::var("FOXHUNT_POSTGRES_URL")
|
||||
.unwrap_or_else(|_| format!("postgresql://{}:5432/foxhunt_dev", db_host)),
|
||||
);
|
||||
urls.insert(
|
||||
"redis".to_owned(),
|
||||
std::env::var("FOXHUNT_REDIS_URL")
|
||||
.unwrap_or_else(|_| format!("redis://{}:6379", db_host)),
|
||||
);
|
||||
urls.insert(
|
||||
"influxdb".to_owned(),
|
||||
std::env::var("FOXHUNT_INFLUXDB_URL")
|
||||
.unwrap_or_else(|_| format!("http://{}:8086", db_host)),
|
||||
);
|
||||
urls
|
||||
},
|
||||
external_apis: {
|
||||
let mut apis = HashMap::new();
|
||||
apis.insert(
|
||||
"databento".to_owned(),
|
||||
ExternalApiConfig {
|
||||
base_url: "https://hist.databento.com".to_owned(),
|
||||
api_key: Some(std::env::var("DATABENTO_API_KEY").unwrap_or_else(|_| {
|
||||
eprintln!("WARNING: DATABENTO_API_KEY not set, using demo mode");
|
||||
"DEMO_MODE".to_owned()
|
||||
})),
|
||||
rate_limit_per_second: Some(10),
|
||||
timeout_seconds: Some(10),
|
||||
enabled: true,
|
||||
},
|
||||
);
|
||||
apis.insert(
|
||||
"benzinga".to_owned(),
|
||||
ExternalApiConfig {
|
||||
base_url: "https://api.benzinga.com".to_owned(),
|
||||
api_key: Some(std::env::var("BENZINGA_API_KEY").unwrap_or_else(|_| {
|
||||
eprintln!("WARNING: BENZINGA_API_KEY not set, using demo mode");
|
||||
"DEMO_MODE".to_owned()
|
||||
})),
|
||||
rate_limit_per_second: Some(5),
|
||||
timeout_seconds: Some(10),
|
||||
enabled: true,
|
||||
},
|
||||
);
|
||||
apis.insert(
|
||||
"binance".to_owned(),
|
||||
ExternalApiConfig {
|
||||
base_url: "https://api.binance.com".to_owned(),
|
||||
api_key: None,
|
||||
rate_limit_per_second: Some(10),
|
||||
timeout_seconds: Some(5),
|
||||
enabled: false,
|
||||
},
|
||||
);
|
||||
apis
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for PerformanceConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
target_latency_us: 150,
|
||||
max_latency_us: 1000,
|
||||
thread_pools: {
|
||||
let mut pools = HashMap::new();
|
||||
pools.insert("trading".to_owned(), 4);
|
||||
pools.insert("market_data".to_owned(), 2);
|
||||
pools.insert("risk".to_owned(), 2);
|
||||
pools.insert("ml".to_owned(), 4);
|
||||
pools
|
||||
},
|
||||
cache_settings: {
|
||||
let mut cache = HashMap::new();
|
||||
cache.insert(
|
||||
"position_cache".to_owned(),
|
||||
CacheConfig {
|
||||
max_size: 10000,
|
||||
ttl_seconds: 300,
|
||||
enabled: true,
|
||||
},
|
||||
);
|
||||
cache.insert(
|
||||
"price_cache".to_owned(),
|
||||
CacheConfig {
|
||||
max_size: 50000,
|
||||
ttl_seconds: 60,
|
||||
enabled: true,
|
||||
},
|
||||
);
|
||||
cache
|
||||
},
|
||||
memory_limits: {
|
||||
let mut limits = HashMap::new();
|
||||
limits.insert("ml_model_cache".to_owned(), 1024 * 1024 * 1024); // 1GB
|
||||
limits.insert("market_data_buffer".to_owned(), 512 * 1024 * 1024); // 512MB
|
||||
limits
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for SecurityConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
jwt: JwtConfig {
|
||||
secret: std::env::var("FOXHUNT_JWT_SECRET").unwrap_or_else(|_| {
|
||||
use sha2::{Digest, Sha256};
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(format!(
|
||||
"foxhunt-{}-{}",
|
||||
std::process::id(),
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_nanos()
|
||||
));
|
||||
format!("{:x}", hasher.finalize())
|
||||
}),
|
||||
expiration_seconds: 3600,
|
||||
issuer: "foxhunt-hft".to_owned(),
|
||||
audience: "foxhunt-services".to_owned(),
|
||||
},
|
||||
tls: TlsConfig {
|
||||
enabled: true,
|
||||
cert_path: "/etc/foxhunt/certs/server.crt".to_owned(),
|
||||
key_path: "/etc/foxhunt/certs/server.key".to_owned(),
|
||||
ca_path: Some("/etc/foxhunt/certs/ca.crt".to_owned()),
|
||||
min_version: "1.3".to_owned(),
|
||||
},
|
||||
rate_limiting: RateLimitConfig {
|
||||
enabled: true,
|
||||
requests_per_second: 100,
|
||||
burst_size: 10,
|
||||
},
|
||||
audit: AuditConfig {
|
||||
enabled: true,
|
||||
log_level: "info".to_owned(),
|
||||
log_path: "/var/log/foxhunt/audit.log".to_owned(),
|
||||
retention_days: 90,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Configuration errors
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum ConfigError {
|
||||
#[error("Failed to read config file {path}: {error}")]
|
||||
FileRead { path: String, error: String },
|
||||
|
||||
#[error("Failed to write config file {path}: {error}")]
|
||||
FileWrite { path: String, error: String },
|
||||
|
||||
#[error("Failed to parse configuration: {error}")]
|
||||
ParseError { error: String },
|
||||
|
||||
#[error("Failed to serialize configuration: {error}")]
|
||||
SerializeError { error: String },
|
||||
|
||||
#[error("Configuration validation failed: {error}")]
|
||||
ValidationError { error: String },
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use tempfile::NamedTempFile;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_config_manager_creation() {
|
||||
let temp_file = NamedTempFile::new().unwrap();
|
||||
let config_manager = ConfigManager::new(temp_file.path()).unwrap();
|
||||
|
||||
let config = config_manager.get_config().await;
|
||||
assert_eq!(config.environment.trading_mode, "paper");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_config_validation() {
|
||||
let temp_file = NamedTempFile::new().unwrap();
|
||||
let config_manager = ConfigManager::new(temp_file.path()).unwrap();
|
||||
|
||||
let warnings = config_manager.validate().await.unwrap();
|
||||
// Should have warnings about empty trading symbols and production API keys
|
||||
assert!(!warnings.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_config_update() {
|
||||
let temp_file = NamedTempFile::new().unwrap();
|
||||
let config_manager = ConfigManager::new(temp_file.path()).unwrap();
|
||||
|
||||
let mut trading_config = config_manager.get_trading_config().await;
|
||||
trading_config.symbols_to_trade.push("AAPL".to_string());
|
||||
|
||||
config_manager
|
||||
.update_trading_config(trading_config)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let updated_config = config_manager.get_trading_config().await;
|
||||
assert!(updated_config
|
||||
.symbols_to_trade
|
||||
.contains(&"AAPL".to_string()));
|
||||
}
|
||||
}
|
||||
@@ -1,278 +0,0 @@
|
||||
//! Trading Engine Configuration
|
||||
//!
|
||||
//! Eliminates hardcoded trading parameters and provides dynamic configuration
|
||||
//! for position sizing, order management, and execution settings.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Trading engine configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TradingConfig {
|
||||
/// List of symbols to trade
|
||||
pub symbols_to_trade: Vec<String>,
|
||||
/// Position sizing configuration
|
||||
pub position_sizing: PositionSizingConfig,
|
||||
/// Order execution configuration
|
||||
pub order_execution: OrderExecutionConfig,
|
||||
/// Risk limits per symbol
|
||||
pub symbol_limits: HashMap<String, SymbolLimits>,
|
||||
/// Default fallback prices (only used if market data fails)
|
||||
pub fallback_prices: HashMap<String, f64>,
|
||||
/// Trading session configuration
|
||||
pub trading_sessions: TradingSessionConfig,
|
||||
}
|
||||
|
||||
/// Position sizing configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PositionSizingConfig {
|
||||
/// Use Kelly criterion for position sizing
|
||||
pub use_kelly_criterion: bool,
|
||||
/// Maximum position size as percentage of portfolio
|
||||
pub max_position_pct: f64,
|
||||
/// Minimum position size as percentage of portfolio
|
||||
pub min_position_pct: f64,
|
||||
/// Default position size when Kelly cannot be calculated
|
||||
pub default_position_pct: f64,
|
||||
/// Maximum Kelly fraction to use
|
||||
pub max_kelly_fraction: f64,
|
||||
/// Use fractional Kelly (e.g., 0.5 = half Kelly)
|
||||
pub fractional_kelly: f64,
|
||||
}
|
||||
|
||||
/// Order execution configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct OrderExecutionConfig {
|
||||
/// Default order type: MARKET, LIMIT, STOP, `STOP_LIMIT`
|
||||
pub default_order_type: String,
|
||||
/// Maximum slippage tolerance (basis points)
|
||||
pub max_slippage_bps: u32,
|
||||
/// Order timeout in seconds
|
||||
pub order_timeout_seconds: u64,
|
||||
/// Maximum order size (USD value)
|
||||
pub max_order_value_usd: f64,
|
||||
/// Minimum order size (USD value)
|
||||
pub min_order_value_usd: f64,
|
||||
/// Enable partial fills
|
||||
pub allow_partial_fills: bool,
|
||||
/// Maximum number of retry attempts
|
||||
pub max_retry_attempts: u32,
|
||||
}
|
||||
|
||||
/// Per-symbol trading limits
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SymbolLimits {
|
||||
/// Maximum position value for this symbol
|
||||
pub max_position_value: f64,
|
||||
/// Maximum daily trading volume for this symbol
|
||||
pub max_daily_volume: f64,
|
||||
/// Maximum number of trades per day for this symbol
|
||||
pub max_trades_per_day: u32,
|
||||
/// Minimum time between trades (seconds)
|
||||
pub min_time_between_trades: u64,
|
||||
/// Symbol-specific risk multiplier
|
||||
pub risk_multiplier: f64,
|
||||
}
|
||||
|
||||
/// Trading session configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TradingSessionConfig {
|
||||
/// Market open time (UTC, format: "09:30:00")
|
||||
pub market_open_utc: String,
|
||||
/// Market close time (UTC, format: "16:00:00")
|
||||
pub market_close_utc: String,
|
||||
/// Pre-market trading enabled
|
||||
pub enable_premarket: bool,
|
||||
/// After-hours trading enabled
|
||||
pub enable_afterhours: bool,
|
||||
/// Weekend trading enabled (for crypto/forex)
|
||||
pub enable_weekend: bool,
|
||||
/// Trading holidays (YYYY-MM-DD format)
|
||||
pub trading_holidays: Vec<String>,
|
||||
}
|
||||
|
||||
impl Default for TradingConfig {
|
||||
fn default() -> Self {
|
||||
let mut symbol_limits = HashMap::new();
|
||||
|
||||
// Default limits for major assets
|
||||
for symbol in ["AAPL", "MSFT", "GOOGL", "AMZN", "TSLA"] {
|
||||
symbol_limits.insert(
|
||||
symbol.to_owned(),
|
||||
SymbolLimits {
|
||||
max_position_value: 50000.0, // $50k max position
|
||||
max_daily_volume: 500000.0, // $500k daily volume
|
||||
max_trades_per_day: 10, // 10 trades per day
|
||||
min_time_between_trades: 300, // 5 minutes between trades
|
||||
risk_multiplier: 1.0, // Normal risk
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Higher risk limits for crypto
|
||||
for symbol in ["BTCUSD", "ETHUSD"] {
|
||||
symbol_limits.insert(
|
||||
symbol.to_owned(),
|
||||
SymbolLimits {
|
||||
max_position_value: 25000.0, // $25k max position (higher volatility)
|
||||
max_daily_volume: 250000.0, // $250k daily volume
|
||||
max_trades_per_day: 20, // More frequent trading allowed
|
||||
min_time_between_trades: 60, // 1 minute between trades
|
||||
risk_multiplier: 1.5, // 50% higher risk due to volatility
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
let mut fallback_prices = HashMap::new();
|
||||
fallback_prices.insert("AAPL".to_owned(), 185.75);
|
||||
fallback_prices.insert("MSFT".to_owned(), 425.50);
|
||||
fallback_prices.insert("GOOGL".to_owned(), 2785.30);
|
||||
fallback_prices.insert("AMZN".to_owned(), 3350.25);
|
||||
fallback_prices.insert("TSLA".to_owned(), 255.80);
|
||||
fallback_prices.insert("BTCUSD".to_owned(), 69750.00);
|
||||
fallback_prices.insert("ETHUSD".to_owned(), 3975.50);
|
||||
|
||||
Self {
|
||||
symbols_to_trade: vec![
|
||||
"AAPL".to_owned(),
|
||||
"MSFT".to_owned(),
|
||||
"GOOGL".to_owned(),
|
||||
"AMZN".to_owned(),
|
||||
"TSLA".to_owned(),
|
||||
],
|
||||
position_sizing: PositionSizingConfig {
|
||||
use_kelly_criterion: true,
|
||||
max_position_pct: 0.10, // 10% max position
|
||||
min_position_pct: 0.005, // 0.5% min position
|
||||
default_position_pct: 0.02, // 2% default position
|
||||
max_kelly_fraction: 0.25, // 25% max Kelly
|
||||
fractional_kelly: 0.50, // Use half Kelly
|
||||
},
|
||||
order_execution: OrderExecutionConfig {
|
||||
default_order_type: "LIMIT".to_owned(),
|
||||
max_slippage_bps: 20, // 20 basis points = 0.2%
|
||||
order_timeout_seconds: 30,
|
||||
max_order_value_usd: 100000.0,
|
||||
min_order_value_usd: 100.0,
|
||||
allow_partial_fills: true,
|
||||
max_retry_attempts: 3,
|
||||
},
|
||||
symbol_limits,
|
||||
fallback_prices,
|
||||
trading_sessions: TradingSessionConfig {
|
||||
market_open_utc: "14:30:00".to_owned(), // 9:30 AM EST = 2:30 PM UTC
|
||||
market_close_utc: "21:00:00".to_owned(), // 4:00 PM EST = 9:00 PM UTC
|
||||
enable_premarket: false,
|
||||
enable_afterhours: false,
|
||||
enable_weekend: false,
|
||||
trading_holidays: vec![
|
||||
"2025-01-01".to_owned(), // New Year's Day
|
||||
"2025-01-20".to_owned(), // MLK Day
|
||||
"2025-02-17".to_owned(), // Presidents Day
|
||||
"2025-04-18".to_owned(), // Good Friday
|
||||
"2025-05-26".to_owned(), // Memorial Day
|
||||
"2025-06-19".to_owned(), // Juneteenth
|
||||
"2025-07-04".to_owned(), // Independence Day
|
||||
"2025-09-01".to_owned(), // Labor Day
|
||||
"2025-11-27".to_owned(), // Thanksgiving
|
||||
"2025-12-25".to_owned(), // Christmas
|
||||
],
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TradingConfig {
|
||||
/// Get fallback price for a symbol
|
||||
pub fn get_fallback_price(&self, symbol: &str) -> Option<f64> {
|
||||
self.fallback_prices.get(symbol).copied()
|
||||
}
|
||||
|
||||
/// Get symbol limits for a symbol
|
||||
pub fn get_symbol_limits(&self, symbol: &str) -> Option<&SymbolLimits> {
|
||||
self.symbol_limits.get(symbol)
|
||||
}
|
||||
|
||||
/// Check if symbol is configured for trading
|
||||
pub fn is_symbol_tradeable(&self, symbol: &str) -> bool {
|
||||
self.symbols_to_trade.contains(&symbol.to_owned())
|
||||
}
|
||||
|
||||
/// Get maximum position size for a symbol given portfolio value
|
||||
pub fn get_max_position_size(&self, symbol: &str, portfolio_value: f64) -> f64 {
|
||||
let portfolio_limit = portfolio_value * self.position_sizing.max_position_pct;
|
||||
|
||||
if let Some(symbol_limits) = self.get_symbol_limits(symbol) {
|
||||
portfolio_limit.min(symbol_limits.max_position_value)
|
||||
} else {
|
||||
portfolio_limit
|
||||
}
|
||||
}
|
||||
|
||||
/// Validate trading configuration
|
||||
pub fn validate(&self) -> Result<(), String> {
|
||||
if self.symbols_to_trade.is_empty() {
|
||||
return Err("No symbols configured for trading".to_owned());
|
||||
}
|
||||
|
||||
if self.position_sizing.max_position_pct <= 0.0
|
||||
|| self.position_sizing.max_position_pct > 1.0
|
||||
{
|
||||
return Err("Invalid max position percentage".to_owned());
|
||||
}
|
||||
|
||||
if self.position_sizing.min_position_pct <= 0.0
|
||||
|| self.position_sizing.min_position_pct > self.position_sizing.max_position_pct
|
||||
{
|
||||
return Err("Invalid min position percentage".to_owned());
|
||||
}
|
||||
|
||||
if self.order_execution.max_order_value_usd <= self.order_execution.min_order_value_usd {
|
||||
return Err("Max order value must be greater than min order value".to_owned());
|
||||
}
|
||||
|
||||
// Check for production values in fallback prices
|
||||
for (symbol, price) in &self.fallback_prices {
|
||||
if *price <= 0.0 {
|
||||
return Err(format!("Invalid fallback price for {}: {}", symbol, price));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_default_trading_config() {
|
||||
let config = TradingConfig::default();
|
||||
|
||||
assert!(!config.symbols_to_trade.is_empty());
|
||||
assert!(config.position_sizing.use_kelly_criterion);
|
||||
assert!(config.get_fallback_price("AAPL").is_some());
|
||||
assert!(config.is_symbol_tradeable("AAPL"));
|
||||
assert!(!config.is_symbol_tradeable("INVALID"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_config_validation() {
|
||||
let config = TradingConfig::default();
|
||||
assert!(config.validate().is_ok());
|
||||
|
||||
let mut invalid_config = config.clone();
|
||||
invalid_config.symbols_to_trade.clear();
|
||||
assert!(invalid_config.validate().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_max_position_size() {
|
||||
let config = TradingConfig::default();
|
||||
let portfolio_value = 100000.0;
|
||||
|
||||
let max_position = config.get_max_position_size("AAPL", portfolio_value);
|
||||
assert_eq!(max_position, 10000.0); // 10% of portfolio, limited by symbol limit
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user