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