Files
foxhunt/crates/ml/tests/dqn_hft_barriers_test.rs
jgrusewski db6462ba7a fix(clippy): resolve all clippy warnings across entire workspace (--all-targets)
Systematic fix of 360+ clippy errors across 37+ crates covering lib,
test, bench, and example targets. Key changes:

- Add targeted #[allow(...)] on #[cfg(test)] modules for test-only lints
  (assertions_on_result_states, float_cmp, str_to_string, indexing, etc.)
- Feature-gate broken integration tests behind __<crate>_integration flags
  where public APIs changed (trading-service, backtesting-service, etc.)
- Remove dead [[test]] entries from Cargo.toml files pointing to deleted files
- Fix production code: field_reassign_with_default, manual_range_contains,
  assert!(false) → panic!(), format!("{}") simplification, len() > 0 → !is_empty()
- Delete truly unused code (Order struct, unused methods/fields/variants)
- Convert sqlx::query!() to sqlx::query() for SQLX_OFFLINE compatibility

Result: cargo clippy --workspace --all-targets -- -D warnings = 0 errors, 0 warnings

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

550 lines
20 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#![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 tests for HFT barrier presets
//!
//! This test suite validates the barrier preset functionality for DQN training,
//! ensuring that scalping and mean-reversion presets produce appropriate episode
//! characteristics and trading behavior.
use ml::labeling::triple_barrier::{BarrierTracker, PricePoint};
use ml::labeling::types::{BarrierConfig, BarrierResult};
#[test]
fn test_scalping_preset_params() {
// Scalping preset: 7 bps profit, 4 bps stop, 30 seconds
let config = BarrierConfig {
profit_target_bps: 7,
stop_loss_bps: 4,
max_holding_period_ns: 30_000_000_000, // 30 seconds in nanoseconds
min_return_threshold_bps: 1,
use_sample_weights: true,
volatility_lookback_periods: Some(20),
};
let entry_price_cents = 500_000; // $5,000.00 ES futures
let entry_timestamp_ns = 1_700_000_000_000_000_000; // Nov 14, 2023
let tracker = BarrierTracker::new(entry_price_cents, entry_timestamp_ns, config);
// Verify barrier calculations
// Profit target: 5000 * (7/10000) = $3.50 = 350 cents
let expected_upper = entry_price_cents + (entry_price_cents * 7) / 10_000;
assert_eq!(
tracker.upper_barrier_cents, expected_upper,
"Scalping profit target should be 7 bps above entry"
);
// Stop loss: 5000 * (4/10000) = $2.00 = 200 cents
let expected_lower = entry_price_cents - (entry_price_cents * 4) / 10_000;
assert_eq!(
tracker.lower_barrier_cents, expected_lower,
"Scalping stop loss should be 4 bps below entry"
);
// Time limit: 30 seconds
let expected_expiry = entry_timestamp_ns + 30_000_000_000;
assert_eq!(
tracker.expiry_timestamp_ns, expected_expiry,
"Scalping max hold time should be 30 seconds"
);
// Verify risk/reward ratio
let profit_range = tracker.upper_barrier_cents - entry_price_cents;
let loss_range = entry_price_cents - tracker.lower_barrier_cents;
let rr_ratio = profit_range as f64 / loss_range as f64;
assert!(
(rr_ratio - 1.75).abs() < 0.1,
"Scalping RR ratio should be ~1.75 (got {})",
rr_ratio
);
}
#[test]
fn test_mean_reversion_preset_params() {
// Mean-reversion preset: 25 bps profit, 12 bps stop, 150 seconds
let config = BarrierConfig {
profit_target_bps: 25,
stop_loss_bps: 12,
max_holding_period_ns: 150_000_000_000, // 150 seconds (2.5 minutes)
min_return_threshold_bps: 5,
use_sample_weights: true,
volatility_lookback_periods: Some(20),
};
let entry_price_cents = 500_000; // $5,000.00
let entry_timestamp_ns = 1_700_000_000_000_000_000;
let tracker = BarrierTracker::new(entry_price_cents, entry_timestamp_ns, config);
// Profit target: 5000 * (25/10000) = $12.50 = 1250 cents
let expected_upper = entry_price_cents + (entry_price_cents * 25) / 10_000;
assert_eq!(
tracker.upper_barrier_cents, expected_upper,
"Mean-reversion profit target should be 25 bps above entry"
);
// Stop loss: 5000 * (12/10000) = $6.00 = 600 cents
let expected_lower = entry_price_cents - (entry_price_cents * 12) / 10_000;
assert_eq!(
tracker.lower_barrier_cents, expected_lower,
"Mean-reversion stop loss should be 12 bps below entry"
);
// Time limit: 150 seconds
let expected_expiry = entry_timestamp_ns + 150_000_000_000;
assert_eq!(
tracker.expiry_timestamp_ns, expected_expiry,
"Mean-reversion max hold time should be 150 seconds"
);
// Verify risk/reward ratio
let profit_range = tracker.upper_barrier_cents - entry_price_cents;
let loss_range = entry_price_cents - tracker.lower_barrier_cents;
let rr_ratio = profit_range as f64 / loss_range as f64;
assert!(
(rr_ratio - 2.08).abs() < 0.1,
"Mean-reversion RR ratio should be ~2.08 (got {})",
rr_ratio
);
}
#[test]
fn test_generic_preset_params() {
// Generic preset: 100 bps profit, 50 bps stop, 3600 seconds
let config = BarrierConfig {
profit_target_bps: 100,
stop_loss_bps: 50,
max_holding_period_ns: 3_600_000_000_000, // 1 hour
min_return_threshold_bps: 10,
use_sample_weights: true,
volatility_lookback_periods: Some(20),
};
let entry_price_cents = 500_000;
let entry_timestamp_ns = 1_700_000_000_000_000_000;
let tracker = BarrierTracker::new(entry_price_cents, entry_timestamp_ns, config);
// Profit target: 5000 * (100/10000) = $50.00 = 5000 cents
let expected_upper = entry_price_cents + (entry_price_cents * 100) / 10_000;
assert_eq!(tracker.upper_barrier_cents, expected_upper);
// Stop loss: 5000 * (50/10000) = $25.00 = 2500 cents
let expected_lower = entry_price_cents - (entry_price_cents * 50) / 10_000;
assert_eq!(tracker.lower_barrier_cents, expected_lower);
// Time limit: 1 hour
let expected_expiry = entry_timestamp_ns + 3_600_000_000_000;
assert_eq!(tracker.expiry_timestamp_ns, expected_expiry);
// Verify risk/reward ratio
let profit_range = tracker.upper_barrier_cents - entry_price_cents;
let loss_range = entry_price_cents - tracker.lower_barrier_cents;
let rr_ratio = profit_range as f64 / loss_range as f64;
assert!(
(rr_ratio - 2.0).abs() < 0.01,
"Generic RR ratio should be 2.0 (got {})",
rr_ratio
);
}
#[test]
fn test_scalping_episode_duration() {
// Scalping episodes should terminate quickly (profit or stop hit within 30s)
let config = BarrierConfig {
profit_target_bps: 7,
stop_loss_bps: 4,
max_holding_period_ns: 30_000_000_000,
min_return_threshold_bps: 1,
use_sample_weights: true,
volatility_lookback_periods: Some(20),
};
let entry_price_cents = 500_000;
let entry_timestamp_ns = 1_700_000_000_000_000_000;
let mut tracker = BarrierTracker::new(entry_price_cents, entry_timestamp_ns, config.clone());
// Test 1: Profit target hit after 5 seconds
let profit_price = tracker.upper_barrier_cents;
let profit_time = entry_timestamp_ns + 5_000_000_000; // 5 seconds
let profit_point = PricePoint::new(profit_price, profit_time);
let result = tracker.update(profit_point);
assert!(result.is_some(), "Should hit profit target");
let label = result.unwrap();
assert!(matches!(label.barrier_result, BarrierResult::ProfitTarget));
assert_eq!(label.label_value, 1); // Profitable exit
// Test 2: Stop loss hit after 3 seconds
let mut tracker2 = BarrierTracker::new(entry_price_cents, entry_timestamp_ns, config.clone());
let stop_price = tracker2.lower_barrier_cents;
let stop_time = entry_timestamp_ns + 3_000_000_000; // 3 seconds
let stop_point = PricePoint::new(stop_price, stop_time);
let result2 = tracker2.update(stop_point);
assert!(result2.is_some(), "Should hit stop loss");
let label2 = result2.unwrap();
assert!(matches!(label2.barrier_result, BarrierResult::StopLoss));
assert_eq!(label2.label_value, -1); // Loss exit
// Test 3: Time expiry at 30 seconds
let mut tracker3 = BarrierTracker::new(entry_price_cents, entry_timestamp_ns, config);
let expiry_price = entry_price_cents; // No movement
let expiry_time = entry_timestamp_ns + 30_000_000_000; // Exactly 30s
let expiry_point = PricePoint::new(expiry_price, expiry_time);
let result3 = tracker3.update(expiry_point);
assert!(result3.is_some(), "Should expire at 30 seconds");
let label3 = result3.unwrap();
assert!(matches!(label3.barrier_result, BarrierResult::TimeExpiry));
}
#[test]
fn test_mean_reversion_episode_duration() {
// Mean-reversion episodes should allow longer holds (up to 150s)
let config = BarrierConfig {
profit_target_bps: 25,
stop_loss_bps: 12,
max_holding_period_ns: 150_000_000_000,
min_return_threshold_bps: 5,
use_sample_weights: true,
volatility_lookback_periods: Some(20),
};
let entry_price_cents = 500_000;
let entry_timestamp_ns = 1_700_000_000_000_000_000;
// Test: Patient hold up to 150 seconds
let mut tracker = BarrierTracker::new(entry_price_cents, entry_timestamp_ns, config);
// Update at 100 seconds with no barrier hit (price unchanged)
let mid_time = entry_timestamp_ns + 100_000_000_000;
let mid_point = PricePoint::new(entry_price_cents, mid_time);
let mid_result = tracker.update(mid_point);
assert!(mid_result.is_none(), "Should not terminate at 100s");
// Update at 150 seconds (expiry)
let expiry_time = entry_timestamp_ns + 150_000_000_000;
let expiry_point = PricePoint::new(entry_price_cents, expiry_time);
let expiry_result = tracker.update(expiry_point);
assert!(expiry_result.is_some(), "Should expire at 150s");
let label = expiry_result.unwrap();
assert!(matches!(label.barrier_result, BarrierResult::TimeExpiry));
}
#[test]
fn test_scalping_vs_mean_reversion_time_horizon() {
// Compare episode duration expectations for different presets
let scalping_config = BarrierConfig {
profit_target_bps: 7,
stop_loss_bps: 4,
max_holding_period_ns: 30_000_000_000,
min_return_threshold_bps: 1,
use_sample_weights: true,
volatility_lookback_periods: Some(20),
};
let mean_rev_config = BarrierConfig {
profit_target_bps: 25,
stop_loss_bps: 12,
max_holding_period_ns: 150_000_000_000,
min_return_threshold_bps: 5,
use_sample_weights: true,
volatility_lookback_periods: Some(20),
};
let entry_price_cents = 500_000;
let entry_timestamp_ns = 1_700_000_000_000_000_000;
let scalping_tracker =
BarrierTracker::new(entry_price_cents, entry_timestamp_ns, scalping_config);
let mean_rev_tracker =
BarrierTracker::new(entry_price_cents, entry_timestamp_ns, mean_rev_config);
// Mean-reversion should allow 5x longer holds
let scalping_duration =
scalping_tracker.expiry_timestamp_ns - scalping_tracker.entry_timestamp_ns;
let mean_rev_duration =
mean_rev_tracker.expiry_timestamp_ns - mean_rev_tracker.entry_timestamp_ns;
assert_eq!(
scalping_duration, 30_000_000_000,
"Scalping max hold: 30s"
);
assert_eq!(
mean_rev_duration, 150_000_000_000,
"Mean-reversion max hold: 150s"
);
assert_eq!(
mean_rev_duration / scalping_duration,
5,
"Mean-reversion allows 5x longer holds"
);
// Mean-reversion should have wider profit targets (3.6x)
let scalping_profit_range =
scalping_tracker.upper_barrier_cents - scalping_tracker.entry_price_cents;
let mean_rev_profit_range =
mean_rev_tracker.upper_barrier_cents - mean_rev_tracker.entry_price_cents;
assert!(
mean_rev_profit_range > scalping_profit_range * 3,
"Mean-reversion profit target should be >3x wider (scalping: {} cents, mean-rev: {} cents)",
scalping_profit_range,
mean_rev_profit_range
);
}
#[test]
fn test_preset_barrier_hit_probability() {
// Validate that barrier parameters are realistic for HFT
// ES futures typical tick: 0.25 points = 0.005% = 0.5 bps
// Scalping targets should be reachable within a few ticks
let entry_price_cents = 500_000; // $5,000
let entry_timestamp_ns = 1_700_000_000_000_000_000;
// Scalping: 7 bps = 14 ticks (0.25pt each), very reachable
let scalping_config = BarrierConfig {
profit_target_bps: 7,
stop_loss_bps: 4,
max_holding_period_ns: 30_000_000_000,
min_return_threshold_bps: 1,
use_sample_weights: true,
volatility_lookback_periods: Some(20),
};
let scalping_tracker =
BarrierTracker::new(entry_price_cents, entry_timestamp_ns, scalping_config);
// Calculate ticks to profit
let profit_cents = scalping_tracker.upper_barrier_cents - entry_price_cents;
let tick_size_cents = 25; // 0.25 points = 25 cents for ES
let ticks_to_profit = profit_cents / tick_size_cents;
assert!(
ticks_to_profit >= 10 && ticks_to_profit <= 20,
"Scalping profit should be 10-20 ticks (got {})",
ticks_to_profit
);
// Mean-reversion: 25 bps = 50 ticks, reachable within 2-5 minutes
let mean_rev_config = BarrierConfig {
profit_target_bps: 25,
stop_loss_bps: 12,
max_holding_period_ns: 150_000_000_000,
min_return_threshold_bps: 5,
use_sample_weights: true,
volatility_lookback_periods: Some(20),
};
let mean_rev_tracker =
BarrierTracker::new(entry_price_cents, entry_timestamp_ns, mean_rev_config);
let mean_rev_profit_cents = mean_rev_tracker.upper_barrier_cents - entry_price_cents;
let mean_rev_ticks = mean_rev_profit_cents / tick_size_cents;
assert!(
mean_rev_ticks >= 40 && mean_rev_ticks <= 60,
"Mean-reversion profit should be 40-60 ticks (got {})",
mean_rev_ticks
);
}
#[test]
fn test_barrier_config_conservative() {
// Test BarrierConfig::conservative() helper method
let config = BarrierConfig::conservative();
// Conservative config should use generic preset values
assert_eq!(config.profit_target_bps, 100, "Conservative profit: 100 bps");
assert_eq!(config.stop_loss_bps, 50, "Conservative stop: 50 bps");
// Max holding period should be 1 hour (3600 seconds)
let expected_ns = 3_600_000_000_000; // 3600s in nanoseconds
assert_eq!(
config.max_holding_period_ns, expected_ns,
"Conservative max hold: 1 hour"
);
}
#[cfg(test)]
mod integration_tests {
use super::*;
#[test]
fn test_scalping_realistic_scenario() {
// Simulate 10 scalping trades over 5 minutes
let config = BarrierConfig {
profit_target_bps: 7,
stop_loss_bps: 4,
max_holding_period_ns: 30_000_000_000,
min_return_threshold_bps: 1,
use_sample_weights: true,
volatility_lookback_periods: Some(20),
};
let mut successful_trades = 0;
let mut total_return_bps = 0i64;
// Entry price: $5,000.00
let base_price_cents = 500_000;
let base_timestamp_ns = 1_700_000_000_000_000_000;
// Simulate 10 trades with realistic outcomes
for i in 0..10 {
let entry_time = base_timestamp_ns + (i * 30_000_000_000); // 30s apart
let mut tracker = BarrierTracker::new(base_price_cents, entry_time, config.clone());
// 60% hit profit, 30% hit stop, 10% time out
let outcome = i % 10;
let result = if outcome < 6 {
// Profit target (7 bps gain)
let profit_price = tracker.upper_barrier_cents;
let profit_time = entry_time + 5_000_000_000; // 5s
tracker.update(PricePoint::new(profit_price, profit_time))
} else if outcome < 9 {
// Stop loss (4 bps loss)
let stop_price = tracker.lower_barrier_cents;
let stop_time = entry_time + 8_000_000_000; // 8s
tracker.update(PricePoint::new(stop_price, stop_time))
} else {
// Time expiry (neutral)
let expiry_price = base_price_cents;
let expiry_time = entry_time + 30_000_000_000;
tracker.update(PricePoint::new(expiry_price, expiry_time))
};
if let Some(label) = result {
if label.label_value == 1 {
successful_trades += 1;
}
total_return_bps += label.return_bps as i64;
}
}
// Expected: 60% win rate, positive total return
assert_eq!(
successful_trades, 6,
"Should have 6 profitable trades (60% win rate)"
);
// Net return: 6 trades × 7 bps - 3 trades × 4 bps = 42 - 12 = 30 bps
assert!(
total_return_bps > 0,
"Scalping should be profitable with 60% win rate (got {} bps)",
total_return_bps
);
}
#[test]
fn test_mean_reversion_realistic_scenario() {
// Simulate mean-reversion trade with patient hold
let config = BarrierConfig {
profit_target_bps: 25,
stop_loss_bps: 12,
max_holding_period_ns: 150_000_000_000,
min_return_threshold_bps: 5,
use_sample_weights: true,
volatility_lookback_periods: Some(20),
};
let entry_price_cents = 500_000;
let entry_time_ns = 1_700_000_000_000_000_000;
let mut tracker = BarrierTracker::new(entry_price_cents, entry_time_ns, config);
// Price drifts down initially (15 bps loss), then reverts to profit
// This tests the patience of mean-reversion strategy
// Update 1: Price drops 10 bps (within stop loss tolerance)
let down_price = entry_price_cents - (entry_price_cents * 10) / 10_000;
let down_time = entry_time_ns + 30_000_000_000; // 30s
let result1 = tracker.update(PricePoint::new(down_price, down_time));
assert!(result1.is_none(), "Should not trigger stop at -10 bps");
// Update 2: Price reverts up to +25 bps (profit target)
let up_price = tracker.upper_barrier_cents;
let up_time = entry_time_ns + 120_000_000_000; // 120s (2 minutes)
let result2 = tracker.update(PricePoint::new(up_price, up_time));
assert!(result2.is_some(), "Should hit profit target on reversion");
let label = result2.unwrap();
assert_eq!(label.label_value, 1, "Should be profitable exit");
assert!(
label.return_bps >= 20,
"Should capture at least 20 bps (got {})",
label.return_bps
);
}
}