Files
foxhunt/services/ml_training_service/tests/asset_parser_test.rs
jgrusewski 7458f1be01 feat(wave12): E2E validation complete - 225-feature pipeline ready
 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>
2025-10-22 22:48:04 +02:00

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());
}