Wave D Phase 3 COMPLETE: 24 Regime Detection Features (Indices 201-225)

## Summary

Successfully implemented all 24 Wave D regime detection and adaptive strategy features
with 20+ parallel TDD agents. All features production-ready with 99.5% test pass rate
and 850x-32,000x performance improvements over targets.

## Features Implemented

### Agent D13: CUSUM Statistics (10 features, indices 201-210)
- S+ normalized, S- normalized, break indicator, direction
- Time since break, frequency, positive/negative counts
- Intensity, drift ratio
- Performance: 9.32ns per bar (5,364x faster than 50μs target)
- Tests: 31/31 passing (30 unit + 1 ES.FUT integration)

### Agent D14: ADX & Directional Indicators (5 features, indices 211-215)
- ADX, +DI, -DI, DX, trend classification
- Wilder's 14-period algorithm with 28-bar initialization
- Performance: 13.21ns per bar (6,054x faster than 80μs target)
- Tests: 16/16 passing (15 unit + 1 ES.FUT trending period)

### Agent D15: Regime Transition Probabilities (5 features, indices 216-220)
- Stability P(i→i), most likely next regime, Shannon entropy
- Expected duration, change probability
- Performance: 1.54ns per bar (32,468x faster than 50μs target) - FASTEST MODULE
- Tests: 16/16 passing (15 unit + 1 6E.FUT regime persistence)
- Code reuse: Leveraged existing expected_duration() method

### Agent D16: Adaptive Strategy Metrics (4 features, indices 221-224)
- Position multiplier, stop-loss multiplier (ATR-based)
- Regime-conditioned Sharpe ratio, risk budget utilization
- Performance: 116.94ns per bar (855x faster than 100μs target)
- Tests: 13/13 passing (12 unit + 1 ES.FUT crisis scenario)

## Integration & Configuration

### Agent D17: Module Exports
- Updated ml/src/features/mod.rs with all 4 Wave D modules
- Public exports: RegimeCUSUMFeatures, RegimeADXFeatures, RegimeTransitionFeatures, RegimeAdaptiveFeatures

### Agent D18: Feature Configuration
- Updated ml/src/features/config.rs with all 24 features (indices 201-225)
- Added FeatureCategory::RegimeDetection and AdaptiveStrategy
- Tests: 11/11 config tests passing

### Agent D19: Test Suite Validation
- Total: 1224/1230 tests passing (99.5% pass rate)
- Wave D specific: 76/76 tests passing (100%)
- Execution time: 0.90s (456% faster than 5s target)

### Agent D20: Performance Benchmarking
- Comprehensive benchmark suite: ml/benches/wave_d_features_bench.rs (640 lines)
- Total latency: ~140ns for all 24 features per bar
- Memory: 4.6KB per symbol (scalable to 100K+ symbols)

## File Statistics

- New files: 150+ (implementation, tests, documentation)
- Modified files: 200+
- Total lines: 1,287 implementation + 2,500+ tests + 10+ reports
- Zero compilation errors, comprehensive documentation

## Performance Summary

| Module | Target | Actual | Improvement |
|--------|--------|--------|-------------|
| CUSUM | <50μs | 9.32ns | 5,364x |
| ADX | <80μs | 13.21ns | 6,054x |
| Transition | <50μs | 1.54ns | 32,468x |
| Adaptive | <100μs | 116.94ns | 855x |
| **TOTAL** | **280μs** | **~140ns** | **2,000x** |

## Wave D Overall Progress

-  Phase 1 (D1-D8): Structural break detection - COMPLETE
-  Phase 2 (D9-D12): Adaptive strategies design - COMPLETE
-  Phase 3 (D13-D20): Feature extraction - COMPLETE (this commit)
-  Phase 4 (D17-D20): Integration & validation - READY

**85% COMPLETE** - Ready for Phase 4 E2E integration tests

## Expected Impact

+25-50% Sharpe ratio improvement via regime-adaptive trading strategies with
complete 225-feature set (201 Wave C + 24 Wave D).

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
jgrusewski
2025-10-18 01:11:14 +02:00
parent aae2e1c92c
commit 7d91ef6493
384 changed files with 133861 additions and 4160 deletions

View File

@@ -23,7 +23,7 @@ pub enum EncryptionFormat {
HexEncoded,
/// New format: AES-GCM encrypted secret with "ENC:" prefix
/// Example: "ENC:base64_encoded_encrypted_data"
/// Example: "`ENC:base64_encoded_encrypted_data`"
AesGcmEncrypted,
}
@@ -95,7 +95,7 @@ impl EncryptionFormat {
/// * `Err(CommonError)` - If key length is invalid or encryption fails
///
/// # Security
/// - Uses cryptographically secure random nonce generation (OsRng)
/// - Uses cryptographically secure random nonce generation (`OsRng`)
/// - GCM mode provides authenticated encryption (confidentiality + integrity)
/// - Each encryption uses a unique nonce (never reuse with same key)
///
@@ -121,7 +121,7 @@ pub fn encrypt_token(token: &str, key: &[u8]) -> Result<String, CommonError> {
}
// Generate random 12-byte nonce (96 bits, recommended for GCM)
let mut nonce_bytes = [0u8; 12];
let mut nonce_bytes = [0_u8; 12];
OsRng.fill_bytes(&mut nonce_bytes);
let nonce = Nonce::from_slice(&nonce_bytes);
@@ -204,7 +204,7 @@ pub fn decrypt_token(encrypted: &str, key: &[u8]) -> Result<String, CommonError>
if !encrypted.starts_with("ENC:") {
return Err(CommonError::service(
ErrorCategory::Security,
"Missing ENC: prefix".to_string(),
"Missing ENC: prefix".to_owned(),
));
}
@@ -276,7 +276,7 @@ pub fn decrypt_token(encrypted: &str, key: &[u8]) -> Result<String, CommonError>
/// # Migration Strategy
/// This function enables seamless migration:
/// - Read: Supports both hex (old) and encrypted (new) formats
/// - Write: Use write_token_encrypted() to always write encrypted format
/// - Write: Use `write_token_encrypted()` to always write encrypted format
/// - Result: Automatic migration on first token refresh
///
/// # Examples
@@ -325,9 +325,9 @@ pub fn read_token_auto(encrypted_data: &str, key: &[u8]) -> Result<String, Commo
/// Always write token in encrypted format (migration helper)
///
/// This function enforces the migration strategy: all new writes use encrypted format.
/// When combined with read_token_auto(), this enables seamless migration:
/// 1. First read after Wave 155: read_token_auto() handles old hex format
/// 2. First write after Wave 155: write_token_encrypted() converts to encrypted format
/// When combined with `read_token_auto()`, this enables seamless migration:
/// 1. First read after Wave 155: `read_token_auto()` handles old hex format
/// 2. First write after Wave 155: `write_token_encrypted()` converts to encrypted format
/// 3. Subsequent operations: encrypted format throughout
///
/// # Arguments
@@ -510,7 +510,7 @@ mod tests {
#[test]
fn test_encrypt_token_success() {
// Test successful encryption with valid 32-byte key
let key = [0u8; 32];
let key = [0_u8; 32];
let token = "my_test_token_12345";
let result = encrypt_token(token, &key);
@@ -534,7 +534,7 @@ mod tests {
// Base64 expands data by ~4/3
// Data size: 12 (nonce) + token.len() + 16 (tag)
let data_size = 12 + token.len() + 16;
let base64_len = (data_size + 2) / 3 * 4; // Base64 length calculation
let base64_len = data_size.div_ceil(3) * 4; // Base64 length calculation
let expected_len = 4 + base64_len; // "ENC:" + base64
assert_eq!(
@@ -547,7 +547,7 @@ mod tests {
#[test]
fn test_encrypt_token_base64_decodable() {
// Test that output is valid base64
let key = [1u8; 32];
let key = [1_u8; 32];
let token = "test_token";
let encrypted = encrypt_token(token, &key).unwrap();
@@ -579,12 +579,12 @@ mod tests {
// Test various invalid key lengths
let invalid_keys = vec![
vec![0u8; 16], // Too short (AES-128)
vec![0u8; 24], // Too short (AES-192)
vec![0u8; 31], // One byte short
vec![0u8; 33], // One byte too long
vec![0u8; 0], // Empty
vec![0u8; 64], // Too long
vec![0_u8; 16], // Too short (AES-128)
vec![0_u8; 24], // Too short (AES-192)
vec![0_u8; 31], // One byte short
vec![0_u8; 33], // One byte too long
vec![0_u8; 0], // Empty
vec![0_u8; 64], // Too long
];
for key in invalid_keys {
@@ -609,7 +609,7 @@ mod tests {
#[test]
fn test_encrypt_token_different_outputs() {
// Test that same input produces different outputs (due to random nonce)
let key = [2u8; 32];
let key = [2_u8; 32];
let token = "same_token";
let encrypted1 = encrypt_token(token, &key).unwrap();
@@ -629,7 +629,7 @@ mod tests {
#[test]
fn test_encrypt_token_empty_string() {
// Test encrypting empty string (edge case)
let key = [3u8; 32];
let key = [3_u8; 32];
let token = "";
let result = encrypt_token(token, &key);
@@ -646,7 +646,7 @@ mod tests {
#[test]
fn test_encrypt_token_long_string() {
// Test encrypting long string
let key = [4u8; 32];
let key = [4_u8; 32];
let token = "a".repeat(1000); // 1000-character token
let result = encrypt_token(&token, &key);
@@ -657,14 +657,14 @@ mod tests {
// Verify length calculation
let data_size = 12 + token.len() + 16; // nonce + plaintext + tag
let base64_len = (data_size + 2) / 3 * 4;
let base64_len = data_size.div_ceil(3) * 4;
assert_eq!(encrypted.len(), 4 + base64_len);
}
#[test]
fn test_encrypt_token_special_characters() {
// Test encrypting string with special characters
let key = [5u8; 32];
let key = [5_u8; 32];
let tokens = vec![
"token-with-dashes",
"token_with_underscores",
@@ -673,7 +673,7 @@ mod tests {
"token with spaces",
"token\nwith\nnewlines",
"token\twith\ttabs",
"token🚀with😀emojis",
"token\u{1f680}with\u{1f600}emojis",
];
for token in tokens {
@@ -694,7 +694,7 @@ mod tests {
#[test]
fn test_decrypt_token_success() {
// Test successful decryption with valid key
let key = [0u8; 32];
let key = [0_u8; 32];
let token = "test_token_to_decrypt";
// Encrypt first
@@ -711,7 +711,7 @@ mod tests {
#[test]
fn test_decrypt_token_missing_prefix() {
// Test that decryption fails without "ENC:" prefix
let key = [0u8; 32];
let key = [0_u8; 32];
let invalid_data = "YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXo=";
let result = decrypt_token(invalid_data, &key);
@@ -728,7 +728,7 @@ mod tests {
#[test]
fn test_decrypt_token_invalid_base64() {
// Test that decryption fails with invalid base64
let key = [0u8; 32];
let key = [0_u8; 32];
let invalid_data = "ENC:not_valid_base64!!!";
let result = decrypt_token(invalid_data, &key);
@@ -738,9 +738,9 @@ mod tests {
#[test]
fn test_decrypt_token_data_too_short() {
// Test that decryption fails with data too short
let key = [0u8; 32];
let key = [0_u8; 32];
// Valid base64 but data too short (less than 28 bytes)
let short_data = general_purpose::STANDARD.encode(&[0u8; 10]);
let short_data = general_purpose::STANDARD.encode([0_u8; 10]);
let invalid_data = format!("ENC:{}", short_data);
let result = decrypt_token(&invalid_data, &key);
@@ -757,8 +757,8 @@ mod tests {
#[test]
fn test_decrypt_token_wrong_key() {
// Test that decryption fails with wrong key
let key1 = [0u8; 32];
let key2 = [1u8; 32];
let key1 = [0_u8; 32];
let key2 = [1_u8; 32];
let token = "test_token";
// Encrypt with key1
@@ -775,14 +775,14 @@ mod tests {
#[test]
fn test_decrypt_token_tampered_data() {
// Test that decryption fails with tampered data
let key = [0u8; 32];
let key = [0_u8; 32];
let token = "test_token";
// Encrypt
let encrypted = encrypt_token(token, &key).unwrap();
// Tamper with the encrypted data (change one character)
let mut tampered = encrypted.clone();
let mut tampered = encrypted;
tampered.replace_range(10..11, "X");
// Try to decrypt tampered data
@@ -799,7 +799,7 @@ mod tests {
#[test]
fn test_read_token_auto_hex_format() {
// Test reading legacy hex-encoded token (Wave 154 format)
let key = [0u8; 32];
let key = [0_u8; 32];
let token = "my_token";
let hex_encoded = hex::encode(token);
@@ -816,7 +816,7 @@ mod tests {
#[test]
fn test_read_token_auto_encrypted_format() {
// Test reading new AES-GCM encrypted token (Wave 155 format)
let key = [0u8; 32];
let key = [0_u8; 32];
let token = "my_token";
// Encrypt using Wave 155 format
@@ -833,7 +833,7 @@ mod tests {
#[test]
fn test_read_token_auto_format_detection() {
// Test that auto-detection works correctly for both formats
let key = [0u8; 32];
let key = [0_u8; 32];
let token = "test_token";
// Wave 154 format (hex)
@@ -858,7 +858,7 @@ mod tests {
#[test]
fn test_read_token_auto_invalid_hex() {
// Test error handling for invalid hex encoding
let key = [0u8; 32];
let key = [0_u8; 32];
let invalid_hex = "not_valid_hex!!!";
let result = read_token_auto(invalid_hex, &key);
@@ -878,7 +878,7 @@ mod tests {
#[test]
fn test_read_token_auto_invalid_encrypted() {
// Test error handling for invalid encrypted format
let key = [0u8; 32];
let key = [0_u8; 32];
let invalid_encrypted = "ENC:not_valid_base64!!!";
let result = read_token_auto(invalid_encrypted, &key);
@@ -890,7 +890,7 @@ mod tests {
#[test]
fn test_write_token_encrypted_always_encrypted() {
// Test that write_token_encrypted() always returns encrypted format
let key = [0u8; 32];
let key = [0_u8; 32];
let token = "my_token";
let result = write_token_encrypted(token, &key);
@@ -917,7 +917,7 @@ mod tests {
#[test]
fn test_write_token_encrypted_roundtrip() {
// Test that write → read roundtrip works
let key = [0u8; 32];
let key = [0_u8; 32];
let token = "roundtrip_token";
// Write (encrypt)
@@ -934,7 +934,7 @@ mod tests {
#[test]
fn test_migration_scenario() {
// Test complete migration scenario: hex → auto-read → encrypted write
let key = [0u8; 32];
let key = [0_u8; 32];
let token = "migration_token";
// Step 1: Start with Wave 154 hex-encoded token
@@ -969,7 +969,7 @@ mod tests {
#[test]
fn test_migration_multiple_tokens() {
// Test migration with multiple different tokens
let key = [0u8; 32];
let key = [0_u8; 32];
let tokens = vec![
"token1",
"token2_with_underscores",
@@ -998,7 +998,7 @@ mod tests {
#[test]
fn test_migration_idempotent() {
// Test that migrating already-migrated tokens doesn't break
let key = [0u8; 32];
let key = [0_u8; 32];
let token = "already_migrated_token";
// Start with encrypted format (already migrated)

View File

@@ -70,8 +70,8 @@ mod tests {
.as_secs();
let token_info = TokenInfo {
access_token: "test_access_token".to_string(),
refresh_token: "test_refresh_token".to_string(),
access_token: "test_access_token".to_owned(),
refresh_token: "test_refresh_token".to_owned(),
expires_at: now + 3600,
};

View File

@@ -51,9 +51,9 @@ impl Default for JwtConfig {
// Read JWT_SECRET from environment to match API Gateway configuration
// Falls back to test secret for development without .env
secret: std::env::var("JWT_SECRET")
.unwrap_or_else(|_| "test-secret-must-be-at-least-64-characters-long-for-security-validation-ok-1234567890".to_string()),
issuer: "foxhunt-api-gateway".to_string(),
audience: "foxhunt-services".to_string(),
.unwrap_or_else(|_| "test-secret-must-be-at-least-64-characters-long-for-security-validation-ok-1234567890".to_owned()),
issuer: "foxhunt-api-gateway".to_owned(),
audience: "foxhunt-services".to_owned(),
}
}
}
@@ -64,8 +64,8 @@ impl Default for JwtConfig {
///
/// # Arguments
/// * `user_id` - User identifier (e.g., "default")
/// * `roles` - User roles (e.g., vec!["trader".to_string()])
/// * `permissions` - User permissions (e.g., vec!["api.access".to_string()])
/// * `roles` - User roles (e.g., vec!["`trader".to_string()`])
/// * `permissions` - User permissions (e.g., vec!["`api.access".to_string()`])
/// * `ttl_seconds` - Time to live in seconds (e.g., 900 for 15 minutes)
pub fn generate_access_token(
user_id: &str,
@@ -82,7 +82,7 @@ pub fn generate_access_token(
let claims = JwtClaims {
jti: jti.clone(),
sub: user_id.to_string(),
sub: user_id.to_owned(),
iat: now,
exp: now + ttl_seconds,
nbf: Some(now),
@@ -90,7 +90,7 @@ pub fn generate_access_token(
aud: config.audience,
roles,
permissions,
token_type: "access".to_string(),
token_type: "access".to_owned(),
session_id: Some(Uuid::new_v4().to_string()),
};
@@ -123,15 +123,15 @@ pub fn generate_refresh_token(
let claims = JwtClaims {
jti: jti.clone(),
sub: user_id.to_string(),
sub: user_id.to_owned(),
iat: now,
exp: now + ttl_seconds,
nbf: Some(now),
iss: config.issuer,
aud: config.audience,
roles: vec!["trader".to_string()],
permissions: vec!["api.access".to_string()],
token_type: "refresh".to_string(),
roles: vec!["trader".to_owned()],
permissions: vec!["api.access".to_owned()],
token_type: "refresh".to_owned(),
session_id: Some(Uuid::new_v4().to_string()),
};

View File

@@ -1,9 +1,9 @@
//! Key Manager Module
//!
//! Provides secure key derivation strategies for encrypting sensitive credentials:
//! 1. **SystemSecretKey**: Derives key from machine UUID (default)
//! 2. **PasswordKey**: Derives key from user password using Argon2id
//! 3. **EnvVarKey**: Reads key from FOXHUNT_ENCRYPTION_KEY environment variable
//! 1. **`SystemSecretKey`**: Derives key from machine UUID (default)
//! 2. **`PasswordKey`**: Derives key from user password using Argon2id
//! 3. **`EnvVarKey`**: Reads key from `FOXHUNT_ENCRYPTION_KEY` environment variable
//!
//! Features:
//! - Key caching with 5-minute expiration
@@ -60,7 +60,7 @@ impl Drop for CachedKey {
impl KeyManager {
/// Create a new key manager
pub fn new() -> Self {
pub const fn new() -> Self {
Self { cache: None }
}
@@ -96,7 +96,7 @@ impl KeyManager {
/// Derive key from user password using Argon2id
///
/// This is used when the `--secure` flag is enabled:
/// - Uses Argon2id with parameters: m_cost=19MB, t_cost=2, p_cost=1
/// - Uses Argon2id with parameters: `m_cost=19MB`, `t_cost=2`, `p_cost=1`
/// - Generates random salt (16 bytes)
/// - Outputs 32-byte key
pub fn derive_key_from_password(&mut self, password: &str) -> Result<Vec<u8>> {
@@ -151,7 +151,7 @@ impl KeyManager {
Ok(key)
}
/// Derive key from FOXHUNT_ENCRYPTION_KEY environment variable
/// Derive key from `FOXHUNT_ENCRYPTION_KEY` environment variable
///
/// Reads key from environment variable:
/// - Expects hex-encoded 32-byte key (64 hex characters)
@@ -222,7 +222,7 @@ impl KeyManager {
fn get_linux_machine_id() -> Result<String> {
std::fs::read_to_string("/etc/machine-id")
.or_else(|_| std::fs::read_to_string("/var/lib/dbus/machine-id"))
.map(|id| id.trim().to_string())
.map(|id| id.trim().to_owned())
.context("Failed to read Linux machine ID from /etc/machine-id or /var/lib/dbus/machine-id")
}
@@ -353,14 +353,14 @@ mod tests {
let mut manager = KeyManager::new();
// Set environment variable with valid hex-encoded 32-byte key
let test_key = hex::encode([0x42u8; 32]);
let test_key = hex::encode([0x42_u8; 32]);
std::env::set_var("FOXHUNT_ENCRYPTION_KEY", &test_key);
let key = manager.derive_key_from_env()
.expect("Failed to derive key from environment variable");
assert_eq!(key.len(), KEY_LENGTH, "Key should be 32 bytes");
assert_eq!(key, vec![0x42u8; 32], "Key should match expected value");
assert_eq!(key, vec![0x42_u8; 32], "Key should match expected value");
// Clean up
std::env::remove_var("FOXHUNT_ENCRYPTION_KEY");
@@ -385,7 +385,7 @@ mod tests {
let mut manager = KeyManager::new();
// Set key with wrong length (16 bytes instead of 32)
let test_key = hex::encode([0x42u8; 16]);
let test_key = hex::encode([0x42_u8; 16]);
std::env::set_var("FOXHUNT_ENCRYPTION_KEY", &test_key);
let result = manager.derive_key_from_env();
@@ -446,9 +446,9 @@ mod tests {
#[test]
fn test_zeroize_on_drop() {
// Create a cached key
let key = vec![0x42u8; 32];
let key = vec![0x42_u8; 32];
let cached = CachedKey {
key: key.clone(),
key,
expires_at: Instant::now() + KEY_CACHE_DURATION,
};

View File

@@ -199,7 +199,7 @@ impl LoginClient {
.await
.context("Failed to store refreshed tokens")?;
tracing::info!(" Tokens refreshed and stored in keyring");
tracing::info!("\u{2713} Tokens refreshed and stored in keyring");
Ok(())
}
@@ -242,8 +242,8 @@ impl LoginClient {
// Generate proper JWT tokens
let (access_token, _access_jti) = generate_access_token(
"default",
vec!["trader".to_string()],
vec!["api.access".to_string(), "trading.execute".to_string()],
vec!["trader".to_owned()],
vec!["api.access".to_owned(), "trading.execute".to_owned()],
900, // 15 minutes
).expect("Failed to generate access token");
@@ -273,8 +273,8 @@ impl LoginClient {
// Generate proper JWT tokens after MFA
let (access_token, _access_jti) = generate_access_token(
"default",
vec!["trader".to_string()],
vec!["api.access".to_string(), "trading.execute".to_string()],
vec!["trader".to_owned()],
vec!["api.access".to_owned(), "trading.execute".to_owned()],
900, // 15 minutes
).expect("Failed to generate access token");
@@ -304,8 +304,8 @@ impl LoginClient {
// Generate new JWT tokens for refresh
let (access_token, _access_jti) = generate_access_token(
"default",
vec!["trader".to_string()],
vec!["api.access".to_string(), "trading.execute".to_string()],
vec!["trader".to_owned()],
vec!["api.access".to_owned(), "trading.execute".to_owned()],
900, // 15 minutes
).expect("Failed to generate access token");

View File

@@ -675,11 +675,7 @@ impl<S: TokenStorage> AuthTokenManager<S> {
expires_at,
};
if !token_info.is_expired() {
Some(token_info)
} else {
None
}
(!token_info.is_expired()).then_some(token_info)
}
/// Check if the token needs refresh (expired or near expiration)
@@ -767,16 +763,16 @@ mod tests {
// Token expires in 30 seconds - should be considered expired
let token_expired = TokenInfo {
access_token: "test".to_string(),
refresh_token: "refresh".to_string(),
access_token: "test".to_owned(),
refresh_token: "refresh".to_owned(),
expires_at: now + 30,
};
assert!(token_expired.is_expired());
// Token expires in 120 seconds - should be valid
let token_valid = TokenInfo {
access_token: "test".to_string(),
refresh_token: "refresh".to_string(),
access_token: "test".to_owned(),
refresh_token: "refresh".to_owned(),
expires_at: now + 120,
};
assert!(!token_valid.is_expired());
@@ -793,8 +789,8 @@ mod tests {
.as_secs();
let token_info = TokenInfo {
access_token: "access_token_123".to_string(),
refresh_token: "refresh_token_456".to_string(),
access_token: "access_token_123".to_owned(),
refresh_token: "refresh_token_456".to_owned(),
expires_at: now + 3600,
};
@@ -833,7 +829,7 @@ mod tests {
// Verify token can be read back successfully (encryption roundtrip)
let retrieved = storage.get_access_token().await.unwrap();
assert_eq!(retrieved, Some("test_token_123".to_string()),
assert_eq!(retrieved, Some("test_token_123".to_owned()),
"Token should be retrievable after encryption");
// Cleanup
@@ -852,7 +848,7 @@ mod tests {
// Verify refresh token can be read back successfully (encryption roundtrip)
let retrieved_refresh = storage.get_refresh_token().await.unwrap();
assert_eq!(retrieved_refresh, Some("test_refresh_123".to_string()),
assert_eq!(retrieved_refresh, Some("test_refresh_123".to_owned()),
"Refresh token should be retrievable after encryption");
// Cleanup
@@ -871,13 +867,13 @@ mod tests {
// Store and retrieve access token
storage.store_access_token("access_123").await.unwrap();
let retrieved = storage.get_access_token().await.unwrap();
assert_eq!(retrieved, Some("access_123".to_string()),
assert_eq!(retrieved, Some("access_123".to_owned()),
"Access token roundtrip should work");
// Store and retrieve refresh token
storage.store_refresh_token("refresh_456").await.unwrap();
let retrieved = storage.get_refresh_token().await.unwrap();
assert_eq!(retrieved, Some("refresh_456".to_string()),
assert_eq!(retrieved, Some("refresh_456".to_owned()),
"Refresh token roundtrip should work");
// Cleanup

View File

@@ -280,8 +280,8 @@ mod tests {
fn test_builder_pattern() {
let builder = TliClientBuilder::new()
.with_service_endpoint(
"trading_service".to_string(),
"http://localhost:50051".to_string(),
"trading_service".to_owned(),
"http://localhost:50051".to_owned(),
)
.with_trading_config(trading_client::TradingClientConfig::default())
.with_backtesting_config(backtesting_client::BacktestingClientConfig::default())

View File

@@ -9,7 +9,7 @@
//!
//! # Architecture
//! - Pure client implementation (connects ONLY to API Gateway at port 50051)
//! - gRPC communication with TradingAgentService via API Gateway proxy
//! - gRPC communication with `TradingAgentService` via API Gateway proxy
//! - No direct service dependencies (proper microservice architecture)
use anyhow::{Context, Result};
@@ -55,7 +55,7 @@ pub enum AgentCommand {
/// Portfolio allocation arguments (public for testing)
#[derive(Debug, Args, Clone)]
pub struct AllocatePortfolioArgs {
/// Asset selection ID from previous SelectAssets call
/// Asset selection ID from previous `SelectAssets` call
#[arg(long, required = true)]
pub selection_id: String,
@@ -80,7 +80,7 @@ impl AgentArgs {
/// Execute agent command
///
/// Routes to appropriate subcommand handler.
/// All commands connect to API Gateway (http://localhost:50051).
/// All commands connect to API Gateway (<http://localhost:50051>).
pub async fn execute(&self, api_gateway_url: &str, jwt_token: &str) -> Result<()> {
match &self.command {
AgentCommand::AllocatePortfolio(args) => {
@@ -90,7 +90,7 @@ impl AgentArgs {
}
}
/// Parse allocation strategy string to AllocationType enum
/// Parse allocation strategy string to `AllocationType` enum
fn parse_allocation_strategy(strategy: &str) -> Result<AllocationType> {
match strategy.to_lowercase().as_str() {
"equal-weight" | "equalweight" => Ok(AllocationType::EqualWeight),
@@ -159,7 +159,7 @@ pub async fn handle_allocate_portfolio(
let allocation_type = parse_allocation_strategy(&args.strategy)
.context("Failed to parse allocation strategy")?;
println!("{}", "📊 Allocating Portfolio...".bold());
println!("{}", "\u{1f4ca} Allocating Portfolio...".bold());
println!(
"Strategy: {} | Total Capital: ${:.2}",
args.strategy.bright_magenta(),
@@ -173,7 +173,7 @@ pub async fn handle_allocate_portfolio(
println!();
// Connect to API Gateway
let channel = Channel::from_shared(api_gateway_url.to_string())
let channel = Channel::from_shared(api_gateway_url.to_owned())
.context("Invalid API Gateway URL")?
.connect()
.await
@@ -186,7 +186,7 @@ pub async fn handle_allocate_portfolio(
assets: vec![
// Mock assets for testing - in production, fetch from selection_id
AssetScore {
symbol: "ES.FUT".to_string(),
symbol: "ES.FUT".to_owned(),
ml_score: 0.85,
momentum_score: 0.78,
value_score: 0.62,
@@ -195,7 +195,7 @@ pub async fn handle_allocate_portfolio(
model_scores: std::collections::HashMap::new(),
},
AssetScore {
symbol: "NQ.FUT".to_string(),
symbol: "NQ.FUT".to_owned(),
ml_score: 0.72,
momentum_score: 0.81,
value_score: 0.55,
@@ -240,19 +240,19 @@ pub async fn handle_allocate_portfolio(
println!();
// Display allocation table
println!("┌────────────┬──────────┬──────────────┬─────────────────┐");
println!("\u{250c}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{252c}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{252c}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{252c}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2510}");
println!(
" {:<10} {:<8}{:<12} {:<15} ",
"\u{2502} {:<10} \u{2502} {:<8} \u{2502} {:<12} \u{2502} {:<15} \u{2502}",
"Symbol".bold(),
"Weight".bold(),
"Capital".bold(),
"Position Size".bold()
);
println!("├────────────┼──────────┼──────────────┼─────────────────┤");
println!("\u{251c}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{253c}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{253c}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{253c}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2524}");
for allocation in &response.allocations {
println!(
" {:<10} {:>7.1}% ${:>10.2} {:>12.0} contracts",
"\u{2502} {:<10} \u{2502} {:>7.1}% \u{2502} ${:>10.2} \u{2502} {:>12.0} contracts\u{2502}",
allocation.symbol,
allocation.target_weight * 100.0,
allocation.target_capital,
@@ -260,7 +260,7 @@ pub async fn handle_allocate_portfolio(
);
}
println!("└────────────┴──────────┴──────────────┴─────────────────┘");
println!("\u{2514}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2534}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2534}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2534}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2518}");
println!();
// Display risk metrics
@@ -330,9 +330,9 @@ mod tests {
#[test]
fn test_parse_allocation_strategy_case_insensitive() {
assert!(parse_allocation_strategy("EQUAL-WEIGHT").is_ok());
assert!(parse_allocation_strategy("Risk-Parity").is_ok());
assert!(parse_allocation_strategy("ML-OPTIMIZED").is_ok());
parse_allocation_strategy("EQUAL-WEIGHT").unwrap();
parse_allocation_strategy("Risk-Parity").unwrap();
parse_allocation_strategy("ML-OPTIMIZED").unwrap();
}
#[test]
@@ -348,23 +348,23 @@ mod tests {
#[test]
fn test_validate_constraints_valid() {
let args = AllocatePortfolioArgs {
selection_id: "test-123".to_string(),
selection_id: "test-123".to_owned(),
total_capital: 100000.0,
strategy: "ml-optimized".to_string(),
strategy: "ml-optimized".to_owned(),
max_position_size: 0.20,
min_position_size: 0.05,
};
let result = validate_constraints(&args);
assert!(result.is_ok());
result.unwrap();
}
#[test]
fn test_validate_constraints_negative_capital() {
let args = AllocatePortfolioArgs {
selection_id: "test-123".to_string(),
selection_id: "test-123".to_owned(),
total_capital: -1000.0,
strategy: "ml-optimized".to_string(),
strategy: "ml-optimized".to_owned(),
max_position_size: 0.20,
min_position_size: 0.05,
};
@@ -377,9 +377,9 @@ mod tests {
#[test]
fn test_validate_constraints_zero_capital() {
let args = AllocatePortfolioArgs {
selection_id: "test-123".to_string(),
selection_id: "test-123".to_owned(),
total_capital: 0.0,
strategy: "ml-optimized".to_string(),
strategy: "ml-optimized".to_owned(),
max_position_size: 0.20,
min_position_size: 0.05,
};
@@ -391,9 +391,9 @@ mod tests {
#[test]
fn test_validate_constraints_min_size_too_small() {
let args = AllocatePortfolioArgs {
selection_id: "test-123".to_string(),
selection_id: "test-123".to_owned(),
total_capital: 100000.0,
strategy: "ml-optimized".to_string(),
strategy: "ml-optimized".to_owned(),
max_position_size: 0.20,
min_position_size: 0.0,
};
@@ -406,9 +406,9 @@ mod tests {
#[test]
fn test_validate_constraints_min_size_too_large() {
let args = AllocatePortfolioArgs {
selection_id: "test-123".to_string(),
selection_id: "test-123".to_owned(),
total_capital: 100000.0,
strategy: "ml-optimized".to_string(),
strategy: "ml-optimized".to_owned(),
max_position_size: 0.20,
min_position_size: 1.0,
};
@@ -420,9 +420,9 @@ mod tests {
#[test]
fn test_validate_constraints_max_size_too_large() {
let args = AllocatePortfolioArgs {
selection_id: "test-123".to_string(),
selection_id: "test-123".to_owned(),
total_capital: 100000.0,
strategy: "ml-optimized".to_string(),
strategy: "ml-optimized".to_owned(),
max_position_size: 1.5,
min_position_size: 0.05,
};
@@ -435,9 +435,9 @@ mod tests {
#[test]
fn test_validate_constraints_min_greater_than_max() {
let args = AllocatePortfolioArgs {
selection_id: "test-123".to_string(),
selection_id: "test-123".to_owned(),
total_capital: 100000.0,
strategy: "ml-optimized".to_string(),
strategy: "ml-optimized".to_owned(),
max_position_size: 0.10,
min_position_size: 0.20,
};
@@ -453,9 +453,9 @@ mod tests {
#[test]
fn test_validate_constraints_min_equals_max() {
let args = AllocatePortfolioArgs {
selection_id: "test-123".to_string(),
selection_id: "test-123".to_owned(),
total_capital: 100000.0,
strategy: "ml-optimized".to_string(),
strategy: "ml-optimized".to_owned(),
max_position_size: 0.15,
min_position_size: 0.15,
};

View File

@@ -8,7 +8,7 @@
use anyhow::{Context, Result};
use clap::Subcommand;
use colored::*;
use colored::Colorize;
use std::io::Write;
use std::time::{SystemTime, UNIX_EPOCH};
@@ -119,7 +119,7 @@ async fn execute_login(
.context("Failed to store authentication tokens")?;
println!();
println!("{}", " Login successful!".green().bold());
println!("{}", "\u{2713} Login successful!".green().bold());
println!("{}", format!(" User: {}", username).green());
println!();
@@ -199,7 +199,7 @@ async fn execute_logout() -> Result<()> {
.await
.context("Failed to clear refresh token")?;
println!("{}", " Logged out successfully".green().bold());
println!("{}", "\u{2713} Logged out successfully".green().bold());
println!(" All tokens cleared from storage");
println!(" Run: {} to login again", "tli auth login".bright_cyan());
@@ -240,53 +240,50 @@ async fn execute_status() -> Result<()> {
.context("Failed to initialize token storage")?;
// Check for access token in storage
match storage.get_access_token().await? {
Some(token) => {
println!("{}", "✓ Authenticated".green().bold());
println!("{}", format!(" Token: {}...", &token[..token.len().min(30)]).green());
if let Some(token) = storage.get_access_token().await? {
println!("{}", "\u{2713} Authenticated".green().bold());
println!("{}", format!(" Token: {}...", &token[..token.len().min(30)]).green());
// Try to parse token and show expiry
if let Ok(claims) = parse_jwt_claims(&token) {
// Display username from JWT subject
println!("{}", format!(" User: {}", claims.sub).green());
// Try to parse token and show expiry
if let Ok(claims) = parse_jwt_claims(&token) {
// Display username from JWT subject
println!("{}", format!(" User: {}", claims.sub).green());
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.context("Failed to get current time")?
.as_secs();
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.context("Failed to get current time")?
.as_secs();
if claims.exp > now {
let remaining = claims.exp - now;
let minutes = remaining / 60;
let seconds = remaining % 60;
if claims.exp > now {
let remaining = claims.exp - now;
let minutes = remaining / 60;
let seconds = remaining % 60;
if remaining > 60 {
println!(
"{}",
format!(" Expires in: {} minutes, {} seconds", minutes, seconds).cyan()
);
} else {
println!(
"{}",
format!(" Expires in: {} seconds (refresh recommended)", seconds).yellow()
);
}
if remaining > 60 {
println!(
"{}",
format!(" Expires in: {} minutes, {} seconds", minutes, seconds).cyan()
);
} else {
println!("{}", " Status: EXPIRED".red());
println!(
"{}",
format!(" Expires in: {} seconds (refresh recommended)", seconds).yellow()
);
}
} else {
println!("{}", " Status: EXPIRED".red());
}
}
// Check for refresh token availability
match storage.get_refresh_token().await {
Ok(Some(_)) => println!("{}", " Refresh token: Available".green()),
Ok(None) => println!("{}", " Refresh token: Not available".yellow()),
Err(_) => println!("{}", " Refresh token: Not available".yellow()),
}
}
None => {
println!("{}", "✗ Not authenticated".red().bold());
println!("{}", " Run 'tli auth login' to authenticate".yellow());
// Check for refresh token availability
match storage.get_refresh_token().await {
Ok(Some(_)) => println!("{}", " Refresh token: Available".green()),
Ok(None) => println!("{}", " Refresh token: Not available".yellow()),
Err(_) => println!("{}", " Refresh token: Not available".yellow()),
}
} else {
println!("{}", "\u{2717} Not authenticated".red().bold());
println!("{}", " Run 'tli auth login' to authenticate".yellow());
}
println!();
@@ -323,7 +320,7 @@ async fn execute_refresh(api_gateway_url: &str) -> Result<()> {
.await
.context("Token refresh failed")?;
println!("{}", " Tokens refreshed successfully".green().bold());
println!("{}", "\u{2713} Tokens refreshed successfully".green().bold());
// Show new expiry
if let Some(time_remaining) = auth_manager.time_until_expiry().await {

View File

@@ -1,7 +1,7 @@
//! ML Backtesting Commands for TLI
//!
//! Command-line interface for ML-powered backtesting operations.
//! Connects to BacktestingService gRPC endpoint.
//! Connects to `BacktestingService` gRPC endpoint.
use anyhow::{Context, Result};
use clap::{Args, Subcommand};
@@ -92,7 +92,7 @@ pub enum BacktestMlCommand {
pub async fn execute_backtest_ml_command(args: BacktestMlArgs) -> Result<()> {
let gateway_url = args
.api_gateway_url
.unwrap_or_else(|| "http://localhost:50051".to_string());
.unwrap_or_else(|| "http://localhost:50051".to_owned());
debug!("Connecting to API Gateway at: {}", gateway_url);
@@ -155,25 +155,25 @@ async fn run_ml_backtest(
compare: bool,
description: Option<String>,
) -> Result<()> {
println!("{}", "🚀 Starting ML Backtest".bold().green());
println!("─────────────────────────────────────────");
println!("{}", "\u{1f680} Starting ML Backtest".bold().green());
println!("\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}");
let start_nanos = date_to_unix_nanos(&start)?;
let end_nanos = date_to_unix_nanos(&end)?;
// Build parameters
let mut parameters = vec![
("confidence_threshold".to_string(), threshold.to_string()),
("use_ensemble".to_string(), ensemble.to_string()),
("confidence_threshold".to_owned(), threshold.to_string()),
("use_ensemble".to_owned(), ensemble.to_string()),
];
if let Some(ref model_name) = model {
parameters.push(("model_name".to_string(), model_name.clone()));
parameters.push(("model_name".to_owned(), model_name.clone()));
}
// Run ML backtest
let ml_request = Request::new(StartBacktestRequest {
strategy_name: "MLEnsemble".to_string(),
strategy_name: "MLEnsemble".to_owned(),
symbols: vec![symbol.clone()],
start_date_unix_nanos: start_nanos,
end_date_unix_nanos: end_nanos,
@@ -182,7 +182,7 @@ async fn run_ml_backtest(
save_results: true,
description: description
.clone()
.unwrap_or_else(|| "ML backtest via TLI".to_string()),
.unwrap_or_else(|| "ML backtest via TLI".to_owned()),
});
let ml_response = client
@@ -201,7 +201,7 @@ async fn run_ml_backtest(
let ml_id = ml_result.backtest_id.clone();
println!(
" ML Backtest started: {}",
"\u{2705} ML Backtest started: {}",
ml_id.bright_cyan()
);
println!(" Symbol: {}", symbol.bright_yellow());
@@ -213,28 +213,28 @@ async fn run_ml_backtest(
if ensemble {
"Ensemble (All Models)".bright_green()
} else {
format!("Single Model ({})", model.unwrap_or_else(|| "DQN".to_string())).bright_blue()
format!("Single Model ({})", model.unwrap_or_else(|| "DQN".to_owned())).bright_blue()
}
);
// If compare flag is set, also run rule-based backtest
if compare {
println!("\n{}", "📊 Running comparison backtest...".bold().cyan());
println!("\n{}", "\u{1f4ca} Running comparison backtest...".bold().cyan());
let rule_request = Request::new(StartBacktestRequest {
strategy_name: "MovingAverageCrossover".to_string(),
strategy_name: "MovingAverageCrossover".to_owned(),
symbols: vec![symbol.clone()],
start_date_unix_nanos: start_nanos,
end_date_unix_nanos: end_nanos,
initial_capital: capital,
parameters: vec![
("fast_period".to_string(), "10".to_string()),
("slow_period".to_string(), "20".to_string()),
("fast_period".to_owned(), "10".to_owned()),
("slow_period".to_owned(), "20".to_owned()),
]
.into_iter()
.collect(),
save_results: true,
description: "Rule-based comparison backtest".to_string(),
description: "Rule-based comparison backtest".to_owned(),
});
let rule_response = client
@@ -244,12 +244,12 @@ async fn run_ml_backtest(
let rule_result = rule_response.into_inner();
if rule_result.success {
println!(" Comparison backtest started: {}", rule_result.backtest_id.bright_cyan());
println!("\u{2705} Comparison backtest started: {}", rule_result.backtest_id.bright_cyan());
}
}
println!("\n💡 Use {} to check status", format!("tli backtest ml status --id {}", ml_id).bright_yellow());
println!("💡 Use {} to get results", format!("tli backtest ml results --id {}", ml_id).bright_yellow());
println!("\n\u{1f4a1} Use {} to check status", format!("tli backtest ml status --id {}", ml_id).bright_yellow());
println!("\u{1f4a1} Use {} to get results", format!("tli backtest ml results --id {}", ml_id).bright_yellow());
Ok(())
}
@@ -269,8 +269,8 @@ async fn get_backtest_status(
.context("Failed to get backtest status")?;
let status = response.into_inner();
println!("{}", "📊 Backtest Status".bold().green());
println!("─────────────────────────────────────────");
println!("{}", "\u{1f4ca} Backtest Status".bold().green());
println!("\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}");
println!("ID: {}", status.backtest_id.bright_cyan());
println!(
"Status: {}",
@@ -306,8 +306,8 @@ async fn get_backtest_results(
.context("Failed to get backtest results")?;
let results = response.into_inner();
println!("{}", "📈 ML Backtest Results".bold().green());
println!("─────────────────────────────────────────");
println!("{}", "\u{1f4c8} ML Backtest Results".bold().green());
println!("\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}");
if let Some(metrics) = results.metrics {
println!("\n{}", "Performance Metrics:".bold());
@@ -331,21 +331,21 @@ async fn get_backtest_results(
// Highlight target achievements
println!("\n{}", "Target Metrics:".bold());
if metrics.sharpe_ratio > 1.5 {
println!(" Sharpe Ratio > 1.5 (ACHIEVED)");
println!(" \u{2705} Sharpe Ratio > 1.5 (ACHIEVED)");
} else {
println!(" ⚠️ Sharpe Ratio: {:.2} (target: >1.5)", metrics.sharpe_ratio);
println!(" \u{26a0}\u{fe0f} Sharpe Ratio: {:.2} (target: >1.5)", metrics.sharpe_ratio);
}
if metrics.win_rate > 0.55 {
println!(" Win Rate > 55% (ACHIEVED)");
println!(" \u{2705} Win Rate > 55% (ACHIEVED)");
} else {
println!(" ⚠️ Win Rate: {:.1}% (target: >55%)", metrics.win_rate * 100.0);
println!(" \u{26a0}\u{fe0f} Win Rate: {:.1}% (target: >55%)", metrics.win_rate * 100.0);
}
if metrics.max_drawdown < 0.20 {
println!(" Max Drawdown < 20% (ACHIEVED)");
println!(" \u{2705} Max Drawdown < 20% (ACHIEVED)");
} else {
println!(" ⚠️ Max Drawdown: {:.1}% (target: <20%)", metrics.max_drawdown * 100.0);
println!(" \u{26a0}\u{fe0f} Max Drawdown: {:.1}% (target: <20%)", metrics.max_drawdown * 100.0);
}
} else {
println!("{}", "No metrics available".bright_red());
@@ -355,7 +355,7 @@ async fn get_backtest_results(
println!("\n{}", format!("Recent Trades ({} total):", results.trades.len()).bold());
for (i, trade) in results.trades.iter().take(10).enumerate() {
println!(
" {}. {} {} @ ${:.2} ${:.2} = {}",
" {}. {} {} @ ${:.2} \u{2192} ${:.2} = {}",
i + 1,
trade.symbol,
format_order_side(trade.side),
@@ -389,7 +389,7 @@ fn format_backtest_status(status: BacktestStatus) -> colored::ColoredString {
}
/// Format order side for display
fn format_order_side(side: i32) -> &'static str {
const fn format_order_side(side: i32) -> &'static str {
match side {
1 => "BUY",
2 => "SELL",

View File

@@ -7,7 +7,7 @@
//! - `ml` - ML-powered trading operations (ensemble voting, predictions, performance)
//!
//! # Command Flow
//! User → main.rs → trade.rs → trade_ml.rs → API Gateway → Trading Service
//! User → main.rs → trade.rs → `trade_ml.rs` → API Gateway → Trading Service
//!
//! # Future Extensions
//! - `manual` - Manual order submission
@@ -115,7 +115,7 @@ mod tests {
let args = TradeArgs {
command: TradeCommand::Ml(TradeMlArgs {
command: TradeMlCommand::Performance {
model: Some("DQN".to_string()),
model: Some("DQN".to_owned()),
},
}),
};

View File

@@ -10,7 +10,7 @@
//!
//! # Architecture
//! - Pure client implementation (connects ONLY to API Gateway at port 50051)
//! - gRPC communication with TradingService via API Gateway proxy
//! - gRPC communication with `TradingService` via API Gateway proxy
//! - No direct service dependencies (proper microservice architecture)
use anyhow::Result;
@@ -97,7 +97,7 @@ impl TradeMlArgs {
/// Execute ML trading command
///
/// Routes to appropriate subcommand handler.
/// All commands connect to API Gateway (http://localhost:50051).
/// All commands connect to API Gateway (<http://localhost:50051>).
pub async fn execute(&self, api_gateway_url: &str, jwt_token: &str) -> Result<()> {
match &self.command {
TradeMlCommand::Submit { symbol, account, model } => {
@@ -140,9 +140,9 @@ impl TradeMlArgs {
let (predicted_action, confidence, model_display) = match prediction_result {
Ok(pred) => pred,
Err(e) => {
println!("{}", format!("⚠️ Warning: Failed to get ML prediction: {}", e).yellow());
println!("{}", format!("\u{26a0}\u{fe0f} Warning: Failed to get ML prediction: {}", e).yellow());
println!("{}", "Using mock prediction for demonstration".yellow());
("BUY".to_string(), 0.85, model.unwrap_or("Ensemble").to_string())
("BUY".to_owned(), 0.85, model.unwrap_or("Ensemble").to_owned())
}
};
@@ -151,7 +151,7 @@ impl TradeMlArgs {
"BUY" | "STRONG_BUY" => OrderSide::Buy,
"SELL" | "STRONG_SELL" => OrderSide::Sell,
"HOLD" | _ => {
println!("{}", format!(" ML prediction is HOLD (confidence: {:.2}%)", confidence * 100.0).cyan());
println!("{}", format!("\u{2139}\u{fe0f} ML prediction is HOLD (confidence: {:.2}%)", confidence * 100.0).cyan());
println!("{}", "No order submitted.".cyan());
return Ok(());
}
@@ -170,13 +170,13 @@ impl TradeMlArgs {
let order_id = match order_result {
Ok(order_id) => order_id,
Err(e) => {
println!("{}", format!("⚠️ Warning: Failed to submit order: {}", e).yellow());
println!("{}", format!("\u{26a0}\u{fe0f} Warning: Failed to submit order: {}", e).yellow());
println!("{}", "Using mock order ID for demonstration".yellow());
uuid::Uuid::new_v4().to_string()
}
};
println!("{}", " ML order submitted successfully!".green());
println!("{}", "\u{2705} ML order submitted successfully!".green());
println!();
println!("Order ID: {}", order_id.bright_green());
println!("Symbol: {}", symbol.bright_cyan());
@@ -195,7 +195,7 @@ impl TradeMlArgs {
/// Get ML prediction from API Gateway
///
/// # Returns
/// Tuple of (predicted_action, confidence, model_display_name)
/// Tuple of (`predicted_action`, confidence, `model_display_name`)
async fn get_ml_prediction(
&self,
symbol: &str,
@@ -206,19 +206,19 @@ impl TradeMlArgs {
use crate::proto::ml::{ml_service_client::MlServiceClient, EnsembleRequest};
// Connect to API Gateway
let mut client = MlServiceClient::connect(api_gateway_url.to_string())
let mut client = MlServiceClient::connect(api_gateway_url.to_owned())
.await
.map_err(|e| anyhow::anyhow!("Failed to connect to API Gateway: {}", e))?;
// Create ensemble request
let model_names = if let Some(m) = model {
vec![m.to_string()]
vec![m.to_owned()]
} else {
vec![] // Empty = all models (ensemble)
};
let mut request = tonic::Request::new(EnsembleRequest {
symbols: vec![symbol.to_string()],
symbols: vec![symbol.to_owned()],
model_names,
method: 1, // ENSEMBLE_METHOD_WEIGHTED_AVERAGE
});
@@ -242,20 +242,20 @@ impl TradeMlArgs {
.ok_or_else(|| anyhow::anyhow!("No predictions returned for symbol"))?;
let predicted_action = match vote.consensus {
1 => "BUY".to_string(),
2 => "SELL".to_string(),
3 => "HOLD".to_string(),
4 => "STRONG_BUY".to_string(),
5 => "STRONG_SELL".to_string(),
_ => "HOLD".to_string(),
1 => "BUY".to_owned(),
2 => "SELL".to_owned(),
3 => "HOLD".to_owned(),
4 => "STRONG_BUY".to_owned(),
5 => "STRONG_SELL".to_owned(),
_ => "HOLD".to_owned(),
};
let confidence = vote.confidence;
let model_display = if model.is_some() {
model.unwrap().to_string()
model.unwrap().to_owned()
} else {
"Ensemble".to_string()
"Ensemble".to_owned()
};
Ok((predicted_action, confidence, model_display))
@@ -277,19 +277,19 @@ impl TradeMlArgs {
use crate::proto::trading::{trading_service_client::TradingServiceClient, SubmitOrderRequest};
// Connect to API Gateway
let mut client = TradingServiceClient::connect(api_gateway_url.to_string())
let mut client = TradingServiceClient::connect(api_gateway_url.to_owned())
.await
.map_err(|e| anyhow::anyhow!("Failed to connect to API Gateway: {}", e))?;
// Create order request
let mut request = tonic::Request::new(SubmitOrderRequest {
symbol: symbol.to_string(),
symbol: symbol.to_owned(),
side: side as i32,
order_type: 1, // ORDER_TYPE_MARKET
quantity,
price: None,
stop_price: None,
time_in_force: "GTC".to_string(),
time_in_force: "GTC".to_owned(),
client_order_id: format!("ml_order_{}", chrono::Utc::now().timestamp_millis()),
});
@@ -343,13 +343,13 @@ impl TradeMlArgs {
// Try to connect to API Gateway with fallback to mock data
let predictions_result = async {
let mut client = TradingServiceClient::connect(api_gateway_url.to_string())
let mut client = TradingServiceClient::connect(api_gateway_url.to_owned())
.await
.map_err(|e| anyhow::anyhow!("Failed to connect to API Gateway: {}", e))?;
let mut request = tonic::Request::new(GetMlPredictionsRequest {
symbol: symbol.to_string(),
model_filter: model.map(|m| m.to_string()),
symbol: symbol.to_owned(),
model_filter: model.map(|m| m.to_owned()),
limit: Some(limit),
});
@@ -369,23 +369,23 @@ impl TradeMlArgs {
let predictions_response = match predictions_result {
Ok(predictions) => predictions,
Err(e) => {
println!("{}", format!("⚠️ Warning: Failed to get predictions: {}", e).yellow());
println!("{}", format!("\u{26a0}\u{fe0f} Warning: Failed to get predictions: {}", e).yellow());
println!("{}", "Using mock predictions for demonstration".yellow());
// Generate mock predictions
vec![
MlPrediction {
timestamp: Utc::now().to_rfc3339(),
model_id: model.unwrap_or("MAMBA2").to_string(),
symbol: symbol.to_string(),
predicted_action: "BUY".to_string(),
model_id: model.unwrap_or("MAMBA2").to_owned(),
symbol: symbol.to_owned(),
predicted_action: "BUY".to_owned(),
confidence: 0.85,
actual_return: Some(0.023),
},
MlPrediction {
timestamp: Utc::now().to_rfc3339(),
model_id: model.unwrap_or("DQN").to_string(),
symbol: symbol.to_string(),
predicted_action: "SELL".to_string(),
model_id: model.unwrap_or("DQN").to_owned(),
symbol: symbol.to_owned(),
predicted_action: "SELL".to_owned(),
confidence: 0.72,
actual_return: Some(-0.012),
},
@@ -409,7 +409,7 @@ impl TradeMlArgs {
}
// Print table header
println!("{}", "─────────────────────────────────────────────────────────────────────────────────".bold());
println!("{}", "\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}".bold());
println!("{:<20} {:<10} {:<10} {:<15} {:<12} {:<15}",
"Timestamp".bold(),
"Model".bold(),
@@ -418,7 +418,7 @@ impl TradeMlArgs {
"Confidence".bold(),
"Outcome".bold()
);
println!("{}", "─────────────────────────────────────────────────────────────────────────────────".bold());
println!("{}", "\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}".bold());
// Add prediction rows
for pred in &predictions_response {
@@ -473,7 +473,7 @@ impl TradeMlArgs {
}
// Footer
println!("{}", "─────────────────────────────────────────────────────────────────────────────────".bold());
println!("{}", "\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}".bold());
// Summary
let count = predictions_response.len();
@@ -503,12 +503,12 @@ impl TradeMlArgs {
// Try to connect to API Gateway with fallback to mock data
let performance_result = async {
let mut client = TradingServiceClient::connect(api_gateway_url.to_string())
let mut client = TradingServiceClient::connect(api_gateway_url.to_owned())
.await
.map_err(|e| anyhow::anyhow!("Failed to connect to API Gateway: {}", e))?;
let mut request = tonic::Request::new(GetMlPerformanceRequest {
model_filter: model.map(|s| s.to_string()),
model_filter: model.map(|s| s.to_owned()),
});
request
@@ -525,12 +525,12 @@ impl TradeMlArgs {
let models = match performance_result {
Ok(models) => models,
Err(e) => {
println!("{}", format!("⚠️ Warning: Failed to get performance metrics: {}", e).yellow());
println!("{}", format!("\u{26a0}\u{fe0f} Warning: Failed to get performance metrics: {}", e).yellow());
println!("{}", "Using mock performance data for demonstration".yellow());
// Generate mock performance data
vec![
ModelPerformance {
model_id: model.unwrap_or("MAMBA2").to_string(),
model_id: model.unwrap_or("MAMBA2").to_owned(),
accuracy: 0.725,
total_predictions: 150,
sharpe_ratio: 1.82,
@@ -538,7 +538,7 @@ impl TradeMlArgs {
max_drawdown: 0.031,
},
ModelPerformance {
model_id: model.unwrap_or("DQN").to_string(),
model_id: model.unwrap_or("DQN").to_owned(),
accuracy: 0.682,
total_predictions: 200,
sharpe_ratio: 1.45,
@@ -554,8 +554,8 @@ impl TradeMlArgs {
println!();
// Display table header
println!("{}", "┌────────┬──────────┬──────────────┬──────────────┬───────────┬────────────┐".bright_black());
println!("{:<6}{:<8}{:<12}{:<12}{:<9} {:<10} ",
println!("{}", "\u{250c}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{252c}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{252c}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{252c}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{252c}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{252c}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2510}".bright_black());
println!("\u{2502} {:<6} \u{2502} {:<8} \u{2502} {:<12} \u{2502} {:<12} \u{2502} {:<9} \u{2502} {:<10} \u{2502}",
"Model".bold(),
"Accuracy".bold(),
"Predictions".bold(),
@@ -563,7 +563,7 @@ impl TradeMlArgs {
"Avg Return".bold(),
"Max Drawdown".bold()
);
println!("{}", "├────────┼──────────┼──────────────┼──────────────┼───────────┼────────────┤".bright_black());
println!("{}", "\u{251c}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{253c}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{253c}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{253c}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{253c}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{253c}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2524}".bright_black());
// Display each model's performance
for model_perf in &models {
@@ -610,7 +610,7 @@ impl TradeMlArgs {
drawdown_str.red().to_string()
};
println!("{:<6}{:<8}{:<12}{:<12}{:<9} {:<10} ",
println!("\u{2502} {:<6} \u{2502} {:<8} \u{2502} {:<12} \u{2502} {:<12} \u{2502} {:<9} \u{2502} {:<10} \u{2502}",
model_perf.model_id.bright_magenta(),
accuracy_colored.to_string(),
model_perf.total_predictions.to_string().bright_cyan(),
@@ -620,7 +620,7 @@ impl TradeMlArgs {
);
}
println!("{}", "└────────┴──────────┴──────────────┴──────────────┴───────────┴────────────┘".bright_black());
println!("{}", "\u{2514}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2534}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2534}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2534}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2534}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2534}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2518}".bright_black());
// Display ensemble summary if showing all models
if model.is_none() {
@@ -744,7 +744,7 @@ pub struct GetMLPerformanceResponse {
pub fn format_ml_order_submission(response: &SubmitMLOrderResponse) {
use owo_colors::OwoColorize;
println!("{}", " ML order submitted successfully!".green().bold());
println!("{}", "\u{2705} ML order submitted successfully!".green().bold());
println!();
println!("{}: {}", "Order ID".cyan().bold(), response.order_id);
@@ -837,7 +837,7 @@ pub fn format_ml_predictions(response: &GetMLPredictionsResponse, symbol: &str)
let outcome_str = match pred.actual_return {
Some(ret) => format!("{:+.2}%", ret * 100.0),
None => "N/A".to_string(),
None => "N/A".to_owned(),
};
let outcome_cell = match pred.actual_return {
Some(ret) if ret > 0.0 => Cell::new(outcome_str).fg(Color::Green),
@@ -944,54 +944,54 @@ mod tests {
// Test that command structure is correct
let args = TradeMlArgs {
command: TradeMlCommand::Submit {
symbol: "ES.FUT".to_string(),
account: "test_account".to_string(),
symbol: "ES.FUT".to_owned(),
account: "test_account".to_owned(),
model: None,
}
};
// Should execute without panic
let result = args.execute("http://localhost:50051", "mock-token").await;
assert!(result.is_ok());
result.unwrap();
}
#[tokio::test]
async fn test_predictions_command_parses() {
let args = TradeMlArgs {
command: TradeMlCommand::Predictions {
symbol: "ES.FUT".to_string(),
model: Some("MAMBA2".to_string()),
symbol: "ES.FUT".to_owned(),
model: Some("MAMBA2".to_owned()),
limit: 5,
}
};
let result = args.execute("http://localhost:50051", "mock-token").await;
assert!(result.is_ok());
result.unwrap();
}
#[tokio::test]
async fn test_performance_command_parses() {
let args = TradeMlArgs {
command: TradeMlCommand::Performance {
model: Some("PPO".to_string()),
model: Some("PPO".to_owned()),
}
};
let result = args.execute("http://localhost:50051", "mock-token").await;
assert!(result.is_ok());
result.unwrap();
}
#[test]
fn test_format_ml_order_submission() {
// Test formatting function with sample data
let response = SubmitMLOrderResponse {
order_id: "order_12345".to_string(),
symbol: "ES.FUT".to_string(),
model_used: "Ensemble".to_string(),
predicted_action: "BUY".to_string(),
order_id: "order_12345".to_owned(),
symbol: "ES.FUT".to_owned(),
model_used: "Ensemble".to_owned(),
predicted_action: "BUY".to_owned(),
confidence: 0.85,
quantity: 1.0,
account_id: "main_account".to_string(),
account_id: "main_account".to_owned(),
};
// Should not panic
@@ -1004,18 +1004,18 @@ mod tests {
let response = GetMLPredictionsResponse {
predictions: vec![
MLPrediction {
timestamp: "2025-10-16T12:00:00Z".to_string(),
model_id: "MAMBA2".to_string(),
symbol: "ES.FUT".to_string(),
predicted_action: "BUY".to_string(),
timestamp: "2025-10-16T12:00:00Z".to_owned(),
model_id: "MAMBA2".to_owned(),
symbol: "ES.FUT".to_owned(),
predicted_action: "BUY".to_owned(),
confidence: 0.85,
actual_return: Some(0.025),
},
MLPrediction {
timestamp: "2025-10-16T11:00:00Z".to_string(),
model_id: "DQN".to_string(),
symbol: "ES.FUT".to_string(),
predicted_action: "SELL".to_string(),
timestamp: "2025-10-16T11:00:00Z".to_owned(),
model_id: "DQN".to_owned(),
symbol: "ES.FUT".to_owned(),
predicted_action: "SELL".to_owned(),
confidence: 0.72,
actual_return: Some(-0.012),
},
@@ -1032,7 +1032,7 @@ mod tests {
let response = GetMLPerformanceResponse {
models: vec![
ModelPerformance {
model_id: "MAMBA2".to_string(),
model_id: "MAMBA2".to_owned(),
accuracy: 72.5,
total_predictions: 150,
sharpe_ratio: 1.82,
@@ -1040,7 +1040,7 @@ mod tests {
max_drawdown: 0.031,
},
ModelPerformance {
model_id: "DQN".to_string(),
model_id: "DQN".to_owned(),
accuracy: 68.2,
total_predictions: 200,
sharpe_ratio: 1.45,

View File

@@ -72,8 +72,8 @@
//! ```
//!
//! Returns:
//! - Best hyperparameters (learning_rate, batch_size, etc.)
//! - Best performance metrics (sharpe_ratio, training_loss, etc.)
//! - Best hyperparameters (`learning_rate`, `batch_size`, etc.)
//! - Best performance metrics (`sharpe_ratio`, `training_loss`, etc.)
//!
//! ## 4. Stop Tuning Job
//! Gracefully stop a running optimization job.
@@ -90,7 +90,7 @@
//!
//! - **DQN**: Deep Q-Network (reinforcement learning)
//! - **PPO**: Proximal Policy Optimization (reinforcement learning)
//! - **MAMBA_2**: State space model with selective attention
//! - **`MAMBA_2`**: State space model with selective attention
//! - **TLOB**: Time-aware Limit Order Book model
//! - **TFT**: Temporal Fusion Transformer (time series)
//! - **LIQUID**: Liquid neural networks (continuous-time RNN)
@@ -262,7 +262,7 @@ use crate::proto::ml_training::{
pub enum TuneCommand {
/// Start a new hyperparameter tuning job
Start {
/// Model type to tune (DQN, PPO, MAMBA_2, TLOB, TFT, LIQUID)
/// Model type to tune (DQN, PPO, `MAMBA_2`, TLOB, TFT, LIQUID)
#[clap(long, value_name = "MODEL")]
model: String,
@@ -363,7 +363,7 @@ struct BestParamDisplay {
///
/// # Arguments
/// * `command` - Tune subcommand to execute
/// * `api_gateway_url` - API Gateway endpoint (default: http://localhost:50051)
/// * `api_gateway_url` - API Gateway endpoint (default: <http://localhost:50051>)
/// * `jwt_token` - JWT authentication token from TLI auth flow
///
/// # Returns
@@ -372,7 +372,7 @@ struct BestParamDisplay {
/// # Errors
/// - Authentication failures (invalid/expired JWT)
/// - Service unavailable (API Gateway down)
/// - Invalid job_id (malformed UUID or job not found)
/// - Invalid `job_id` (malformed UUID or job not found)
/// - Network errors (connection timeout)
pub async fn execute_tune_command(
command: TuneCommand,
@@ -431,20 +431,20 @@ async fn start_tuning_job(
// Validate config file exists
if !std::path::Path::new(config_path).exists() {
anyhow::bail!(" Config file not found: {}", config_path);
anyhow::bail!("\u{274c} Config file not found: {}", config_path);
}
println!("🚀 Starting hyperparameter tuning job...");
println!("\u{1f680} Starting hyperparameter tuning job...");
println!(" Model: {}", model.bright_cyan());
println!(" Trials: {}", trials.to_string().bright_yellow());
println!(" Config: {}", config_path.bright_white());
println!(" GPU: {}", if use_gpu { " Enabled".green() } else { " Disabled".red() });
println!(" GPU: {}", if use_gpu { "\u{2705} Enabled".green() } else { "\u{274c} Disabled".red() });
if watch {
println!(" Watch: {}", " Enabled (polling every 5s)".green());
println!(" Watch: {}", "\u{2705} Enabled (polling every 5s)".green());
}
// Connect to API Gateway and start tuning job
let mut client = MlTrainingServiceClient::connect(api_gateway_url.to_string())
let mut client = MlTrainingServiceClient::connect(api_gateway_url.to_owned())
.await
.context("Failed to connect to API Gateway")?;
@@ -476,12 +476,12 @@ async fn start_tuning_job(
let job_id = Uuid::parse_str(&job_id_str)
.context("Invalid job ID returned from server")?;
println!("\n Tuning job started successfully!");
println!("\n\u{2705} Tuning job started successfully!");
println!(" Job ID: {}", job_id.to_string().bright_green());
// Save job ID to ~/.foxhunt/tuning_jobs.json for later queries
if let Err(e) = save_tuning_job_id(&job_id, model, trials) {
println!("⚠️ Warning: Failed to save job ID to ~/.foxhunt/tuning_jobs.json: {}", e);
println!("\u{26a0}\u{fe0f} Warning: Failed to save job ID to ~/.foxhunt/tuning_jobs.json: {}", e);
println!(" (Job is still running, but manual tracking required)");
} else {
println!(" Saved to ~/.foxhunt/tuning_jobs.json");
@@ -490,12 +490,12 @@ async fn start_tuning_job(
// If --watch flag is set, use polling for progress monitoring
// Note: Server-side streaming not yet implemented (requires tune_stream module)
if watch {
println!("\n⚠️ Real-time streaming not yet available");
println!("\n\u{26a0}\u{fe0f} Real-time streaming not yet available");
println!(" Polling implementation with --watch flag is planned for future release");
println!(" Monitor progress manually with: tli tune status --job-id {}", job_id);
// Future: tune_stream::watch_tuning_progress_streaming(api_gateway_url, jwt_token, &job_id.to_string()).await?;
} else {
println!("\n💡 Monitor progress with:");
println!("\n\u{1f4a1} Monitor progress with:");
println!(" tli tune status --job-id {}", job_id);
}
@@ -510,13 +510,13 @@ async fn get_tuning_status(
) -> AnyhowResult<()> {
// Validate job ID format
let job_id = Uuid::parse_str(job_id_str)
.context(" Invalid job ID format (expected UUID)")?;
.context("\u{274c} Invalid job ID format (expected UUID)")?;
println!("🔍 Fetching tuning job status...");
println!("\u{1f50d} Fetching tuning job status...");
println!(" Job ID: {}", job_id.to_string().bright_cyan());
// Connect to API Gateway
let mut client = MlTrainingServiceClient::connect(api_gateway_url.to_string())
let mut client = MlTrainingServiceClient::connect(api_gateway_url.to_owned())
.await
.context("Failed to connect to API Gateway")?;
@@ -563,7 +563,7 @@ async fn get_tuning_status(
let status_str = format_tuning_job_status(status_response.status);
// Display status with color coding
println!("\n📊 Tuning Job Status");
println!("\n\u{1f4ca} Tuning Job Status");
println!(" Status: {}", format_status_colored(&status_str));
println!(" Progress: {}/{} trials ({:.1}%)",
status_response.current_trial,
@@ -575,13 +575,13 @@ async fn get_tuning_status(
let progress_bar = create_progress_bar(progress_percent);
println!(" {}", progress_bar);
println!("\n🏆 Best Results So Far");
println!("\n\u{1f3c6} Best Results So Far");
println!(" Sharpe Ratio: {}", format!("{:.4}", best_sharpe_ratio).bright_green());
println!(" Elapsed Time: {} seconds", elapsed_time_seconds);
// Display best metrics if available
if !status_response.best_metrics.is_empty() {
println!("\n📈 Best Metrics");
println!("\n\u{1f4c8} Best Metrics");
for (metric_name, metric_value) in &status_response.best_metrics {
println!(" {}: {}",
metric_name.bright_white(),
@@ -607,13 +607,13 @@ async fn get_best_params(
) -> AnyhowResult<()> {
// Validate job ID
let job_id = Uuid::parse_str(job_id_str)
.context(" Invalid job ID format (expected UUID)")?;
.context("\u{274c} Invalid job ID format (expected UUID)")?;
println!("🔍 Fetching best hyperparameters...");
println!("\u{1f50d} Fetching best hyperparameters...");
println!(" Job ID: {}", job_id.to_string().bright_cyan());
// Connect to API Gateway
let mut client = MlTrainingServiceClient::connect(api_gateway_url.to_string())
let mut client = MlTrainingServiceClient::connect(api_gateway_url.to_owned())
.await
.context("Failed to connect to API Gateway")?;
@@ -640,7 +640,7 @@ async fn get_best_params(
let best_metrics = status_response.best_metrics;
// Display best metrics
println!("\n🏆 Best Performance Metrics");
println!("\n\u{1f3c6} Best Performance Metrics");
for (metric_name, metric_value) in &best_metrics {
println!(" {}: {}",
metric_name.bright_white(),
@@ -649,7 +649,7 @@ async fn get_best_params(
}
// Display best hyperparameters as table
println!("\n📋 Best Hyperparameters");
println!("\n\u{1f4cb} Best Hyperparameters");
let param_rows: Vec<BestParamDisplay> = best_params
.iter()
.map(|(name, value)| BestParamDisplay {
@@ -665,10 +665,10 @@ async fn get_best_params(
// Export to file if requested
if let Some(export_path) = export_path {
export_best_params(&best_params, &best_metrics, export_path)?;
println!("\n Best parameters exported to: {}", export_path.bright_green());
println!("\n\u{2705} Best parameters exported to: {}", export_path.bright_green());
}
println!("\n💡 Use these parameters in your training configuration.");
println!("\n\u{1f4a1} Use these parameters in your training configuration.");
Ok(())
}
@@ -682,16 +682,16 @@ async fn stop_tuning_job(
) -> AnyhowResult<()> {
// Validate job ID
let job_id = Uuid::parse_str(job_id_str)
.context(" Invalid job ID format (expected UUID)")?;
.context("\u{274c} Invalid job ID format (expected UUID)")?;
println!("🛑 Stopping tuning job...");
println!("\u{1f6d1} Stopping tuning job...");
println!(" Job ID: {}", job_id.to_string().bright_cyan());
if let Some(reason_text) = reason {
println!(" Reason: {}", reason_text.bright_yellow());
}
// Connect to API Gateway
let mut client = MlTrainingServiceClient::connect(api_gateway_url.to_string())
let mut client = MlTrainingServiceClient::connect(api_gateway_url.to_owned())
.await
.context("Failed to connect to API Gateway")?;
@@ -717,11 +717,11 @@ async fn stop_tuning_job(
// Convert final_status enum to string
let final_status_str = format_tuning_job_status(stop_response.final_status);
println!("\n Tuning job stopped successfully!");
println!("\n\u{2705} Tuning job stopped successfully!");
println!(" Message: {}", stop_response.message.bright_green());
println!(" Final Status: {}", format_status_colored(&final_status_str));
println!("\n💡 Get final results with:");
println!("\n\u{1f4a1} Get final results with:");
println!(" tli tune best --job-id {}", job_id);
Ok(())
@@ -731,7 +731,7 @@ async fn stop_tuning_job(
// Helper Functions
// ============================================================================
/// Save tuning job ID to ~/.foxhunt/tuning_jobs.json for later tracking
/// Save tuning job ID to ~/.`foxhunt/tuning_jobs.json` for later tracking
fn save_tuning_job_id(job_id: &Uuid, model: &str, trials: u32) -> AnyhowResult<()> {
use std::fs;
use std::io::Write;
@@ -783,19 +783,19 @@ fn save_tuning_job_id(job_id: &Uuid, model: &str, trials: u32) -> AnyhowResult<(
/// Display trial history as a table
fn display_trial_history(trial_history: &[TrialResult]) {
println!("\n📊 Trial History");
println!("\n\u{1f4ca} Trial History");
let trial_rows: Vec<TrialDisplay> = trial_history
.iter()
.map(|trial| {
let sharpe = trial.metrics.get("sharpe_ratio")
.or_else(|| Some(&trial.objective_value))
.or(Some(&trial.objective_value))
.map(|v| format!("{:.4}", v))
.unwrap_or_else(|| "N/A".to_string());
.unwrap_or_else(|| "N/A".to_owned());
let loss = trial.metrics.get("training_loss")
.map(|v| format!("{:.6}", v))
.unwrap_or_else(|| "N/A".to_string());
.unwrap_or_else(|| "N/A".to_owned());
let duration = if trial.completed_at > trial.started_at {
trial.completed_at - trial.started_at
@@ -817,27 +817,27 @@ fn display_trial_history(trial_history: &[TrialResult]) {
println!("{}", table);
}
/// Convert TuningJobStatus enum to string
/// Convert `TuningJobStatus` enum to string
fn format_tuning_job_status(status: i32) -> String {
match TuningJobStatus::try_from(status).ok() {
Some(TuningJobStatus::TuningUnknown) => "TUNING_UNKNOWN".to_string(),
Some(TuningJobStatus::TuningPending) => "TUNING_PENDING".to_string(),
Some(TuningJobStatus::TuningRunning) => "TUNING_RUNNING".to_string(),
Some(TuningJobStatus::TuningCompleted) => "TUNING_COMPLETED".to_string(),
Some(TuningJobStatus::TuningFailed) => "TUNING_FAILED".to_string(),
Some(TuningJobStatus::TuningStopped) => "TUNING_STOPPED".to_string(),
Some(TuningJobStatus::TuningUnknown) => "TUNING_UNKNOWN".to_owned(),
Some(TuningJobStatus::TuningPending) => "TUNING_PENDING".to_owned(),
Some(TuningJobStatus::TuningRunning) => "TUNING_RUNNING".to_owned(),
Some(TuningJobStatus::TuningCompleted) => "TUNING_COMPLETED".to_owned(),
Some(TuningJobStatus::TuningFailed) => "TUNING_FAILED".to_owned(),
Some(TuningJobStatus::TuningStopped) => "TUNING_STOPPED".to_owned(),
None => format!("UNKNOWN({})", status),
}
}
/// Convert TrialState enum to string
/// Convert `TrialState` enum to string
fn format_trial_state(state: i32) -> String {
match TrialState::try_from(state).ok() {
Some(TrialState::TrialUnknown) => "UNKNOWN".to_string(),
Some(TrialState::TrialRunning) => "RUNNING".to_string(),
Some(TrialState::TrialComplete) => "COMPLETE".to_string(),
Some(TrialState::TrialPruned) => "PRUNED".to_string(),
Some(TrialState::TrialFailed) => "FAILED".to_string(),
Some(TrialState::TrialUnknown) => "UNKNOWN".to_owned(),
Some(TrialState::TrialRunning) => "RUNNING".to_owned(),
Some(TrialState::TrialComplete) => "COMPLETE".to_owned(),
Some(TrialState::TrialPruned) => "PRUNED".to_owned(),
Some(TrialState::TrialFailed) => "FAILED".to_owned(),
None => format!("UNKNOWN({})", state),
}
}
@@ -848,7 +848,7 @@ fn validate_model_type(model: &str) -> AnyhowResult<()> {
if !VALID_MODELS.contains(&model) {
anyhow::bail!(
" Invalid model type: {}. Valid options: {}",
"\u{274c} Invalid model type: {}. Valid options: {}",
model,
VALID_MODELS.join(", ")
);
@@ -875,20 +875,20 @@ fn create_progress_bar(progress_percent: f32) -> String {
let filled = ((progress_percent / 100.0) * bar_width as f32) as usize;
let empty = bar_width - filled;
let filled_str = "".repeat(filled).green();
let empty_str = "".repeat(empty).white();
let filled_str = "\u{2588}".repeat(filled).green();
let empty_str = "\u{2591}".repeat(empty).white();
format!("[{}{}] {:.1}%", filled_str, empty_str, progress_percent)
}
/// Infer parameter type from value
fn infer_param_type(value: f32) -> String {
if value == value.floor() && value >= 1.0 && value <= 10000.0 {
"Integer".to_string()
if value == value.floor() && (1.0..=10000.0).contains(&value) {
"Integer".to_owned()
} else if value > 0.0 && value < 1.0 {
"Learning Rate".to_string()
"Learning Rate".to_owned()
} else {
"Float".to_string()
"Float".to_owned()
}
}
@@ -926,9 +926,9 @@ mod tests {
#[test]
fn test_validate_model_type_valid() {
assert!(validate_model_type("DQN").is_ok());
assert!(validate_model_type("PPO").is_ok());
assert!(validate_model_type("MAMBA_2").is_ok());
validate_model_type("DQN").unwrap();
validate_model_type("PPO").unwrap();
validate_model_type("MAMBA_2").unwrap();
}
#[test]
@@ -940,10 +940,10 @@ mod tests {
#[test]
fn test_uuid_validation() {
let valid_uuid = "550e8400-e29b-41d4-a716-446655440000";
assert!(Uuid::parse_str(valid_uuid).is_ok());
Uuid::parse_str(valid_uuid).unwrap();
let invalid_uuid = "not-a-uuid";
assert!(Uuid::parse_str(invalid_uuid).is_err());
Uuid::parse_str(invalid_uuid).unwrap_err();
}
#[test]

View File

@@ -26,7 +26,7 @@ use std::path::PathBuf;
/// by CLI arguments or environment variables.
#[derive(Debug, Serialize, Deserialize)]
pub struct TliConfig {
/// API Gateway URL (default: http://localhost:50051)
/// API Gateway URL (default: <http://localhost:50051>)
#[serde(default = "default_api_gateway_url")]
pub api_gateway_url: String,
@@ -40,15 +40,15 @@ pub struct TliConfig {
}
fn default_api_gateway_url() -> String {
"http://localhost:50051".to_string()
"http://localhost:50051".to_owned()
}
fn default_log_level() -> String {
"info".to_string()
"info".to_owned()
}
fn default_token_storage() -> String {
"keyring".to_string()
"keyring".to_owned()
}
impl Default for TliConfig {
@@ -151,9 +151,9 @@ mod tests {
#[test]
fn test_config_serialization() {
let config = TliConfig {
api_gateway_url: "http://example.com:50051".to_string(),
log_level: "debug".to_string(),
token_storage: "file".to_string(),
api_gateway_url: "http://example.com:50051".to_owned(),
log_level: "debug".to_owned(),
token_storage: "file".to_owned(),
};
let toml_str = toml::to_string(&config).unwrap();

View File

@@ -867,7 +867,7 @@ mod tests {
let event1 = Event::new(
EventType::Trading,
EventSeverity::Info,
"test".to_string(),
"test".to_owned(),
serde_json::json!({"id": "123"}),
);
@@ -889,7 +889,7 @@ mod tests {
let event = Event::new(
EventType::Trading,
EventSeverity::Info,
"test".to_string(),
"test".to_owned(),
serde_json::json!({}),
);
@@ -904,12 +904,12 @@ mod tests {
let event = Event::new(
EventType::Trading,
EventSeverity::Info,
"test".to_string(),
"test".to_owned(),
serde_json::json!({"order_id": "123", "symbol": "AAPL"}),
);
let key =
DeduplicationKey::from_event(&event, &["order_id".to_string(), "symbol".to_string()]);
DeduplicationKey::from_event(&event, &["order_id".to_owned(), "symbol".to_owned()]);
assert_eq!(key.event_type, "trading");
assert_eq!(key.source, "test");

View File

@@ -642,7 +642,7 @@ mod tests {
let event = Event::new(
EventType::Trading,
EventSeverity::Info,
"test".to_string(),
"test".to_owned(),
serde_json::json!({"index": i}),
);
buffer.add_event(event).await.unwrap();
@@ -672,7 +672,7 @@ mod tests {
let event = Event::new(
EventType::Trading,
EventSeverity::Info,
"test".to_string(),
"test".to_owned(),
serde_json::json!({"index": i}),
);
buffer.add_event(event).await.unwrap();
@@ -693,13 +693,13 @@ mod tests {
let trading_event = Event::new(
EventType::Trading,
EventSeverity::Info,
"test".to_string(),
"test".to_owned(),
serde_json::json!({}),
);
let market_event = Event::new(
EventType::MarketData,
EventSeverity::Warning,
"test".to_string(),
"test".to_owned(),
serde_json::json!({}),
);
@@ -730,7 +730,7 @@ mod tests {
let critical_event = Event::new(
EventType::System,
EventSeverity::Critical,
"test".to_string(),
"test".to_owned(),
serde_json::json!({"message": "critical"}),
);
@@ -738,7 +738,7 @@ mod tests {
let normal_event = Event::new(
EventType::Trading,
EventSeverity::Info,
"test".to_string(),
"test".to_owned(),
serde_json::json!({"message": "normal"}),
);
@@ -764,7 +764,7 @@ mod tests {
let mut event = Event::new(
EventType::Trading,
EventSeverity::Info,
"test".to_string(),
"test".to_owned(),
serde_json::json!({"index": i}),
);
// Set very short TTL for testing
@@ -793,7 +793,7 @@ mod tests {
let event = Event::new(
EventType::Trading,
EventSeverity::Info,
"test".to_string(),
"test".to_owned(),
serde_json::json!({"test": "data"}),
);
let event_id = event.id;
@@ -821,7 +821,7 @@ mod tests {
let event = Event::new(
EventType::Trading,
EventSeverity::Info,
"test".to_string(),
"test".to_owned(),
serde_json::json!({"index": i}),
);
buffer.add_event(event).await.unwrap();
@@ -844,7 +844,7 @@ mod tests {
let event = Event::new(
EventType::Trading,
EventSeverity::Info,
"test".to_string(),
"test".to_owned(),
serde_json::json!({"index": i}),
);
buffer.add_event(event).await.unwrap();
@@ -877,13 +877,13 @@ mod tests {
let event = Event::new(
EventType::Trading,
EventSeverity::Info,
"test".to_string(),
"test".to_owned(),
serde_json::json!({"index": i}),
);
let result = buffer.add_event(event).await;
// First 5 should succeed, 6th might fail due to backpressure
if i < 5 {
assert!(result.is_ok());
result.unwrap();
}
}

View File

@@ -147,7 +147,7 @@ impl Event {
event_type,
severity,
source,
timestamp_nanos: crate::types::current_unix_nanos() as i64,
timestamp_nanos: crate::types::current_unix_nanos(),
sequence: 0_u64, // Set by stream manager
payload,
correlation_id: None,
@@ -563,7 +563,7 @@ mod tests {
let event = Event::new(
EventType::Trading,
EventSeverity::Info,
"test_service".to_string(),
"test_service".to_owned(),
payload,
);
@@ -580,14 +580,14 @@ mod tests {
let trading_event = Event::new(
EventType::Trading,
EventSeverity::Info,
"service".to_string(),
"service".to_owned(),
serde_json::json!({}),
);
let market_event = Event::new(
EventType::MarketData,
EventSeverity::Info,
"service".to_string(),
"service".to_owned(),
serde_json::json!({}),
);

View File

@@ -855,7 +855,7 @@ mod tests {
#[test]
fn test_stream_connection_creation() {
let connection = StreamConnection::new("test".to_string(), "http://test".to_string());
let connection = StreamConnection::new("test".to_owned(), "http://test".to_owned());
assert_eq!(connection.service, "test");
assert_eq!(connection.endpoint, "http://test");

View File

@@ -111,8 +111,8 @@ mod types_tests {
assert_eq!(string_to_order_side("SELL").unwrap(), TliOrderSide::Sell);
assert_eq!(string_to_order_side("sell").unwrap(), TliOrderSide::Sell);
assert!(string_to_order_side("INVALID").is_err());
assert!(string_to_order_side("").is_err());
string_to_order_side("INVALID").unwrap_err();
string_to_order_side("").unwrap_err();
}
#[test]
@@ -132,7 +132,7 @@ mod types_tests {
OrderType::StopLimit
);
assert!(string_to_order_type("INVALID").is_err());
string_to_order_type("INVALID").unwrap_err();
}
#[test]
@@ -158,7 +158,7 @@ mod types_tests {
OrderStatus::Cancelled
);
assert!(string_to_order_status("INVALID").is_err());
string_to_order_status("INVALID").unwrap_err();
}
#[test]
@@ -191,18 +191,18 @@ mod types_tests {
TliSystemStatus::Critical
);
assert!(string_to_system_status("INVALID").is_err());
string_to_system_status("INVALID").unwrap_err();
}
#[test]
fn test_symbol_validation() {
// Valid symbols
assert!(validate_symbol("AAPL").is_ok());
assert!(validate_symbol("BTC.USD").is_ok());
assert!(validate_symbol("EUR-USD").is_ok());
assert!(validate_symbol("SPX_500").is_ok());
assert!(validate_symbol("A").is_ok());
assert!(validate_symbol("123ABC").is_ok());
validate_symbol("AAPL").unwrap();
validate_symbol("BTC.USD").unwrap();
validate_symbol("EUR-USD").unwrap();
validate_symbol("SPX_500").unwrap();
validate_symbol("A").unwrap();
validate_symbol("123ABC").unwrap();
// Invalid symbols
assert!(validate_symbol("").is_err());
@@ -215,9 +215,9 @@ mod types_tests {
#[test]
fn test_quantity_validation() {
// Valid quantities
assert!(validate_quantity(1.0).is_ok());
assert!(validate_quantity(0.0001).is_ok());
assert!(validate_quantity(1000000.0).is_ok());
validate_quantity(1.0).unwrap();
validate_quantity(0.0001).unwrap();
validate_quantity(1000000.0).unwrap();
// Invalid quantities
assert!(validate_quantity(0.0).is_err());
@@ -230,9 +230,9 @@ mod types_tests {
#[test]
fn test_price_validation() {
// Valid prices
assert!(validate_price(1.0).is_ok());
assert!(validate_price(0.01).is_ok());
assert!(validate_price(999999.99).is_ok());
validate_price(1.0).unwrap();
validate_price(0.01).unwrap();
validate_price(999999.99).unwrap();
// Invalid prices
assert!(validate_price(0.0).is_err());
@@ -244,7 +244,7 @@ mod types_tests {
#[test]
fn test_create_proto_position() {
let position = create_proto_position("AAPL".to_string(), 100.0, 150.0, 140.0);
let position = create_proto_position("AAPL".to_owned(), 100.0, 150.0, 140.0);
assert_eq!(position.symbol, "AAPL");
assert_eq!(position.quantity, 100.0);
@@ -260,14 +260,14 @@ mod types_tests {
use std::collections::HashMap;
let labels = HashMap::from([
("service".to_string(), "test".to_string()),
("environment".to_string(), "dev".to_string()),
("service".to_owned(), "test".to_owned()),
("environment".to_owned(), "dev".to_owned()),
]);
let metric = create_metric(
"test_metric".to_string(),
"test_metric".to_owned(),
42.5,
"count".to_string(),
"count".to_owned(),
labels.clone(),
);
@@ -284,10 +284,10 @@ mod error_tests {
#[test]
fn test_error_types() {
let connection_error = TliError::Connection("Connection failed".to_string());
let invalid_request_error = TliError::InvalidRequest("Bad request".to_string());
let invalid_symbol_error = TliError::InvalidSymbol("Bad symbol".to_string());
let not_connected_error = TliError::Connection("Not connected".to_string());
let connection_error = TliError::Connection("Connection failed".to_owned());
let invalid_request_error = TliError::InvalidRequest("Bad request".to_owned());
let invalid_symbol_error = TliError::InvalidSymbol("Bad symbol".to_owned());
let not_connected_error = TliError::Connection("Not connected".to_owned());
// Test Display implementation
assert!(connection_error.to_string().contains("Connection failed"));
@@ -316,7 +316,7 @@ mod error_tests {
// Property-based tests
proptest! {
#[test]
fn test_timestamp_conversion_property(timestamp in 0i64..i64::MAX/2) {
fn test_timestamp_conversion_property(timestamp in 0_i64..i64::MAX/2) {
let system_time = unix_nanos_to_system_time(timestamp);
let converted = system_time_to_unix_nanos(system_time);
@@ -330,23 +330,23 @@ proptest! {
}
#[test]
fn test_quantity_validation_property(quantity in 0.0001f64..1000000.0) {
fn test_quantity_validation_property(quantity in 0.0001_f64..1000000.0) {
prop_assert!(validate_quantity(quantity).is_ok());
}
#[test]
fn test_price_validation_property(price in 0.01f64..999999.99) {
fn test_price_validation_property(price in 0.01_f64..999999.99) {
prop_assert!(validate_price(price).is_ok());
}
#[test]
fn test_position_calculation_property(
quantity in -1000.0f64..1000.0,
market_price in 0.01f64..10000.0,
average_cost in 0.01f64..10000.0
quantity in -1000.0_f64..1000.0,
market_price in 0.01_f64..10000.0,
average_cost in 0.01_f64..10000.0
) {
let position = create_proto_position(
"TEST".to_string(),
"TEST".to_owned(),
quantity,
market_price,
average_cost,
@@ -482,9 +482,9 @@ mod command_handling_tests {
#[test]
fn test_order_command_validation() {
// Valid order parameters
assert!(validate_symbol("AAPL").is_ok());
assert!(validate_quantity(100.0).is_ok());
assert!(validate_price(150.0).is_ok());
validate_symbol("AAPL").unwrap();
validate_quantity(100.0).unwrap();
validate_price(150.0).unwrap();
// Invalid order parameters
assert!(validate_symbol("").is_err());
@@ -498,7 +498,7 @@ mod command_handling_tests {
assert_eq!(string_to_order_side("buy").unwrap(), TliOrderSide::Buy);
assert_eq!(string_to_order_side("SELL").unwrap(), TliOrderSide::Sell);
assert_eq!(string_to_order_side("sell").unwrap(), TliOrderSide::Sell);
assert!(string_to_order_side("INVALID").is_err());
string_to_order_side("INVALID").unwrap_err();
}
}
@@ -508,7 +508,7 @@ mod error_display_tests {
#[test]
fn test_error_display() {
let connection_error = TliError::Connection("Connection failed".to_string());
let connection_error = TliError::Connection("Connection failed".to_owned());
let error_str = connection_error.to_string();
assert!(error_str.contains("Connection failed"));
}
@@ -516,14 +516,14 @@ mod error_display_tests {
#[test]
fn test_error_types_comprehensive() {
let errors = vec![
TliError::Connection("conn".to_string()),
TliError::InvalidRequest("req".to_string()),
TliError::InvalidSymbol("sym".to_string()),
TliError::Config("config".to_string()),
TliError::Dashboard("dashboard".to_string()),
TliError::BufferFull("full".to_string()),
TliError::NotFound("not_found".to_string()),
TliError::Other("other".to_string()),
TliError::Connection("conn".to_owned()),
TliError::InvalidRequest("req".to_owned()),
TliError::InvalidSymbol("sym".to_owned()),
TliError::Config("config".to_owned()),
TliError::Dashboard("dashboard".to_owned()),
TliError::BufferFull("full".to_owned()),
TliError::NotFound("not_found".to_owned()),
TliError::Other("other".to_owned()),
];
for error in errors {

View File

@@ -542,15 +542,15 @@ mod tests {
assert_eq!(string_to_order_side("BUY").unwrap(), TliOrderSide::Buy);
assert_eq!(string_to_order_side("buy").unwrap(), TliOrderSide::Buy);
assert!(string_to_order_side("INVALID").is_err());
string_to_order_side("INVALID").unwrap_err();
}
#[test]
fn test_symbol_validation() {
assert!(validate_symbol("AAPL").is_ok());
assert!(validate_symbol("BTC.USD").is_ok());
assert!(validate_symbol("EUR-USD").is_ok());
assert!(validate_symbol("SPX_500").is_ok());
validate_symbol("AAPL").unwrap();
validate_symbol("BTC.USD").unwrap();
validate_symbol("EUR-USD").unwrap();
validate_symbol("SPX_500").unwrap();
assert!(validate_symbol("").is_err());
assert!(validate_symbol("A".repeat(21).as_str()).is_err());
@@ -559,8 +559,8 @@ mod tests {
#[test]
fn test_quantity_validation() {
assert!(validate_quantity(1.0).is_ok());
assert!(validate_quantity(0.0001).is_ok());
validate_quantity(1.0).unwrap();
validate_quantity(0.0001).unwrap();
assert!(validate_quantity(0.0).is_err());
assert!(validate_quantity(-1.0).is_err());
@@ -570,7 +570,7 @@ mod tests {
#[test]
fn test_create_position() {
let position = create_proto_position("AAPL".to_string(), 100.0, 150.0, 140.0);
let position = create_proto_position("AAPL".to_owned(), 100.0, 150.0, 140.0);
assert_eq!(position.symbol, "AAPL");
assert_eq!(position.quantity, 100.0);