Files
foxhunt/crates/ml/tests/ppo_continuous_transaction_costs_tests.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

633 lines
18 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 continuous transaction costs
//!
//! Tests cost computation, slippage models, gradients, and order type differences
//! for continuous position sizing in PPO.
use ml::ppo::continuous_transaction_costs::{
conservative_cost_model, default_hft_cost_model, zero_cost_model,
ContinuousTransactionCosts, OrderType, SlippageModel,
};
#[test]
fn test_order_type_cost_bps_values() {
assert_eq!(OrderType::Market.cost_bps(), 15.0);
assert_eq!(OrderType::LimitMaker.cost_bps(), 5.0);
assert_eq!(OrderType::IoC.cost_bps(), 10.0);
}
#[test]
fn test_order_type_cost_decimal_conversion() {
assert!((OrderType::Market.cost_decimal() - 0.0015).abs() < 1e-9);
assert!((OrderType::LimitMaker.cost_decimal() - 0.0005).abs() < 1e-9);
assert!((OrderType::IoC.cost_decimal() - 0.001).abs() < 1e-9);
}
#[test]
fn test_order_type_cost_ordering() {
assert!(OrderType::Market.cost_bps() > OrderType::IoC.cost_bps());
assert!(OrderType::IoC.cost_bps() > OrderType::LimitMaker.cost_bps());
}
#[test]
fn test_compute_cost_no_slippage_small_trade() {
let costs = ContinuousTransactionCosts::new(
5.0, // 5 bps
SlippageModel::None,
OrderType::LimitMaker,
5000.0,
);
// 0.5 contracts @ $5000 = $2500 trade
// Cost: $2500 × 0.0005 = $1.25
let cost = costs.compute_cost(0.5, 0.0, 0.5);
assert!((cost - 1.25).abs() < 0.01);
}
#[test]
fn test_compute_cost_no_slippage_unit_trade() {
let costs = ContinuousTransactionCosts::new(
5.0,
SlippageModel::None,
OrderType::LimitMaker,
5000.0,
);
// 1 contract @ $5000 = $5000 trade
// Cost: $5000 × 0.0005 = $2.50
let cost = costs.compute_cost(1.0, 0.0, 1.0);
assert!((cost - 2.5).abs() < 0.01);
}
#[test]
fn test_compute_cost_no_slippage_large_trade() {
let costs = ContinuousTransactionCosts::new(
5.0,
SlippageModel::None,
OrderType::LimitMaker,
5000.0,
);
// 10 contracts @ $5000 = $50,000 trade
// Cost: $50,000 × 0.0005 = $25.00
let cost = costs.compute_cost(10.0, 0.0, 10.0);
assert!((cost - 25.0).abs() < 0.1);
}
#[test]
fn test_compute_cost_linear_slippage_scaling() {
let costs = ContinuousTransactionCosts::new(
5.0, // 5 bps base
SlippageModel::Linear { slope: 0.1 },
OrderType::LimitMaker,
5000.0,
);
// 1 contract: base=$5000×0.0005=$2.50, slippage=$5000×0.00001=$0.05, total=$2.55
let cost_1 = costs.compute_cost(1.0, 0.0, 1.0);
assert!((cost_1 - 2.55).abs() < 0.01);
// 2 contracts: base=$10000×0.0005=$5.00, slippage=$10000×0.00002=$0.20, total=$5.20
let cost_2 = costs.compute_cost(2.0, 0.0, 2.0);
assert!((cost_2 - 5.20).abs() < 0.02);
// 5 contracts: base=$25000×0.0005=$12.50, slippage=$25000×0.00005=$1.25, total=$13.75
let cost_5 = costs.compute_cost(5.0, 0.0, 5.0);
assert!((cost_5 - 13.75).abs() < 0.05);
}
#[test]
fn test_compute_cost_quadratic_slippage_scaling() {
let costs = ContinuousTransactionCosts::new(
5.0,
SlippageModel::Quadratic { coefficient: 0.5 },
OrderType::LimitMaker,
5000.0,
);
// 1 contract: base=2.50, slippage=0.25, total=2.75
let cost_1 = costs.compute_cost(1.0, 0.0, 1.0);
assert!((cost_1 - 2.75).abs() < 0.1);
// 2 contracts: base=5.00, slippage=2.00, total=7.00
let cost_2 = costs.compute_cost(2.0, 0.0, 2.0);
assert!((cost_2 - 7.0).abs() < 0.2);
// 3 contracts: base=7.50, slippage=6.75, total=14.25
let cost_3 = costs.compute_cost(3.0, 0.0, 3.0);
assert!((cost_3 - 14.25).abs() < 0.5);
}
#[test]
fn test_compute_cost_quadratic_grows_faster_than_linear() {
let linear = ContinuousTransactionCosts::new(
5.0,
SlippageModel::Linear { slope: 0.5 },
OrderType::LimitMaker,
5000.0,
);
let quadratic = ContinuousTransactionCosts::new(
5.0,
SlippageModel::Quadratic { coefficient: 0.5 },
OrderType::LimitMaker,
5000.0,
);
// At 1 contract, they're equal: both have slippage of 0.5 bps
let linear_1 = linear.compute_cost(1.0, 0.0, 1.0);
let quadratic_1 = quadratic.compute_cost(1.0, 0.0, 1.0);
assert!((linear_1 - quadratic_1).abs() < 0.01);
// At large sizes, quadratic > linear
// Linear: slope × 5 = 2.5 bps slippage
// Quadratic: coeff × 25 = 12.5 bps slippage
let linear_5 = linear.compute_cost(5.0, 0.0, 5.0);
let quadratic_5 = quadratic.compute_cost(5.0, 0.0, 5.0);
assert!(quadratic_5 > linear_5);
}
#[test]
fn test_compute_cost_zero_position_change() {
let costs = default_hft_cost_model();
let cost = costs.compute_cost(0.0, 0.0, 0.0);
assert_eq!(cost, 0.0);
}
#[test]
fn test_compute_cost_tiny_position_change() {
let costs = default_hft_cost_model();
// Below threshold (1e-6)
let cost = costs.compute_cost(1e-8, 0.0, 1e-8);
assert_eq!(cost, 0.0);
}
#[test]
fn test_compute_cost_negative_position_change() {
let costs = default_hft_cost_model();
// Should use absolute value
let cost_pos = costs.compute_cost(1.0, 0.0, 1.0);
let cost_neg = costs.compute_cost(-1.0, 0.0, -1.0);
assert!((cost_pos - cost_neg).abs() < 0.01);
}
#[test]
fn test_compute_cost_different_order_types() {
let market = ContinuousTransactionCosts::new(
15.0,
SlippageModel::None,
OrderType::Market,
5000.0,
);
let limit = ContinuousTransactionCosts::new(
5.0,
SlippageModel::None,
OrderType::LimitMaker,
5000.0,
);
let ioc = ContinuousTransactionCosts::new(
10.0,
SlippageModel::None,
OrderType::IoC,
5000.0,
);
// 1 contract @ $5000
let market_cost = market.compute_cost(1.0, 0.0, 1.0);
let limit_cost = limit.compute_cost(1.0, 0.0, 1.0);
let ioc_cost = ioc.compute_cost(1.0, 0.0, 1.0);
// Market (15 bps): $7.50
assert!((market_cost - 7.5).abs() < 0.1);
// LimitMaker (5 bps): $2.50
assert!((limit_cost - 2.5).abs() < 0.1);
// IoC (10 bps): $5.00
assert!((ioc_cost - 5.0).abs() < 0.1);
// Ordering
assert!(market_cost > ioc_cost);
assert!(ioc_cost > limit_cost);
}
#[test]
fn test_compute_cost_gradient_linear_slippage() {
let costs = ContinuousTransactionCosts::new(
5.0,
SlippageModel::Linear { slope: 0.1 },
OrderType::LimitMaker,
5000.0,
);
// Gradient = 5000 × (5.0 + 0.1) / 10000 = 2.55
let grad = costs.compute_cost_gradient(1.0);
assert!((grad - 2.55).abs() < 0.01);
// Linear: gradient independent of position
let grad_2 = costs.compute_cost_gradient(2.0);
assert!((grad_2 - 2.55).abs() < 0.01);
}
#[test]
fn test_compute_cost_gradient_quadratic_slippage() {
let costs = ContinuousTransactionCosts::new(
5.0,
SlippageModel::Quadratic { coefficient: 0.5 },
OrderType::LimitMaker,
5000.0,
);
// At pos=1.0: gradient = 5000 × (5.0 + 2×0.5×1.0) / 10000 = 3.0
let grad_1 = costs.compute_cost_gradient(1.0);
assert!((grad_1 - 3.0).abs() < 0.01);
// At pos=2.0: gradient = 5000 × (5.0 + 2×0.5×2.0) / 10000 = 3.5
let grad_2 = costs.compute_cost_gradient(2.0);
assert!((grad_2 - 3.5).abs() < 0.01);
// At pos=5.0: gradient = 5000 × (5.0 + 5.0) / 10000 = 5.0
let grad_5 = costs.compute_cost_gradient(5.0);
assert!((grad_5 - 5.0).abs() < 0.01);
}
#[test]
fn test_compute_cost_gradient_no_slippage() {
let costs = ContinuousTransactionCosts::new(
5.0,
SlippageModel::None,
OrderType::LimitMaker,
5000.0,
);
// Gradient = 5000 × 5.0 / 10000 = 2.5
let grad = costs.compute_cost_gradient(1.0);
assert!((grad - 2.5).abs() < 0.01);
// No slippage: constant gradient
let grad_5 = costs.compute_cost_gradient(5.0);
assert!((grad_5 - 2.5).abs() < 0.01);
}
#[test]
fn test_compute_cost_gradient_different_contract_values() {
let cheap = ContinuousTransactionCosts::new(
5.0,
SlippageModel::None,
OrderType::LimitMaker,
1000.0,
);
let expensive = ContinuousTransactionCosts::new(
5.0,
SlippageModel::None,
OrderType::LimitMaker,
10000.0,
);
let grad_cheap = cheap.compute_cost_gradient(1.0);
let grad_expensive = expensive.compute_cost_gradient(1.0);
// 10x contract value → 10x gradient
assert!((grad_expensive / grad_cheap - 10.0).abs() < 0.1);
}
#[test]
fn test_total_cost_bps_no_slippage() {
let costs = ContinuousTransactionCosts::new(
5.0,
SlippageModel::None,
OrderType::LimitMaker,
5000.0,
);
assert_eq!(costs.total_cost_bps(1.0), 5.0);
assert_eq!(costs.total_cost_bps(10.0), 5.0);
}
#[test]
fn test_total_cost_bps_linear_slippage() {
let costs = ContinuousTransactionCosts::new(
5.0,
SlippageModel::Linear { slope: 0.1 },
OrderType::LimitMaker,
5000.0,
);
// 1 contract: 5.0 + 0.1 = 5.1 bps
assert!((costs.total_cost_bps(1.0) - 5.1).abs() < 0.01);
// 2 contracts: 5.0 + 0.2 = 5.2 bps
assert!((costs.total_cost_bps(2.0) - 5.2).abs() < 0.01);
}
#[test]
fn test_total_cost_bps_quadratic_slippage() {
let costs = ContinuousTransactionCosts::new(
5.0,
SlippageModel::Quadratic { coefficient: 0.5 },
OrderType::LimitMaker,
5000.0,
);
// 1 contract: 5.0 + 0.5×1 = 5.5 bps
assert!((costs.total_cost_bps(1.0) - 5.5).abs() < 0.01);
// 2 contracts: 5.0 + 0.5×4 = 7.0 bps
assert!((costs.total_cost_bps(2.0) - 7.0).abs() < 0.01);
// 3 contracts: 5.0 + 0.5×9 = 9.5 bps
assert!((costs.total_cost_bps(3.0) - 9.5).abs() < 0.01);
}
#[test]
fn test_default_hft_cost_model_parameters() {
let costs = default_hft_cost_model();
assert_eq!(costs.base_cost_bps, 5.0);
assert_eq!(costs.order_type, OrderType::LimitMaker);
assert_eq!(costs.contract_value, 5000.0);
matches!(costs.slippage_model, SlippageModel::Linear { slope: 0.1 });
}
#[test]
fn test_default_hft_cost_model_reasonable_costs() {
let costs = default_hft_cost_model();
// 1 contract should cost around $3
let cost_1 = costs.compute_cost(1.0, 0.0, 1.0);
assert!(cost_1 > 2.5 && cost_1 < 3.5);
// 5 contracts should cost around $15
let cost_5 = costs.compute_cost(5.0, 0.0, 5.0);
assert!(cost_5 > 13.0 && cost_5 < 17.0);
}
#[test]
fn test_conservative_cost_model_parameters() {
let costs = conservative_cost_model();
assert_eq!(costs.base_cost_bps, 15.0);
assert_eq!(costs.order_type, OrderType::Market);
assert_eq!(costs.contract_value, 5000.0);
matches!(costs.slippage_model, SlippageModel::Quadratic { .. });
}
#[test]
fn test_conservative_cost_model_higher_than_default() {
let default_costs = default_hft_cost_model();
let conservative_costs = conservative_cost_model();
let default_cost = default_costs.compute_cost(1.0, 0.0, 1.0);
let conservative_cost = conservative_costs.compute_cost(1.0, 0.0, 1.0);
assert!(conservative_cost > default_cost);
}
#[test]
fn test_zero_cost_model_parameters() {
let costs = zero_cost_model();
assert_eq!(costs.base_cost_bps, 0.0);
matches!(costs.slippage_model, SlippageModel::None);
}
#[test]
fn test_zero_cost_model_always_zero() {
let costs = zero_cost_model();
assert_eq!(costs.compute_cost(0.0, 0.0, 0.0), 0.0);
assert_eq!(costs.compute_cost(1.0, 0.0, 1.0), 0.0);
assert_eq!(costs.compute_cost(100.0, 0.0, 100.0), 0.0);
}
#[test]
fn test_cost_scales_linearly_with_contract_value() {
let base = ContinuousTransactionCosts::new(
5.0,
SlippageModel::None,
OrderType::LimitMaker,
5000.0,
);
let double = ContinuousTransactionCosts::new(
5.0,
SlippageModel::None,
OrderType::LimitMaker,
10000.0,
);
let cost_base = base.compute_cost(1.0, 0.0, 1.0);
let cost_double = double.compute_cost(1.0, 0.0, 1.0);
// 2x contract value → 2x cost
assert!((cost_double / cost_base - 2.0).abs() < 0.01);
}
#[test]
fn test_fractional_position_changes() {
let costs = default_hft_cost_model();
// 0.1 contracts
let cost_0_1 = costs.compute_cost(0.1, 0.0, 0.1);
assert!(cost_0_1 > 0.0 && cost_0_1 < 1.0);
// 0.5 contracts
let cost_0_5 = costs.compute_cost(0.5, 0.0, 0.5);
assert!(cost_0_5 > cost_0_1);
// 0.9 contracts
let cost_0_9 = costs.compute_cost(0.9, 0.0, 0.9);
assert!(cost_0_9 > cost_0_5);
}
#[test]
fn test_large_position_changes() {
let costs = ContinuousTransactionCosts::new(
5.0,
SlippageModel::Quadratic { coefficient: 0.5 },
OrderType::LimitMaker,
5000.0,
);
// 10 contracts
let cost_10 = costs.compute_cost(10.0, 0.0, 10.0);
// 100 contracts
let cost_100 = costs.compute_cost(100.0, 0.0, 100.0);
// Quadratic slippage: 100 contracts >> 10x cost of 10 contracts
assert!(cost_100 > cost_10 * 50.0);
}
#[test]
fn test_cost_symmetry_buy_vs_sell() {
let costs = default_hft_cost_model();
// Buy: 0 → 5
let buy_cost = costs.compute_cost(5.0, 0.0, 5.0);
// Sell: 5 → 0
let sell_cost = costs.compute_cost(-5.0, 5.0, 0.0);
// Should be identical (abs value used)
assert!((buy_cost - sell_cost).abs() < 0.01);
}
#[test]
fn test_new_constructor_custom_values() {
let costs = ContinuousTransactionCosts::new(
10.0,
SlippageModel::Linear { slope: 0.5 },
OrderType::IoC,
10000.0,
);
assert_eq!(costs.base_cost_bps, 10.0);
assert_eq!(costs.order_type, OrderType::IoC);
assert_eq!(costs.contract_value, 10000.0);
}
#[test]
fn test_extreme_slippage_coefficient() {
let high_slippage = ContinuousTransactionCosts::new(
5.0,
SlippageModel::Quadratic { coefficient: 5.0 }, // Very high
OrderType::LimitMaker,
5000.0,
);
// 1 contract: base=2.50, slippage=2.50, total=5.00
let cost_1 = high_slippage.compute_cost(1.0, 0.0, 1.0);
assert!((cost_1 - 5.0).abs() < 0.1);
// 2 contracts: base=5.00, slippage=20.00, total=25.00
let cost_2 = high_slippage.compute_cost(2.0, 0.0, 2.0);
assert!((cost_2 - 25.0).abs() < 0.5);
}
#[test]
fn test_gradient_consistency_with_cost() {
let costs = ContinuousTransactionCosts::new(
5.0,
SlippageModel::Linear { slope: 0.1 },
OrderType::LimitMaker,
5000.0,
);
// Numerical gradient check (finite difference)
let pos = 1.0;
let delta = 0.0001;
let cost_plus = costs.compute_cost(pos + delta, 0.0, pos + delta);
let cost_minus = costs.compute_cost(pos - delta, 0.0, pos - delta);
let numerical_grad = (cost_plus - cost_minus) / (2.0 * delta);
let analytical_grad = costs.compute_cost_gradient(pos);
// Should be very close
assert!((analytical_grad - numerical_grad).abs() < 0.1);
}
#[test]
fn test_cost_components_breakdown() {
let costs = ContinuousTransactionCosts::new(
5.0,
SlippageModel::Linear { slope: 0.2 },
OrderType::LimitMaker,
5000.0,
);
// 1 contract @ $5000
let total_cost = costs.compute_cost(1.0, 0.0, 1.0);
// Base cost: $5000 × 0.0005 = $2.50
let expected_base = 2.5;
// Slippage: $5000 × (0.2 / 10000) = $5000 × 0.00002 = $0.10
let expected_slippage = 0.10;
let expected_total = expected_base + expected_slippage;
assert!((total_cost - expected_total).abs() < 0.01);
}