Files
foxhunt/crates/ml/tests/triple_barrier_test.rs
jgrusewski ca4c38d921 fix(tests): CI GPU test stability, walltime reduction, BF16 tolerance
- Reduce CI GPU test datasets 16x for walltime reduction
- Reduce early-stop epochs 50→10, add --test-threads=1
- Serialize all GPU lib tests to prevent cuBLAS init race
- Align state_dim to 16 for BF16 tensor core HMMA dispatch
- BF16 precision tolerance in ml-dqn tests
- Enable branching DQN + tracing subscriber in smoke tests
- Prevent min_replay_size > buffer_size deadlock in early-stop tests
- Prevent AutoReplaySizer from breaking gradient collapse warmup
- Replace racy tokio::spawn checkpoint counter with AtomicUsize
- Set warmup_steps=0 and max_training_steps_per_epoch=300 in early-stop tests
- RealDataLoader respects TEST_DATA_DIR for CI PVC layout
- Add collapse_warmup_capacity to gpu_smoketest DQNConfig
- Drain CUDA context between test binaries
- Detached HEAD checkout prevents local branch corruption
- GPU pipeline tests: fix BF16 dtype and rank-1 squeeze assertions
- OOD input handling tests use use_gpu: true

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-15 12:00:13 +01:00

965 lines
33 KiB
Rust

#![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"
);
}