#![allow( clippy::assertions_on_constants, clippy::assertions_on_result_states, clippy::clone_on_copy, clippy::decimal_literal_representation, clippy::doc_markdown, clippy::empty_line_after_doc_comments, clippy::field_reassign_with_default, clippy::get_unwrap, clippy::identity_op, clippy::inconsistent_digit_grouping, clippy::indexing_slicing, clippy::integer_division, clippy::len_zero, clippy::let_underscore_must_use, clippy::manual_div_ceil, clippy::manual_let_else, clippy::manual_range_contains, clippy::modulo_arithmetic, clippy::needless_range_loop, clippy::non_ascii_literal, clippy::redundant_clone, clippy::shadow_reuse, clippy::shadow_same, clippy::shadow_unrelated, clippy::single_match_else, clippy::str_to_string, clippy::string_slice, clippy::tests_outside_test_module, clippy::too_many_lines, clippy::unnecessary_wraps, clippy::unseparated_literal_suffix, clippy::use_debug, clippy::useless_vec, clippy::wildcard_enum_match_arm, clippy::else_if_without_else, clippy::expect_used, clippy::missing_const_for_fn, clippy::similar_names, clippy::type_complexity, clippy::collapsible_else_if, clippy::doc_lazy_continuation, clippy::items_after_test_module, clippy::map_clone, clippy::multiple_unsafe_ops_per_block, clippy::unwrap_or_default, clippy::assign_op_pattern, clippy::needless_borrow, clippy::println_empty_string, clippy::unnecessary_cast, clippy::used_underscore_binding, clippy::create_dir, clippy::implicit_saturating_sub, clippy::exit, clippy::expect_fun_call, clippy::too_many_arguments, clippy::unnecessary_map_or, clippy::unwrap_used, dead_code, unused_imports, unused_variables, clippy::cloned_ref_to_slice_refs, clippy::neg_multiply, clippy::while_let_loop, clippy::bool_assert_comparison, clippy::excessive_precision, clippy::trivially_copy_pass_by_ref, clippy::op_ref, clippy::redundant_closure, clippy::unnecessary_lazy_evaluations, clippy::if_then_some_else_none, clippy::unnecessary_to_owned, clippy::single_component_path_imports, )] //! Comprehensive TDD Test Suite for Triple Barrier Labeling //! //! This test suite validates the triple barrier method implementation following //! TDD methodology and MLFinLab research principles. //! //! ## Test Coverage //! 1. **Profit Target Tests**: Upper barrier hit first //! 2. **Stop Loss Tests**: Lower barrier hit first //! 3. **Time Horizon Tests**: Expiry without barrier touch //! 4. **Volatility-Based Barriers**: Dynamic barrier calculation //! 5. **Edge Cases**: Gaps, extreme moves, simultaneous touches //! 6. **Label Balance Tests**: Symmetric vs asymmetric barriers //! 7. **Quality Score Tests**: Label quality metrics //! 8. **Performance Tests**: <80μs latency target use ml::labeling::{ triple_barrier::{BarrierTracker, PricePoint, TripleBarrierEngine}, types::{BarrierConfig, BarrierResult, BarrierTouchedFirst}, utils, }; use tracing::info; // ============================================================================ // TEST 1: Profit Target Hit First (Upper Barrier) // ============================================================================ #[test] fn test_profit_target_hit_first() { // GIVEN: A tracker with 1% profit target, 0.5% stop loss let config = BarrierConfig::conservative(); // 100bps profit, 50bps stop let entry_price = 100.00; // $100.00 let entry_price_cents = utils::price_to_cents(entry_price); let entry_timestamp_ns = 1692000000_000_000_000; let mut tracker = BarrierTracker::new(entry_price_cents, entry_timestamp_ns, config); // WHEN: Price moves to $101.05 (above 1% profit target) let profit_price = utils::price_to_cents(101.05); let profit_timestamp = entry_timestamp_ns + 1_000_000_000; // +1 second let price_point = PricePoint::new(profit_price, profit_timestamp); // THEN: Label should be BUY (+1) with profit target result let result = tracker.update(price_point); assert!(result.is_some(), "Should return a label"); let label = result.unwrap(); assert_eq!(label.label_value, 1, "Should be BUY label"); assert!(matches!(label.barrier_result, BarrierResult::ProfitTarget)); assert!(label.return_bps > 0, "Return should be positive"); assert!(label.is_profitable()); assert_eq!(tracker.touched_first, Some(BarrierTouchedFirst::Upper)); } #[test] fn test_profit_target_exact_touch() { // GIVEN: A tracker with 1% profit target let config = BarrierConfig::conservative(); let entry_price_cents = 10000; // $100.00 let entry_timestamp_ns = 1692000000_000_000_000; let mut tracker = BarrierTracker::new(entry_price_cents, entry_timestamp_ns, config); // WHEN: Price touches exactly the upper barrier ($101.00) let upper_barrier = tracker.upper_barrier_cents; let price_point = PricePoint::new(upper_barrier, entry_timestamp_ns + 500_000_000); // THEN: Should trigger profit target let result = tracker.update(price_point); assert!(result.is_some()); let label = result.unwrap(); assert_eq!(label.label_value, 1); assert!(matches!(label.barrier_result, BarrierResult::ProfitTarget)); } #[test] fn test_profit_target_gap_up() { // GIVEN: A tracker with 1% profit target let config = BarrierConfig::conservative(); let entry_price_cents = 10000; let entry_timestamp_ns = 1692000000_000_000_000; let mut tracker = BarrierTracker::new(entry_price_cents, entry_timestamp_ns, config); // WHEN: Price gaps up to $102.50 (far above profit target) let gap_price = utils::price_to_cents(102.50); let price_point = PricePoint::new(gap_price, entry_timestamp_ns + 100_000_000); // THEN: Should still trigger profit target (not miss due to gap) let result = tracker.update(price_point); assert!(result.is_some()); let label = result.unwrap(); assert_eq!(label.label_value, 1); assert!(label.return_bps > 100, "Return should be > 1%"); } // ============================================================================ // TEST 2: Stop Loss Hit First (Lower Barrier) // ============================================================================ #[test] fn test_stop_loss_hit_first() { // GIVEN: A tracker with 0.5% stop loss let config = BarrierConfig::conservative(); let entry_price = 100.00; let entry_price_cents = utils::price_to_cents(entry_price); let entry_timestamp_ns = 1692000000_000_000_000; let mut tracker = BarrierTracker::new(entry_price_cents, entry_timestamp_ns, config); // WHEN: Price drops to $99.40 (below 0.5% stop loss) let stop_price = utils::price_to_cents(99.40); let stop_timestamp = entry_timestamp_ns + 2_000_000_000; // +2 seconds let price_point = PricePoint::new(stop_price, stop_timestamp); // THEN: Label should be SELL (-1) with stop loss result let result = tracker.update(price_point); assert!(result.is_some()); let label = result.unwrap(); assert_eq!(label.label_value, -1, "Should be SELL label"); assert!(matches!(label.barrier_result, BarrierResult::StopLoss)); assert!(label.return_bps < 0, "Return should be negative"); assert!(!label.is_profitable()); assert_eq!(tracker.touched_first, Some(BarrierTouchedFirst::Lower)); } #[test] fn test_stop_loss_exact_touch() { // GIVEN: A tracker with 0.5% stop loss let config = BarrierConfig::conservative(); let entry_price_cents = 10000; let entry_timestamp_ns = 1692000000_000_000_000; let mut tracker = BarrierTracker::new(entry_price_cents, entry_timestamp_ns, config); // WHEN: Price touches exactly the lower barrier ($99.50) let lower_barrier = tracker.lower_barrier_cents; let price_point = PricePoint::new(lower_barrier, entry_timestamp_ns + 1_000_000_000); // THEN: Should trigger stop loss let result = tracker.update(price_point); assert!(result.is_some()); let label = result.unwrap(); assert_eq!(label.label_value, -1); assert!(matches!(label.barrier_result, BarrierResult::StopLoss)); } #[test] fn test_stop_loss_gap_down() { // GIVEN: A tracker let config = BarrierConfig::conservative(); let entry_price_cents = 10000; let entry_timestamp_ns = 1692000000_000_000_000; let mut tracker = BarrierTracker::new(entry_price_cents, entry_timestamp_ns, config); // WHEN: Price gaps down to $97.00 (far below stop loss) let gap_price = utils::price_to_cents(97.00); let price_point = PricePoint::new(gap_price, entry_timestamp_ns + 50_000_000); // THEN: Should still trigger stop loss let result = tracker.update(price_point); assert!(result.is_some()); let label = result.unwrap(); assert_eq!(label.label_value, -1); assert!(label.return_bps < -50, "Return should be < -0.5%"); } // ============================================================================ // TEST 3: Time Horizon Expiry (No Barrier Touch) // ============================================================================ #[test] fn test_time_expiry_no_barrier_touch() { // GIVEN: A tracker with 1-hour time horizon let config = BarrierConfig::conservative(); // 3600s = 1 hour let entry_price_cents = 10000; let entry_timestamp_ns = 1692000000_000_000_000; let mut tracker = BarrierTracker::new(entry_price_cents, entry_timestamp_ns, config); // WHEN: Price stays at $100.30 (within barriers) until expiry let neutral_price = utils::price_to_cents(100.30); let expiry_timestamp = entry_timestamp_ns + 3700_000_000_000; // 1 hour + 100s let price_point = PricePoint::new(neutral_price, expiry_timestamp); // THEN: Label should be HOLD (0) or BUY (1) depending on return sign let result = tracker.update(price_point); assert!(result.is_some()); let label = result.unwrap(); assert!(matches!(label.barrier_result, BarrierResult::TimeExpiry)); // Since price is above entry (100.30 > 100.00), label should be BUY (1) assert_eq!(label.label_value, 1, "Positive return at expiry → BUY"); assert!(label.return_bps > 0); } #[test] fn test_time_expiry_negative_return() { // GIVEN: A tracker let config = BarrierConfig::conservative(); let entry_price_cents = 10000; let entry_timestamp_ns = 1692000000_000_000_000; let mut tracker = BarrierTracker::new(entry_price_cents, entry_timestamp_ns, config); // WHEN: Price is at $99.70 (negative but within stop loss) at expiry let negative_price = utils::price_to_cents(99.70); let expiry_timestamp = entry_timestamp_ns + 3700_000_000_000; let price_point = PricePoint::new(negative_price, expiry_timestamp); // THEN: Label should be SELL (-1) due to negative return let result = tracker.update(price_point); assert!(result.is_some()); let label = result.unwrap(); assert!(matches!(label.barrier_result, BarrierResult::TimeExpiry)); assert_eq!(label.label_value, -1, "Negative return at expiry → SELL"); assert!(label.return_bps < 0); } #[test] fn test_time_expiry_exactly_zero_return() { // GIVEN: A tracker let config = BarrierConfig::conservative(); let entry_price_cents = 10000; let entry_timestamp_ns = 1692000000_000_000_000; let mut tracker = BarrierTracker::new(entry_price_cents, entry_timestamp_ns, config); // WHEN: Price returns exactly to entry price at expiry let same_price = 10000; let expiry_timestamp = entry_timestamp_ns + 3700_000_000_000; let price_point = PricePoint::new(same_price, expiry_timestamp); // THEN: Label should be HOLD (0) due to zero return let result = tracker.update(price_point); assert!(result.is_some()); let label = result.unwrap(); assert!(matches!(label.barrier_result, BarrierResult::TimeExpiry)); assert_eq!(label.label_value, 0, "Zero return at expiry → HOLD"); assert_eq!(label.return_bps, 0); } // ============================================================================ // TEST 4: Barrier Calculation (Volatility-Based) // ============================================================================ #[test] fn test_barrier_calculation_conservative() { // GIVEN: Conservative config (1% profit, 0.5% stop) let config = BarrierConfig::conservative(); let entry_price_cents = 10000; // $100.00 let entry_timestamp_ns = 1692000000_000_000_000; // WHEN: Creating tracker let tracker = BarrierTracker::new(entry_price_cents, entry_timestamp_ns, config); // THEN: Barriers should match expected values assert_eq!( tracker.upper_barrier_cents, 10100, "Upper barrier should be +1% = $101.00" ); assert_eq!( tracker.lower_barrier_cents, 9950, "Lower barrier should be -0.5% = $99.50" ); assert_eq!( tracker.expiry_timestamp_ns, entry_timestamp_ns + 3600_000_000_000, "Expiry should be 1 hour later" ); } #[test] fn test_barrier_calculation_asymmetric() { // GIVEN: Asymmetric config (2% profit, 1% stop) let config = BarrierConfig { profit_target_bps: 200, stop_loss_bps: 100, max_holding_period_ns: 1800_000_000_000, // 30 minutes min_return_threshold_bps: 10, use_sample_weights: true, volatility_lookback_periods: Some(20), }; let entry_price_cents = 50000; // $500.00 let entry_timestamp_ns = 1692000000_000_000_000; // WHEN: Creating tracker let tracker = BarrierTracker::new(entry_price_cents, entry_timestamp_ns, config); // THEN: Barriers should match expected asymmetric values assert_eq!( tracker.upper_barrier_cents, 51000, "Upper barrier should be +2% = $510.00" ); assert_eq!( tracker.lower_barrier_cents, 49500, "Lower barrier should be -1% = $495.00" ); } #[test] fn test_barrier_calculation_edge_case_low_price() { // GIVEN: Low price stock ($0.50) let config = BarrierConfig::conservative(); let entry_price_cents = 50; // $0.50 let entry_timestamp_ns = 1692000000_000_000_000; // WHEN: Creating tracker let tracker = BarrierTracker::new(entry_price_cents, entry_timestamp_ns, config); // THEN: Barriers should still be calculated correctly // Upper: $0.50 * 1.01 = $0.505 (rounded to 50 cents due to integer math) // Lower: $0.50 * 0.995 = $0.4975 (rounded to 49 cents) assert!( tracker.upper_barrier_cents >= entry_price_cents, "Upper barrier should be >= entry" ); assert!( tracker.lower_barrier_cents <= entry_price_cents, "Lower barrier should be <= entry" ); } // ============================================================================ // TEST 5: Edge Cases // ============================================================================ #[test] fn test_multiple_updates_same_tracker() { // GIVEN: A tracker let config = BarrierConfig::conservative(); let mut tracker = BarrierTracker::new(10000, 1692000000_000_000_000, config); // WHEN: Multiple price updates within barriers let price1 = PricePoint::new(10020, 1692000000_000_000_000 + 100_000_000); let price2 = PricePoint::new(10040, 1692000000_000_000_000 + 200_000_000); let price3 = PricePoint::new(10060, 1692000000_000_000_000 + 300_000_000); // THEN: Should return None until barrier is touched assert!(tracker.update(price1).is_none()); assert!(tracker.update(price2).is_none()); assert!(tracker.update(price3).is_none()); assert!(!tracker.is_closed()); // AND WHEN: Final price hits profit target let price_final = PricePoint::new(10150, 1692000000_000_000_000 + 400_000_000); let result = tracker.update(price_final); // THEN: Should return label assert!(result.is_some()); assert!(tracker.is_closed()); } #[test] fn test_tracker_closed_after_barrier_touch() { // GIVEN: A tracker that hit profit target let config = BarrierConfig::conservative(); let mut tracker = BarrierTracker::new(10000, 1692000000_000_000_000, config); let profit_price = PricePoint::new(10150, 1692000000_000_000_000 + 1_000_000_000); let _label = tracker.update(profit_price); assert!(tracker.is_closed()); // WHEN: Trying to update again let new_price = PricePoint::new(10200, 1692000000_000_000_000 + 2_000_000_000); let result = tracker.update(new_price); // THEN: Should return None (tracker is closed) assert!(result.is_none()); } #[test] fn test_extreme_volatility_scenario() { // GIVEN: A tracker with tight barriers let config = BarrierConfig { profit_target_bps: 10, // 0.1% stop_loss_bps: 10, // 0.1% max_holding_period_ns: 60_000_000_000, // 1 minute min_return_threshold_bps: 1, use_sample_weights: true, volatility_lookback_periods: Some(5), }; let entry_price_cents = 10000; let entry_timestamp_ns = 1692000000_000_000_000; let mut tracker = BarrierTracker::new(entry_price_cents, entry_timestamp_ns, config); // WHEN: Price moves very quickly to profit target let fast_profit = PricePoint::new(10011, entry_timestamp_ns + 1_000_000); // 1ms later let result = tracker.update(fast_profit); // THEN: Should still capture the profit assert!(result.is_some()); let label = result.unwrap(); assert_eq!(label.label_value, 1); } #[test] fn test_price_oscillation_around_entry() { // GIVEN: A tracker let config = BarrierConfig::conservative(); let mut tracker = BarrierTracker::new(10000, 1692000000_000_000_000, config); // WHEN: Price oscillates but stays within barriers let prices = vec![ PricePoint::new(10030, 1692000000_000_000_000 + 100_000_000), PricePoint::new(9970, 1692000000_000_000_000 + 200_000_000), PricePoint::new(10020, 1692000000_000_000_000 + 300_000_000), PricePoint::new(9980, 1692000000_000_000_000 + 400_000_000), ]; // THEN: No labels should be generated for price in prices { let result = tracker.update(price); assert!(result.is_none()); } } // ============================================================================ // TEST 6: Label Balance (Symmetric vs Asymmetric Barriers) // ============================================================================ #[test] fn test_symmetric_barriers_balance() { // GIVEN: Symmetric barriers (equal profit and stop) let config = BarrierConfig { profit_target_bps: 100, stop_loss_bps: 100, max_holding_period_ns: 3600_000_000_000, min_return_threshold_bps: 10, use_sample_weights: true, volatility_lookback_periods: Some(20), }; // WHEN: Creating tracker let entry_price_cents = 10000; let entry_timestamp_ns = 1692000000_000_000_000; let tracker = BarrierTracker::new(entry_price_cents, entry_timestamp_ns, config); // THEN: Upper and lower barriers should be equidistant from entry let upper_distance = tracker.upper_barrier_cents - entry_price_cents; let lower_distance = entry_price_cents - tracker.lower_barrier_cents; assert_eq!( upper_distance, lower_distance, "Symmetric barriers should have equal distance" ); } #[test] fn test_asymmetric_barriers_reduce_false_positives() { // GIVEN: Asymmetric barriers (profit > stop) let config = BarrierConfig { profit_target_bps: 200, // 2x the stop loss stop_loss_bps: 100, max_holding_period_ns: 3600_000_000_000, min_return_threshold_bps: 10, use_sample_weights: true, volatility_lookback_periods: Some(20), }; let entry_price_cents = 10000; let entry_timestamp_ns = 1692000000_000_000_000; let tracker = BarrierTracker::new(entry_price_cents, entry_timestamp_ns, config); // THEN: Profit barrier should be farther from entry than stop loss let upper_distance = tracker.upper_barrier_cents - entry_price_cents; let lower_distance = entry_price_cents - tracker.lower_barrier_cents; assert!( upper_distance > lower_distance, "Asymmetric barriers: profit target further than stop loss" ); assert_eq!(upper_distance, lower_distance * 2); } // ============================================================================ // TEST 7: Quality Score Validation // ============================================================================ #[test] fn test_quality_score_profit_target() { // GIVEN: A tracker that hits profit target let config = BarrierConfig::conservative(); let mut tracker = BarrierTracker::new(10000, 1692000000_000_000_000, config); // WHEN: Hitting profit target let profit_price = PricePoint::new(10150, 1692000000_000_000_000 + 1_000_000_000); let result = tracker.update(profit_price); // THEN: Quality score should be high (0.9) assert!(result.is_some()); let label = result.unwrap(); assert!( label.quality_score >= 0.85, "Profit targets should have high quality score" ); } #[test] fn test_quality_score_stop_loss() { // GIVEN: A tracker that hits stop loss let config = BarrierConfig::conservative(); let mut tracker = BarrierTracker::new(10000, 1692000000_000_000_000, config); // WHEN: Hitting stop loss let stop_price = PricePoint::new(9940, 1692000000_000_000_000 + 1_000_000_000); let result = tracker.update(stop_price); // THEN: Quality score should be moderate (0.8) assert!(result.is_some()); let label = result.unwrap(); assert!( label.quality_score >= 0.75, "Stop losses should have moderate quality score" ); } #[test] fn test_quality_score_time_expiry() { // GIVEN: A tracker that expires let config = BarrierConfig::conservative(); let mut tracker = BarrierTracker::new(10000, 1692000000_000_000_000, config); // WHEN: Time expiry with neutral price let neutral_price = PricePoint::new(10020, 1692000000_000_000_000 + 3700_000_000_000); let result = tracker.update(neutral_price); // THEN: Quality score should be low (0.5) assert!(result.is_some()); let label = result.unwrap(); assert!( label.quality_score <= 0.6, "Time expiry should have lower quality score" ); } // ============================================================================ // TEST 8: Engine Multi-Tracker Tests // ============================================================================ #[test] fn test_engine_start_tracking() { // GIVEN: An empty engine let mut engine = TripleBarrierEngine::new(1000); assert_eq!(engine.active_count(), 0); // WHEN: Starting new tracker let config = BarrierConfig::conservative(); let result = engine.start_tracking(config, 10000, 1692000000_000_000_000); // THEN: Should return tracker ID and increment count assert!(result.is_ok()); assert_eq!(engine.active_count(), 1); } #[test] fn test_engine_max_active_trackers() { // GIVEN: An engine with max 2 trackers let mut engine = TripleBarrierEngine::new(2); // WHEN: Starting 2 trackers (should succeed) let config = BarrierConfig::conservative(); let r1 = engine.start_tracking(config.clone(), 10000, 1692000000_000_000_000); let r2 = engine.start_tracking(config.clone(), 10100, 1692000000_000_000_000); assert!(r1.is_ok()); assert!(r2.is_ok()); assert_eq!(engine.active_count(), 2); // WHEN: Starting 3rd tracker (should fail) let r3 = engine.start_tracking(config, 10200, 1692000000_000_000_000); // THEN: Should return error assert!(r3.is_err()); assert_eq!(engine.active_count(), 2); } #[test] fn test_engine_update_all() { // GIVEN: Engine with multiple trackers at different entry prices let mut engine = TripleBarrierEngine::new(100); let config = BarrierConfig::conservative(); // Start 5 trackers with varying entry prices for i in 0..5 { let entry_price = 10000 + i * 100; let _ = engine.start_tracking(config.clone(), entry_price, 1692000000_000_000_000); } assert_eq!(engine.active_count(), 5); // WHEN: Price moves to a level that hits some profit targets let price_point = PricePoint::new(10200, 1692000000_000_000_000 + 1_000_000_000); let labels = engine.update_all(price_point); // THEN: Some trackers should close (those with profit targets hit) assert!(labels.len() > 0, "Should generate some labels"); assert!(engine.active_count() < 5, "Some trackers should be closed"); assert_eq!(engine.completed_count() as usize, labels.len()); } #[test] fn test_engine_expire_old_trackers() { // GIVEN: Engine with trackers at entry time T0 let mut engine = TripleBarrierEngine::new(100); let config = BarrierConfig::conservative(); // 1 hour max holding let entry_timestamp = 1692000000_000_000_000; // Start 3 trackers for i in 0..3 { let entry_price = 10000 + i * 100; let _ = engine.start_tracking(config.clone(), entry_price, entry_timestamp); } // WHEN: Forcing expiry at T0 + 2 hours (past 1-hour limit) let expiry_timestamp = entry_timestamp + 7200_000_000_000; let expired_labels = engine.expire_old_trackers(expiry_timestamp); // THEN: All 3 trackers should be expired assert_eq!(expired_labels.len(), 3, "All trackers should expire"); assert_eq!(engine.active_count(), 0, "No active trackers left"); // All labels should be time expiry for label in &expired_labels { assert!(matches!(label.barrier_result, BarrierResult::TimeExpiry)); } } #[test] fn test_engine_drain_completed_labels() { // GIVEN: Engine with completed labels let mut engine = TripleBarrierEngine::new(100); let config = BarrierConfig::conservative(); // Start and complete a tracker let _ = engine.start_tracking(config, 10000, 1692000000_000_000_000); let price_point = PricePoint::new(10150, 1692000000_000_000_000 + 1_000_000_000); let _labels = engine.update_all(price_point); // WHEN: Draining completed labels let drained = engine.drain_completed_labels(); // THEN: Should return labels and clear internal buffer assert_eq!(drained.len(), 1); // Draining again should return empty let drained_again = engine.drain_completed_labels(); assert_eq!(drained_again.len(), 0); } #[test] fn test_engine_get_tracker() { // GIVEN: Engine with a tracker let mut engine = TripleBarrierEngine::new(100); let config = BarrierConfig::conservative(); let tracker_id = engine .start_tracking(config, 10000, 1692000000_000_000_000) .unwrap(); // WHEN: Getting tracker by ID let tracker = engine.get_tracker(&tracker_id); // THEN: Should return the tracker assert!(tracker.is_some()); let tracker = tracker.unwrap(); assert_eq!(tracker.entry_price_cents, 10000); assert!(!tracker.is_closed()); } #[test] fn test_engine_clear() { // GIVEN: Engine with multiple active trackers let mut engine = TripleBarrierEngine::new(100); let config = BarrierConfig::conservative(); for i in 0..5 { let _ = engine.start_tracking(config.clone(), 10000 + i * 100, 1692000000_000_000_000); } // WHEN: Clearing the engine engine.clear(); // THEN: All trackers and stats should be reset assert_eq!(engine.active_count(), 0); assert_eq!(engine.completed_count(), 0); } // ============================================================================ // TEST 9: Performance Tests (<80μs latency target) // ============================================================================ #[test] fn test_latency_single_update() { use std::time::Instant; // GIVEN: A tracker let config = BarrierConfig::conservative(); let mut tracker = BarrierTracker::new(10000, 1692000000_000_000_000, config); // WHEN: Updating with a price point let price_point = PricePoint::new(10150, 1692000000_000_000_000 + 1_000_000_000); let start = Instant::now(); let result = tracker.update(price_point); let elapsed_us = start.elapsed().as_micros(); // THEN: Should complete in <80μs assert!(result.is_some()); assert!( elapsed_us < 80, "Single update should be <80μs, got {}μs", elapsed_us ); } #[test] fn test_latency_engine_update_all() { use std::time::Instant; // GIVEN: Engine with 100 active trackers let mut engine = TripleBarrierEngine::new(1000); let config = BarrierConfig::conservative(); for i in 0..100 { let _ = engine.start_tracking( config.clone(), 10000 + (i % 20) * 10, 1692000000_000_000_000, ); } // WHEN: Updating all trackers let price_point = PricePoint::new(10150, 1692000000_000_000_000 + 1_000_000_000); let start = Instant::now(); let _labels = engine.update_all(price_point); let elapsed_us = start.elapsed().as_micros(); // THEN: Should complete in reasonable time (<10ms for 100 trackers) assert!( elapsed_us < 10_000, "Update 100 trackers should be <10ms, got {}μs", elapsed_us ); } #[test] fn test_throughput_batch_processing() { use std::time::Instant; // GIVEN: Engine with many trackers let mut engine = TripleBarrierEngine::new(10_000); let config = BarrierConfig::conservative(); // Start 1000 trackers for i in 0..1000 { let _ = engine.start_tracking( config.clone(), 10000 + (i % 50) * 10, 1692000000_000_000_000 + i * 1_000_000, ); } // WHEN: Processing 100 price updates let start = Instant::now(); let mut total_labels = 0; for i in 0..100 { let price = 10000 + (i % 300); let timestamp = 1692000000_000_000_000 + i * 10_000_000; let price_point = PricePoint::new(price, timestamp); let labels = engine.update_all(price_point); total_labels += labels.len(); } let elapsed_ms = start.elapsed().as_millis(); let throughput = (total_labels as f64 / elapsed_ms as f64) * 1000.0; // THEN: Should achieve >10K labels/second info!( total_labels, elapsed_ms, labels_per_sec = format_args!("{:.0}", throughput), "Processed labels" ); assert!( throughput > 10_000.0, "Should achieve >10K labels/sec, got {:.0}", throughput ); } // ============================================================================ // TEST 10: Integration Tests (Real-World Scenarios) // ============================================================================ #[test] fn test_realistic_trading_scenario() { // GIVEN: A realistic trading scenario with ES futures let config = BarrierConfig { profit_target_bps: 50, // 0.5% profit target (realistic for ES) stop_loss_bps: 25, // 0.25% stop loss (2:1 risk-reward) max_holding_period_ns: 900_000_000_000, // 15 minutes min_return_threshold_bps: 5, use_sample_weights: true, volatility_lookback_periods: Some(20), }; let mut engine = TripleBarrierEngine::new(1000); let entry_price = 475000; // ES at $4,750.00 let entry_timestamp = 1692000000_000_000_000; // WHEN: Starting position let tracker_id = engine .start_tracking(config, entry_price, entry_timestamp) .unwrap(); // Simulate price movement over 5 minutes (profit scenario) let price_updates = vec![ (475100, entry_timestamp + 60_000_000_000), // +1 min: $4,751 (475200, entry_timestamp + 120_000_000_000), // +2 min: $4,752 (475300, entry_timestamp + 180_000_000_000), // +3 min: $4,753 (475400, entry_timestamp + 240_000_000_000), // +4 min: $4,754 (477500, entry_timestamp + 300_000_000_000), // +5 min: $4,775 (hit profit) ]; let mut final_label = None; for (price, timestamp) in price_updates { let price_point = PricePoint::new(price, timestamp); if let Some(label) = engine.update_tracker(tracker_id, price_point) { final_label = Some(label); break; } } // THEN: Should hit profit target assert!(final_label.is_some()); let label = final_label.unwrap(); assert_eq!(label.label_value, 1); assert!(matches!(label.barrier_result, BarrierResult::ProfitTarget)); assert!(label.return_bps >= 50); // At least 0.5% return } #[test] fn test_config_validation() { // GIVEN: Invalid config (stop loss >= profit target) let invalid_config = BarrierConfig { profit_target_bps: 50, stop_loss_bps: 100, // Greater than profit target max_holding_period_ns: 3600_000_000_000, min_return_threshold_bps: 10, use_sample_weights: true, volatility_lookback_periods: Some(20), }; // WHEN: Validating config let result = invalid_config.validate(); // THEN: Should return error assert!(result.is_err()); } #[test] fn test_return_calculation_accuracy() { // GIVEN: A tracker let config = BarrierConfig::conservative(); let entry_price_cents = 10000; // $100.00 let mut tracker = BarrierTracker::new(entry_price_cents, 1692000000_000_000_000, config); // WHEN: Price moves to $102.50 (exactly +2.5%) let profit_price = 10250; let price_point = PricePoint::new(profit_price, 1692000000_000_000_000 + 1_000_000_000); let result = tracker.update(price_point); // THEN: Return should be exactly 250 bps (2.5%) assert!(result.is_some()); let label = result.unwrap(); assert_eq!(label.return_bps, 250, "Return should be exactly 2.5%"); assert!( (label.return_as_ratio() - 0.025).abs() < 1e-10, "Return ratio should be 0.025" ); }