Bug #8 (CRITICAL): Fixed action selection frequency catastrophe - Root cause: execute_action called during training (522,713 orders/epoch) - Fix: Removed execute_action from experience collection loop (line 928-936) - Impact: 522,713 → 0 orders/epoch (100% reduction) - Transaction costs: $338K → $0 (eliminated) - Test suite: ml/tests/action_selection_frequency_test.rs (3/3 passing) P2-A: Configurable Initial Capital - CLI argument: --initial-capital (default: $100K, min: $1K) - Files modified: trainers/dqn.rs, train_dqn.rs, hyperopt adapter - Test suite: ml/tests/configurable_capital_test.rs (8/8 passing) - Supports: Small accounts ($10K), Standard ($100K), Institutional ($500K+) P2-B: Cash Reserve Requirement - CLI argument: --cash-reserve-percent (default: 0%, range: 0-100%) - Reserve enforcement: BUY trades only (SELL always allowed) - Dynamic reserve adjusts with portfolio value - Files modified: portfolio_tracker.rs (70 lines), trainers/dqn.rs, train_dqn.rs - Test suite: ml/tests/cash_reserve_requirement_test.rs (10/10 passing) Test Status: 21/21 core tests passing (P2-C deferred due to API mismatch) Wave 16S-V11 Agents: - Agent #1: Bug #8 investigation (transaction cost analysis) - Agent #2: P2-A implementation (configurable capital) - Agent #3: P2-B implementation + test fix (cash reserve) - Agent #4: Integration validation (certification report)
596 lines
18 KiB
Rust
596 lines
18 KiB
Rust
//! Transaction Cost Calculation Test Suite
|
||
//!
|
||
//! Wave 16S-V11 Bug #8: Transaction Cost Catastrophe
|
||
//!
|
||
//! **Problem**: Net transaction costs = $338,375 (338% of $100K capital!)
|
||
//! - 522,713 orders in 1 epoch (~31 orders per training step)
|
||
//! - Total fees: $501,795, Total rebates: $163,420
|
||
//! - Portfolio dropped to $28,573 (-71.43%)
|
||
//!
|
||
//! **Root Cause**: High-frequency flipping causing excessive transaction costs
|
||
//! - Not a fee calculation bug (formula is correct)
|
||
//! - Issue is ACTION FREQUENCY and position reversals
|
||
//!
|
||
//! **This Test Suite Validates**:
|
||
//! 1. Fee calculation correctness (per-order-value, not per-contract)
|
||
//! 2. Order type fee rates (Market 0.15%, IoC 0.10%, LimitMaker 0.05%)
|
||
//! 3. Transaction cost accumulation over multiple trades
|
||
//! 4. Position reversal handling (Long→Short should charge fees correctly)
|
||
//! 5. Fractional contract handling
|
||
//! 6. Integration with PortfolioTracker
|
||
|
||
use ml::dqn::action_space::{FactoredAction, ExposureLevel, OrderType, Urgency};
|
||
use ml::dqn::portfolio_tracker::PortfolioTracker;
|
||
use ml::dqn::TradingModel;
|
||
|
||
// ============================================================================
|
||
// Category 1: Fee Rate Validation
|
||
// ============================================================================
|
||
|
||
#[test]
|
||
fn test_market_order_fee_rate() {
|
||
// Market orders should charge 0.15% (0.0015)
|
||
let action = FactoredAction::new(
|
||
ExposureLevel::Long100,
|
||
OrderType::Market,
|
||
Urgency::Normal,
|
||
);
|
||
assert_eq!(action.transaction_cost(), 0.0015);
|
||
}
|
||
|
||
#[test]
|
||
fn test_ioc_order_fee_rate() {
|
||
// IoC (Immediate or Cancel) orders should charge 0.10% (0.0010)
|
||
let action = FactoredAction::new(
|
||
ExposureLevel::Long100,
|
||
OrderType::IoC,
|
||
Urgency::Aggressive,
|
||
);
|
||
assert_eq!(action.transaction_cost(), 0.0010);
|
||
}
|
||
|
||
#[test]
|
||
fn test_limit_maker_fee_rate() {
|
||
// LimitMaker orders should charge 0.05% (0.0005) - rebate in reality
|
||
let action = FactoredAction::new(
|
||
ExposureLevel::Long100,
|
||
OrderType::LimitMaker,
|
||
Urgency::Patient,
|
||
);
|
||
assert_eq!(action.transaction_cost(), 0.0005);
|
||
}
|
||
|
||
// ============================================================================
|
||
// Category 2: Per-Order-Value Fee Calculation (NOT per-contract)
|
||
// ============================================================================
|
||
|
||
#[test]
|
||
fn test_fee_calculated_on_order_value_not_contracts() {
|
||
// Wave 16S-V11 Bug #8 FIX: Fees MUST be calculated as:
|
||
// fee = (position_delta × price × multiplier) × fee_rate
|
||
// NOT: fee = position_delta × fee_rate × price
|
||
|
||
let mut tracker = PortfolioTracker::new(
|
||
100_000.0,
|
||
0.0001,
|
||
"ES",
|
||
TradingModel::Futures(10.0), // 10% margin, effective_multiplier = 50 × 0.10 = 5.0
|
||
);
|
||
|
||
let initial_cash = tracker.cash_balance();
|
||
|
||
// Execute a Market order for 1.0 contract at $5,600 price
|
||
let action = FactoredAction::new(
|
||
ExposureLevel::Long100,
|
||
OrderType::Market,
|
||
Urgency::Normal,
|
||
);
|
||
|
||
let price = 5_600.0;
|
||
let max_position = 1.0; // Clamped to 1.0 contract
|
||
|
||
tracker.execute_action(action, price, max_position);
|
||
|
||
// Expected fee calculation:
|
||
// trade_value = 1.0 (delta) × $5,600 (price) × 5.0 (effective_multiplier) = $28,000
|
||
// transaction_cost = $28,000 × 0.0015 (Market fee) = $42.00
|
||
//
|
||
// Cash update:
|
||
// cash_after = cash_before - (position_delta × price × multiplier) - transaction_cost
|
||
// cash_after = $100,000 - (1.0 × $5,600 × 5.0) - $42.00
|
||
// cash_after = $100,000 - $28,000 - $42.00 = $71,958.00
|
||
|
||
let expected_cash = initial_cash - (1.0 * price * 5.0) - 42.0;
|
||
let actual_cash = tracker.cash_balance();
|
||
|
||
assert!(
|
||
(actual_cash - expected_cash).abs() < 0.01,
|
||
"Fee calculation error! Expected cash: ${:.2}, Actual: ${:.2}. \
|
||
Fee should be calculated on order value ($28,000 × 0.0015 = $42), \
|
||
not per-contract ($1 × 0.0015 × $5,600 = $8.40 would be wrong).",
|
||
expected_cash,
|
||
actual_cash
|
||
);
|
||
|
||
// Verify transaction costs tracked correctly
|
||
let total_fees = tracker.transaction_costs();
|
||
assert!(
|
||
(total_fees - 42.0).abs() < 0.01,
|
||
"Transaction cost tracking error! Expected: $42.00, Actual: ${:.2}",
|
||
total_fees
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn test_fractional_contract_fee_calculation() {
|
||
// Test that fees work correctly for fractional contracts (e.g., 0.5 contracts)
|
||
let mut tracker = PortfolioTracker::new(
|
||
100_000.0,
|
||
0.0001,
|
||
"ES",
|
||
TradingModel::Futures(10.0),
|
||
);
|
||
|
||
let action = FactoredAction::new(
|
||
ExposureLevel::Long50, // 50% exposure = 0.5 contracts
|
||
OrderType::Market,
|
||
Urgency::Normal,
|
||
);
|
||
|
||
let price = 5_600.0;
|
||
let max_position = 1.0;
|
||
|
||
tracker.execute_action(action, price, max_position);
|
||
|
||
// Expected fee calculation for 0.5 contracts:
|
||
// trade_value = 0.5 × $5,600 × 5.0 = $14,000
|
||
// transaction_cost = $14,000 × 0.0015 = $21.00
|
||
|
||
let expected_fee = 21.0;
|
||
let actual_fee = tracker.transaction_costs();
|
||
|
||
assert!(
|
||
(actual_fee - expected_fee).abs() < 0.01,
|
||
"Fractional contract fee error! Expected: ${:.2}, Actual: ${:.2}",
|
||
expected_fee,
|
||
actual_fee
|
||
);
|
||
}
|
||
|
||
// ============================================================================
|
||
// Category 3: Fee Accumulation Over Multiple Trades
|
||
// ============================================================================
|
||
|
||
#[test]
|
||
fn test_fee_accumulation_across_multiple_trades() {
|
||
// Validate that transaction costs accumulate correctly
|
||
let mut tracker = PortfolioTracker::new(
|
||
100_000.0,
|
||
0.0001,
|
||
"ES",
|
||
TradingModel::Futures(10.0),
|
||
);
|
||
|
||
let price = 5_600.0;
|
||
let max_position = 1.0;
|
||
|
||
// Trade 1: Buy 1 contract (Market order, 0.15% fee)
|
||
let action1 = FactoredAction::new(
|
||
ExposureLevel::Long100,
|
||
OrderType::Market,
|
||
Urgency::Normal,
|
||
);
|
||
tracker.execute_action(action1, price, max_position);
|
||
|
||
// Expected fee: $28,000 × 0.0015 = $42.00
|
||
assert!(
|
||
(tracker.transaction_costs() - 42.0).abs() < 0.01,
|
||
"Trade 1 fee incorrect: ${:.2}",
|
||
tracker.transaction_costs()
|
||
);
|
||
|
||
// Trade 2: Close position (Sell 1 contract, Market order, 0.15% fee)
|
||
let action2 = FactoredAction::new(
|
||
ExposureLevel::Flat,
|
||
OrderType::Market,
|
||
Urgency::Normal,
|
||
);
|
||
tracker.execute_action(action2, price, max_position);
|
||
|
||
// Expected cumulative fee: $42.00 (Trade 1) + $42.00 (Trade 2) = $84.00
|
||
let expected_total = 84.0;
|
||
let actual_total = tracker.transaction_costs();
|
||
|
||
assert!(
|
||
(actual_total - expected_total).abs() < 0.01,
|
||
"Cumulative fee error! Expected: ${:.2}, Actual: ${:.2}",
|
||
expected_total,
|
||
actual_total
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn test_fee_accumulation_with_different_order_types() {
|
||
// Mix Market, IoC, and LimitMaker orders
|
||
let mut tracker = PortfolioTracker::new(
|
||
100_000.0,
|
||
0.0001,
|
||
"ES",
|
||
TradingModel::Futures(10.0),
|
||
);
|
||
|
||
let price = 5_600.0;
|
||
let max_position = 1.0;
|
||
|
||
// Trade 1: Market order (0.15%)
|
||
let action1 = FactoredAction::new(
|
||
ExposureLevel::Long100,
|
||
OrderType::Market,
|
||
Urgency::Normal,
|
||
);
|
||
tracker.execute_action(action1, price, max_position);
|
||
let fee1 = 28_000.0 * 0.0015; // $42.00
|
||
|
||
// Trade 2: IoC order (0.10%)
|
||
let action2 = FactoredAction::new(
|
||
ExposureLevel::Flat,
|
||
OrderType::IoC,
|
||
Urgency::Aggressive,
|
||
);
|
||
tracker.execute_action(action2, price, max_position);
|
||
let fee2 = 28_000.0 * 0.0010; // $28.00
|
||
|
||
// Trade 3: LimitMaker order (0.05%)
|
||
let action3 = FactoredAction::new(
|
||
ExposureLevel::Long50,
|
||
OrderType::LimitMaker,
|
||
Urgency::Patient,
|
||
);
|
||
tracker.execute_action(action3, price, max_position);
|
||
let fee3 = 14_000.0 * 0.0005; // $7.00 (0.5 contracts)
|
||
|
||
let expected_total = fee1 + fee2 + fee3; // $77.00
|
||
let actual_total = tracker.transaction_costs();
|
||
|
||
assert!(
|
||
(actual_total - expected_total).abs() < 0.01,
|
||
"Mixed order type fee error! Expected: ${:.2}, Actual: ${:.2}",
|
||
expected_total,
|
||
actual_total
|
||
);
|
||
}
|
||
|
||
// ============================================================================
|
||
// Category 4: Position Reversal Handling
|
||
// ============================================================================
|
||
|
||
#[test]
|
||
fn test_position_reversal_fees_long_to_short() {
|
||
// Long→Short reversal should charge fees for BOTH:
|
||
// 1. Closing long position (1.0 contract)
|
||
// 2. Opening short position (1.0 contract)
|
||
// Total delta: 2.0 contracts
|
||
|
||
let mut tracker = PortfolioTracker::new(
|
||
100_000.0,
|
||
0.0001,
|
||
"ES",
|
||
TradingModel::Futures(10.0),
|
||
);
|
||
|
||
let price = 5_600.0;
|
||
let max_position = 1.0;
|
||
|
||
// Step 1: Establish long position
|
||
let action1 = FactoredAction::new(
|
||
ExposureLevel::Long100,
|
||
OrderType::Market,
|
||
Urgency::Normal,
|
||
);
|
||
tracker.execute_action(action1, price, max_position);
|
||
|
||
let fee_after_long = tracker.transaction_costs();
|
||
assert!((fee_after_long - 42.0).abs() < 0.01);
|
||
|
||
// Step 2: Reverse to short position (Long100 → Short100)
|
||
let action2 = FactoredAction::new(
|
||
ExposureLevel::Short100,
|
||
OrderType::Market,
|
||
Urgency::Normal,
|
||
);
|
||
tracker.execute_action(action2, price, max_position);
|
||
|
||
// Expected reversal fee:
|
||
// position_delta = -1.0 (Short100) - 1.0 (Long100) = -2.0 contracts
|
||
// trade_value = 2.0 × $5,600 × 5.0 = $56,000
|
||
// reversal_fee = $56,000 × 0.0015 = $84.00
|
||
//
|
||
// Total fees = $42.00 (initial long) + $84.00 (reversal) = $126.00
|
||
|
||
let expected_total = 126.0;
|
||
let actual_total = tracker.transaction_costs();
|
||
|
||
assert!(
|
||
(actual_total - expected_total).abs() < 0.01,
|
||
"Reversal fee error! Expected: ${:.2}, Actual: ${:.2}. \
|
||
Reversal should charge fees for 2.0 contracts (close long + open short).",
|
||
expected_total,
|
||
actual_total
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn test_position_reversal_fees_short_to_long() {
|
||
// Short→Long reversal should also charge for full delta
|
||
let mut tracker = PortfolioTracker::new(
|
||
100_000.0,
|
||
0.0001,
|
||
"ES",
|
||
TradingModel::Futures(10.0),
|
||
);
|
||
|
||
let price = 5_600.0;
|
||
let max_position = 1.0;
|
||
|
||
// Step 1: Establish short position
|
||
let action1 = FactoredAction::new(
|
||
ExposureLevel::Short100,
|
||
OrderType::Market,
|
||
Urgency::Normal,
|
||
);
|
||
tracker.execute_action(action1, price, max_position);
|
||
|
||
let fee_after_short = tracker.transaction_costs();
|
||
assert!((fee_after_short - 42.0).abs() < 0.01);
|
||
|
||
// Step 2: Reverse to long position (Short100 → Long100)
|
||
let action2 = FactoredAction::new(
|
||
ExposureLevel::Long100,
|
||
OrderType::Market,
|
||
Urgency::Normal,
|
||
);
|
||
tracker.execute_action(action2, price, max_position);
|
||
|
||
// Expected reversal fee: $84.00 (same as long→short)
|
||
// Total fees = $42.00 (initial short) + $84.00 (reversal) = $126.00
|
||
|
||
let expected_total = 126.0;
|
||
let actual_total = tracker.transaction_costs();
|
||
|
||
assert!(
|
||
(actual_total - expected_total).abs() < 0.01,
|
||
"Short→Long reversal fee error! Expected: ${:.2}, Actual: ${:.2}",
|
||
expected_total,
|
||
actual_total
|
||
);
|
||
}
|
||
|
||
// ============================================================================
|
||
// Category 5: High-Frequency Trading Scenario (Bug #8 Root Cause)
|
||
// ============================================================================
|
||
|
||
#[test]
|
||
fn test_high_frequency_flipping_transaction_costs() {
|
||
// Simulate high-frequency flipping (31 trades per training step)
|
||
// This is the root cause of Bug #8: excessive trading frequency
|
||
let mut tracker = PortfolioTracker::new(
|
||
100_000.0,
|
||
0.0001,
|
||
"ES",
|
||
TradingModel::Futures(10.0),
|
||
);
|
||
|
||
let price = 5_600.0;
|
||
let max_position = 1.0;
|
||
|
||
// Simulate 100 trades (flipping Long↔Short)
|
||
let num_trades = 100;
|
||
for i in 0..num_trades {
|
||
let action = if i % 2 == 0 {
|
||
FactoredAction::new(ExposureLevel::Long100, OrderType::Market, Urgency::Normal)
|
||
} else {
|
||
FactoredAction::new(ExposureLevel::Short100, OrderType::Market, Urgency::Normal)
|
||
};
|
||
tracker.execute_action(action, price, max_position);
|
||
}
|
||
|
||
// Expected total fees:
|
||
// - First trade: 1.0 contract × $28,000 × 0.0015 = $42.00
|
||
// - Each subsequent trade (reversal): 2.0 contracts × $28,000 × 0.0015 = $84.00
|
||
// - Total: $42 + (99 × $84) = $42 + $8,316 = $8,358.00
|
||
|
||
let expected_total = 42.0 + (99.0 * 84.0);
|
||
let actual_total = tracker.transaction_costs();
|
||
|
||
assert!(
|
||
(actual_total - expected_total).abs() < 1.0, // Allow $1 tolerance
|
||
"High-frequency flipping fee error! Expected: ${:.2}, Actual: ${:.2}",
|
||
expected_total,
|
||
actual_total
|
||
);
|
||
|
||
// Verify portfolio is severely depleted by transaction costs
|
||
let remaining_cash = tracker.cash_balance();
|
||
println!(
|
||
"After {} trades: Cash = ${:.2}, Transaction costs = ${:.2}",
|
||
num_trades, remaining_cash, actual_total
|
||
);
|
||
|
||
// Cash should be significantly reduced
|
||
assert!(
|
||
remaining_cash < 92_000.0,
|
||
"Transaction costs should drain cash significantly. Remaining: ${:.2}",
|
||
remaining_cash
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn test_transaction_costs_vs_capital_ratio() {
|
||
// Validate that 522,713 orders with 17× overcharge would drain capital
|
||
// This test calculates what the cost SHOULD be vs what was observed
|
||
|
||
// Observed in Bug #8:
|
||
let observed_net_costs = 338_375.0; // $338K
|
||
let _observed_total_fees = 501_795.0;
|
||
let _observed_total_rebates = 163_420.0;
|
||
let initial_capital = 100_000.0;
|
||
|
||
// Calculate ratio
|
||
let cost_to_capital_ratio = observed_net_costs / initial_capital;
|
||
assert!(
|
||
cost_to_capital_ratio > 3.0,
|
||
"Net costs (${:.2}) are {:.1}% of capital - CATASTROPHIC!",
|
||
observed_net_costs,
|
||
cost_to_capital_ratio * 100.0
|
||
);
|
||
|
||
// Expected Market order fees (assuming 67,908 orders at $100K avg):
|
||
let expected_market_fees = 67_908.0 * 100_000.0 * 0.0015;
|
||
println!(
|
||
"Expected Market fees: ${:.2}, Observed: ${:.2}, Ratio: {:.1}×",
|
||
expected_market_fees,
|
||
172_850.69,
|
||
172_850.69 / expected_market_fees
|
||
);
|
||
|
||
// The 17× ratio suggests either:
|
||
// 1. Orders are being executed 17× more frequently than expected
|
||
// 2. Each order is being charged 17× the correct fee
|
||
// 3. max_position is 17× larger than it should be
|
||
//
|
||
// Based on portfolio_tracker.rs analysis, #1 is most likely:
|
||
// High-frequency flipping (31 orders/step) × position reversals (2× cost)
|
||
}
|
||
|
||
// ============================================================================
|
||
// Category 6: Contract Multiplier Impact on Fees
|
||
// ============================================================================
|
||
|
||
#[test]
|
||
fn test_fee_calculation_with_different_multipliers() {
|
||
// ES (multiplier=50.0) vs NQ (multiplier=20.0) should have different fees
|
||
// even with same price and position size
|
||
|
||
// ES futures (multiplier=50.0, effective=5.0 with 10% margin)
|
||
let mut tracker_es = PortfolioTracker::new(
|
||
100_000.0,
|
||
0.0001,
|
||
"ES",
|
||
TradingModel::Futures(10.0),
|
||
);
|
||
|
||
let price = 5_600.0;
|
||
let max_position = 1.0;
|
||
let action = FactoredAction::new(
|
||
ExposureLevel::Long100,
|
||
OrderType::Market,
|
||
Urgency::Normal,
|
||
);
|
||
|
||
tracker_es.execute_action(action, price, max_position);
|
||
|
||
// ES fee: 1.0 × $5,600 × 5.0 × 0.0015 = $42.00
|
||
let es_fee = tracker_es.transaction_costs();
|
||
assert!((es_fee - 42.0).abs() < 0.01);
|
||
|
||
// NQ futures (multiplier=20.0, effective=2.0 with 10% margin)
|
||
let mut tracker_nq = PortfolioTracker::new(
|
||
100_000.0,
|
||
0.0001,
|
||
"NQ",
|
||
TradingModel::Futures(10.0),
|
||
);
|
||
|
||
tracker_nq.execute_action(action, price, max_position);
|
||
|
||
// NQ fee: 1.0 × $5,600 × 2.0 × 0.0015 = $16.80
|
||
let nq_fee = tracker_nq.transaction_costs();
|
||
assert!(
|
||
(nq_fee - 16.8).abs() < 0.01,
|
||
"NQ fee calculation error! Expected: $16.80, Actual: ${:.2}",
|
||
nq_fee
|
||
);
|
||
|
||
// Verify ES fees are 2.5× higher than NQ (multiplier ratio: 50/20 = 2.5)
|
||
let ratio = es_fee / nq_fee;
|
||
assert!(
|
||
(ratio - 2.5).abs() < 0.01,
|
||
"Multiplier ratio error! ES/NQ fee ratio should be 2.5×, got {:.2}×",
|
||
ratio
|
||
);
|
||
}
|
||
|
||
// ============================================================================
|
||
// Category 7: Edge Cases
|
||
// ============================================================================
|
||
|
||
#[test]
|
||
fn test_zero_position_delta_zero_fees() {
|
||
// Executing the same action twice should result in zero fees on second call
|
||
let mut tracker = PortfolioTracker::new(
|
||
100_000.0,
|
||
0.0001,
|
||
"ES",
|
||
TradingModel::Futures(10.0),
|
||
);
|
||
|
||
let price = 5_600.0;
|
||
let max_position = 1.0;
|
||
let action = FactoredAction::new(
|
||
ExposureLevel::Long100,
|
||
OrderType::Market,
|
||
Urgency::Normal,
|
||
);
|
||
|
||
// First execution
|
||
tracker.execute_action(action, price, max_position);
|
||
let fee_after_first = tracker.transaction_costs();
|
||
assert!((fee_after_first - 42.0).abs() < 0.01);
|
||
|
||
// Second execution (same action, no position change)
|
||
tracker.execute_action(action, price, max_position);
|
||
let fee_after_second = tracker.transaction_costs();
|
||
|
||
// Fee should be unchanged (no new position delta)
|
||
assert!(
|
||
(fee_after_second - fee_after_first).abs() < 0.01,
|
||
"Duplicate action should not charge fees! First: ${:.2}, Second: ${:.2}",
|
||
fee_after_first,
|
||
fee_after_second
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn test_hold_action_zero_fees() {
|
||
// HOLD action should never charge fees
|
||
let mut tracker = PortfolioTracker::new(
|
||
100_000.0,
|
||
0.0001,
|
||
"ES",
|
||
TradingModel::Futures(10.0),
|
||
);
|
||
|
||
let price = 5_600.0;
|
||
let max_position = 1.0;
|
||
|
||
// Execute multiple HOLD actions
|
||
for _ in 0..10 {
|
||
// Note: FactoredAction doesn't have a HOLD exposure level
|
||
// Flat is the closest equivalent (target exposure = 0.0)
|
||
let action = FactoredAction::new(
|
||
ExposureLevel::Flat,
|
||
OrderType::Market,
|
||
Urgency::Normal,
|
||
);
|
||
tracker.execute_action(action, price, max_position);
|
||
}
|
||
|
||
// All HOLD actions should result in zero fees
|
||
let total_fees = tracker.transaction_costs();
|
||
assert!(
|
||
total_fees.abs() < 0.01,
|
||
"HOLD actions should never charge fees! Total: ${:.2}",
|
||
total_fees
|
||
);
|
||
}
|