✅ Validation Results: - PPO training: 24.2s (1 epoch, 950 samples, dim=225) - Feature extraction: 105μs/bar (9.5x faster than target) - Model checkpoint: 293KB (147KB actor + 146KB critic) - GPU memory: 145MB used (96.4% headroom) - Zero dimension mismatches 📊 Success Criteria (5/5): ✅ Feature dimension = 225 (Wave C 201 + Wave D 24) ✅ Model state_dim = 225 ✅ Training completed without errors ✅ Checkpoint saved successfully ✅ No dimension mismatch errors 📁 Training Data Ready: - ES.FUT: 2.9MB, 180 days - NQ.FUT: 4.4MB, 180 days - 6E.FUT: 2.8MB, 180 days - ZN.FUT: 65KB, 90 days (clean) 🚀 Next: Full production model retraining (4 models, ~10min GPU time) 🤖 Generated with Claude Code (https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
489 lines
14 KiB
Rust
489 lines
14 KiB
Rust
//! Asset Parser Unit Tests
|
|
//!
|
|
//! Comprehensive test suite for asset parsing and validation.
|
|
//! Tests cover futures, equities, mixed assets, whitespace handling,
|
|
//! case normalization, and error cases.
|
|
|
|
use ml_training_service::asset_parser::{Asset, AssetParser};
|
|
|
|
// ============================================================================
|
|
// FUTURES PARSING TESTS
|
|
// ============================================================================
|
|
|
|
#[test]
|
|
fn test_parse_single_future_es() {
|
|
let result = AssetParser::parse("ES.FUT").unwrap();
|
|
assert_eq!(result.len(), 1);
|
|
match &result[0] {
|
|
Asset::Future { symbol } => assert_eq!(symbol, "ES.FUT"),
|
|
_ => panic!("Expected Future variant"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_single_future_nq() {
|
|
let result = AssetParser::parse("NQ.FUT").unwrap();
|
|
assert_eq!(result.len(), 1);
|
|
match &result[0] {
|
|
Asset::Future { symbol } => assert_eq!(symbol, "NQ.FUT"),
|
|
_ => panic!("Expected Future variant"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_single_future_6e() {
|
|
let result = AssetParser::parse("6E.FUT").unwrap();
|
|
assert_eq!(result.len(), 1);
|
|
match &result[0] {
|
|
Asset::Future { symbol } => assert_eq!(symbol, "6E.FUT"),
|
|
_ => panic!("Expected Future variant"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_single_future_zn() {
|
|
let result = AssetParser::parse("ZN.FUT").unwrap();
|
|
assert_eq!(result.len(), 1);
|
|
match &result[0] {
|
|
Asset::Future { symbol } => assert_eq!(symbol, "ZN.FUT"),
|
|
_ => panic!("Expected Future variant"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_multiple_futures() {
|
|
let result = AssetParser::parse("ES.FUT,NQ.FUT").unwrap();
|
|
assert_eq!(result.len(), 2);
|
|
|
|
let symbols: Vec<String> = result.iter().map(|a| match a {
|
|
Asset::Future { symbol } => symbol.clone(),
|
|
_ => panic!("Expected Future variant"),
|
|
}).collect();
|
|
|
|
assert!(symbols.contains(&"ES.FUT".to_string()));
|
|
assert!(symbols.contains(&"NQ.FUT".to_string()));
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_four_futures() {
|
|
let result = AssetParser::parse("ES.FUT,NQ.FUT,6E.FUT,ZN.FUT").unwrap();
|
|
assert_eq!(result.len(), 4);
|
|
|
|
let symbols: Vec<String> = result.iter().map(|a| match a {
|
|
Asset::Future { symbol } => symbol.clone(),
|
|
_ => panic!("Expected Future variant"),
|
|
}).collect();
|
|
|
|
assert!(symbols.contains(&"ES.FUT".to_string()));
|
|
assert!(symbols.contains(&"NQ.FUT".to_string()));
|
|
assert!(symbols.contains(&"6E.FUT".to_string()));
|
|
assert!(symbols.contains(&"ZN.FUT".to_string()));
|
|
}
|
|
|
|
// ============================================================================
|
|
// EQUITIES PARSING TESTS
|
|
// ============================================================================
|
|
|
|
#[test]
|
|
fn test_parse_single_equity_aapl() {
|
|
let result = AssetParser::parse("AAPL").unwrap();
|
|
assert_eq!(result.len(), 1);
|
|
match &result[0] {
|
|
Asset::Equity { symbol } => assert_eq!(symbol, "AAPL"),
|
|
_ => panic!("Expected Equity variant"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_single_equity_msft() {
|
|
let result = AssetParser::parse("MSFT").unwrap();
|
|
assert_eq!(result.len(), 1);
|
|
match &result[0] {
|
|
Asset::Equity { symbol } => assert_eq!(symbol, "MSFT"),
|
|
_ => panic!("Expected Equity variant"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_single_equity_googl() {
|
|
let result = AssetParser::parse("GOOGL").unwrap();
|
|
assert_eq!(result.len(), 1);
|
|
match &result[0] {
|
|
Asset::Equity { symbol } => assert_eq!(symbol, "GOOGL"),
|
|
_ => panic!("Expected Equity variant"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_single_equity_tsla() {
|
|
let result = AssetParser::parse("TSLA").unwrap();
|
|
assert_eq!(result.len(), 1);
|
|
match &result[0] {
|
|
Asset::Equity { symbol } => assert_eq!(symbol, "TSLA"),
|
|
_ => panic!("Expected Equity variant"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_multiple_equities() {
|
|
let result = AssetParser::parse("AAPL,MSFT,GOOGL").unwrap();
|
|
assert_eq!(result.len(), 3);
|
|
|
|
let symbols: Vec<String> = result.iter().map(|a| match a {
|
|
Asset::Equity { symbol } => symbol.clone(),
|
|
_ => panic!("Expected Equity variant"),
|
|
}).collect();
|
|
|
|
assert!(symbols.contains(&"AAPL".to_string()));
|
|
assert!(symbols.contains(&"MSFT".to_string()));
|
|
assert!(symbols.contains(&"GOOGL".to_string()));
|
|
}
|
|
|
|
// ============================================================================
|
|
// MIXED ASSETS PARSING TESTS
|
|
// ============================================================================
|
|
|
|
#[test]
|
|
fn test_parse_mixed_futures_and_equities() {
|
|
let result = AssetParser::parse("ES.FUT,AAPL,NQ.FUT,MSFT").unwrap();
|
|
assert_eq!(result.len(), 4);
|
|
|
|
// Count futures vs equities
|
|
let futures_count = result.iter().filter(|a| matches!(a, Asset::Future { .. })).count();
|
|
let equities_count = result.iter().filter(|a| matches!(a, Asset::Equity { .. })).count();
|
|
|
|
assert_eq!(futures_count, 2);
|
|
assert_eq!(equities_count, 2);
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_complex_mixed_list() {
|
|
let result = AssetParser::parse("ES.FUT,AAPL,NQ.FUT,MSFT,6E.FUT,GOOGL,ZN.FUT,TSLA").unwrap();
|
|
assert_eq!(result.len(), 8);
|
|
|
|
let futures_count = result.iter().filter(|a| matches!(a, Asset::Future { .. })).count();
|
|
let equities_count = result.iter().filter(|a| matches!(a, Asset::Equity { .. })).count();
|
|
|
|
assert_eq!(futures_count, 4);
|
|
assert_eq!(equities_count, 4);
|
|
}
|
|
|
|
// ============================================================================
|
|
// WHITESPACE HANDLING TESTS
|
|
// ============================================================================
|
|
|
|
#[test]
|
|
fn test_parse_with_leading_whitespace() {
|
|
let result = AssetParser::parse(" ES.FUT").unwrap();
|
|
assert_eq!(result.len(), 1);
|
|
match &result[0] {
|
|
Asset::Future { symbol } => assert_eq!(symbol, "ES.FUT"),
|
|
_ => panic!("Expected Future variant"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_with_trailing_whitespace() {
|
|
let result = AssetParser::parse("ES.FUT ").unwrap();
|
|
assert_eq!(result.len(), 1);
|
|
match &result[0] {
|
|
Asset::Future { symbol } => assert_eq!(symbol, "ES.FUT"),
|
|
_ => panic!("Expected Future variant"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_with_surrounding_whitespace() {
|
|
let result = AssetParser::parse(" ES.FUT ").unwrap();
|
|
assert_eq!(result.len(), 1);
|
|
match &result[0] {
|
|
Asset::Future { symbol } => assert_eq!(symbol, "ES.FUT"),
|
|
_ => panic!("Expected Future variant"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_with_whitespace_in_list() {
|
|
let result = AssetParser::parse(" ES.FUT , AAPL , NQ.FUT ").unwrap();
|
|
assert_eq!(result.len(), 3);
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_with_tabs_and_spaces() {
|
|
let result = AssetParser::parse("ES.FUT\t,\t AAPL , NQ.FUT").unwrap();
|
|
assert_eq!(result.len(), 3);
|
|
}
|
|
|
|
// ============================================================================
|
|
// CASE NORMALIZATION TESTS
|
|
// ============================================================================
|
|
|
|
#[test]
|
|
fn test_parse_lowercase_future_normalizes() {
|
|
let result = AssetParser::parse("es.fut").unwrap();
|
|
assert_eq!(result.len(), 1);
|
|
match &result[0] {
|
|
Asset::Future { symbol } => assert_eq!(symbol, "ES.FUT"),
|
|
_ => panic!("Expected Future variant"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_lowercase_equity_normalizes() {
|
|
let result = AssetParser::parse("aapl").unwrap();
|
|
assert_eq!(result.len(), 1);
|
|
match &result[0] {
|
|
Asset::Equity { symbol } => assert_eq!(symbol, "AAPL"),
|
|
_ => panic!("Expected Equity variant"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_mixed_case_future_normalizes() {
|
|
let result = AssetParser::parse("Es.FuT").unwrap();
|
|
assert_eq!(result.len(), 1);
|
|
match &result[0] {
|
|
Asset::Future { symbol } => assert_eq!(symbol, "ES.FUT"),
|
|
_ => panic!("Expected Future variant"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_mixed_case_equity_normalizes() {
|
|
let result = AssetParser::parse("AaPl").unwrap();
|
|
assert_eq!(result.len(), 1);
|
|
match &result[0] {
|
|
Asset::Equity { symbol } => assert_eq!(symbol, "AAPL"),
|
|
_ => panic!("Expected Equity variant"),
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// DEDUPLICATION TESTS
|
|
// ============================================================================
|
|
|
|
#[test]
|
|
fn test_parse_deduplicates_exact_matches() {
|
|
let result = AssetParser::parse("ES.FUT,ES.FUT,ES.FUT").unwrap();
|
|
assert_eq!(result.len(), 1);
|
|
match &result[0] {
|
|
Asset::Future { symbol } => assert_eq!(symbol, "ES.FUT"),
|
|
_ => panic!("Expected Future variant"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_deduplicates_case_insensitive() {
|
|
let result = AssetParser::parse("ES.FUT,es.fut,Es.FuT").unwrap();
|
|
assert_eq!(result.len(), 1);
|
|
match &result[0] {
|
|
Asset::Future { symbol } => assert_eq!(symbol, "ES.FUT"),
|
|
_ => panic!("Expected Future variant"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_deduplicates_mixed_list() {
|
|
let result = AssetParser::parse("ES.FUT,AAPL,ES.FUT,AAPL,NQ.FUT").unwrap();
|
|
assert_eq!(result.len(), 3);
|
|
}
|
|
|
|
// ============================================================================
|
|
// ERROR HANDLING TESTS - FUTURES
|
|
// ============================================================================
|
|
|
|
#[test]
|
|
fn test_reject_future_without_suffix() {
|
|
let result = AssetParser::parse("ES");
|
|
assert!(result.is_err());
|
|
let err_msg = result.unwrap_err().to_string();
|
|
assert!(err_msg.contains("Invalid asset format"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_reject_future_wrong_suffix() {
|
|
let result = AssetParser::parse("ES.FUTURE");
|
|
assert!(result.is_err());
|
|
let err_msg = result.unwrap_err().to_string();
|
|
assert!(err_msg.contains("Invalid asset format"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_reject_future_too_long_symbol() {
|
|
let result = AssetParser::parse("TOOLONG.FUT");
|
|
assert!(result.is_err());
|
|
let err_msg = result.unwrap_err().to_string();
|
|
assert!(err_msg.contains("Invalid asset format"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_reject_future_empty_symbol() {
|
|
let result = AssetParser::parse(".FUT");
|
|
assert!(result.is_err());
|
|
}
|
|
|
|
// ============================================================================
|
|
// ERROR HANDLING TESTS - EQUITIES
|
|
// ============================================================================
|
|
|
|
#[test]
|
|
fn test_reject_equity_too_long() {
|
|
let result = AssetParser::parse("TOOLONG");
|
|
assert!(result.is_err());
|
|
let err_msg = result.unwrap_err().to_string();
|
|
assert!(err_msg.contains("Invalid asset format"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_reject_equity_with_numbers() {
|
|
let result = AssetParser::parse("AAPL1");
|
|
assert!(result.is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn test_reject_equity_with_special_chars() {
|
|
let result = AssetParser::parse("AAPL@");
|
|
assert!(result.is_err());
|
|
}
|
|
|
|
// ============================================================================
|
|
// ERROR HANDLING TESTS - GENERAL
|
|
// ============================================================================
|
|
|
|
#[test]
|
|
fn test_reject_empty_string() {
|
|
let result = AssetParser::parse("");
|
|
assert!(result.is_err());
|
|
let err_msg = result.unwrap_err().to_string();
|
|
assert!(err_msg.contains("empty") || err_msg.contains("asset list"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_reject_only_whitespace() {
|
|
let result = AssetParser::parse(" ");
|
|
assert!(result.is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn test_reject_only_commas() {
|
|
let result = AssetParser::parse(",,,");
|
|
assert!(result.is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn test_reject_invalid_characters_in_future() {
|
|
let result = AssetParser::parse("ES@.FUT");
|
|
assert!(result.is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn test_reject_invalid_characters_in_equity() {
|
|
let result = AssetParser::parse("A#PPL");
|
|
assert!(result.is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn test_partial_invalid_list_fails_all() {
|
|
// If any asset is invalid, the entire parse should fail
|
|
let result = AssetParser::parse("ES.FUT,INVALID_FORMAT,AAPL");
|
|
assert!(result.is_err());
|
|
}
|
|
|
|
// ============================================================================
|
|
// VALIDATE METHODS TESTS
|
|
// ============================================================================
|
|
|
|
#[test]
|
|
fn test_validate_future_valid_es() {
|
|
let result = AssetParser::validate_future("ES.FUT");
|
|
assert!(result.is_ok());
|
|
match result.unwrap() {
|
|
Asset::Future { symbol } => assert_eq!(symbol, "ES.FUT"),
|
|
_ => panic!("Expected Future variant"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_validate_future_valid_6e() {
|
|
let result = AssetParser::validate_future("6E.FUT");
|
|
assert!(result.is_ok());
|
|
}
|
|
|
|
#[test]
|
|
fn test_validate_future_invalid_missing_suffix() {
|
|
let result = AssetParser::validate_future("ES");
|
|
assert!(result.is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn test_validate_equity_valid_aapl() {
|
|
let result = AssetParser::validate_equity("AAPL");
|
|
assert!(result.is_ok());
|
|
match result.unwrap() {
|
|
Asset::Equity { symbol } => assert_eq!(symbol, "AAPL"),
|
|
_ => panic!("Expected Equity variant"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_validate_equity_valid_googl() {
|
|
let result = AssetParser::validate_equity("GOOGL");
|
|
assert!(result.is_ok());
|
|
}
|
|
|
|
#[test]
|
|
fn test_validate_equity_invalid_too_long() {
|
|
let result = AssetParser::validate_equity("TOOLONG");
|
|
assert!(result.is_err());
|
|
}
|
|
|
|
// ============================================================================
|
|
// DISPLAY METHODS TESTS
|
|
// ============================================================================
|
|
|
|
#[test]
|
|
fn test_asset_display_future() {
|
|
let asset = Asset::Future { symbol: "ES.FUT".to_string() };
|
|
assert_eq!(format!("{}", asset), "ES.FUT");
|
|
}
|
|
|
|
#[test]
|
|
fn test_asset_display_equity() {
|
|
let asset = Asset::Equity { symbol: "AAPL".to_string() };
|
|
assert_eq!(format!("{}", asset), "AAPL");
|
|
}
|
|
|
|
// ============================================================================
|
|
// EDGE CASES
|
|
// ============================================================================
|
|
|
|
#[test]
|
|
fn test_reject_single_character_equity_ambiguous() {
|
|
// Single character symbols rejected to avoid futures ambiguity
|
|
// Use .FUT suffix for futures: X.FUT
|
|
let result = AssetParser::parse("X");
|
|
assert!(result.is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_one_character_future() {
|
|
let result = AssetParser::parse("E.FUT");
|
|
assert!(result.is_ok());
|
|
match &result.unwrap()[0] {
|
|
Asset::Future { symbol } => assert_eq!(symbol, "E.FUT"),
|
|
_ => panic!("Expected Future variant"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_max_length_future() {
|
|
let result = AssetParser::parse("ABCD.FUT");
|
|
assert!(result.is_ok());
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_max_length_equity() {
|
|
let result = AssetParser::parse("ABCDE");
|
|
assert!(result.is_ok());
|
|
}
|