"""Tests for SmartExecAlgorithm enhancements.""" import pytest from unittest.mock import Mock, MagicMock from nautilus_trader.model.enums import OrderSide from nautilus_trader.model.objects import Price from nautilus_dolphin.nautilus.smart_exec_algorithm import SmartExecAlgorithmForTesting class TestSmartExecAlgorithmAbortLogic: """Tests for the abort logic when price moves against position.""" def test_should_abort_entry_buy_price_rises(self): """Test BUY entry aborted when ask rises 5bps above limit.""" algo = SmartExecAlgorithmForTesting({'entry_abort_threshold_bps': 5.0}) # Create mock order (BUY at 50000) order = Mock() order.side = OrderSide.BUY # Create mock tick (ask moved up 1% - well above 5bps threshold) tick = Mock() tick.ask_price = Price(50500, 2) tick.bid_price = Price(50499, 2) metadata = {'limit_price': 50000.0} # Should abort (price moved more than 5bps against) assert algo._should_abort_entry(order, tick, metadata) is True def test_should_not_abort_entry_buy_price_stable(self): """Test BUY entry not aborted when price hasn't moved much.""" algo = SmartExecAlgorithmForTesting({'entry_abort_threshold_bps': 5.0}) order = Mock() order.side = OrderSide.BUY # Ask only 2bps higher (below 5bps threshold) tick = Mock() tick.ask_price = Price(50010, 2) tick.bid_price = Price(50009, 2) metadata = {'limit_price': 50000.0} assert algo._should_abort_entry(order, tick, metadata) is False def test_should_abort_entry_sell_price_drops(self): """Test SELL entry aborted when bid drops 5bps below limit.""" algo = SmartExecAlgorithmForTesting({'entry_abort_threshold_bps': 5.0}) order = Mock() order.side = OrderSide.SELL # Bid moved down 1% - well above 5bps threshold tick = Mock() tick.ask_price = Price(49501, 2) tick.bid_price = Price(49500, 2) metadata = {'limit_price': 50000.0} assert algo._should_abort_entry(order, tick, metadata) is True def test_abort_entry_cancels_order(self): """Test abort_entry cancels the order and updates metrics.""" algo = SmartExecAlgorithmForTesting({'entry_abort_threshold_bps': 5.0}) # Setup pending entry order = Mock() order.is_closed = False algo._pending_entries = {'order1': {'limit_price': 50000.0}} algo._abort_entry('order1', {'limit_price': 50000.0}) assert algo.cancel_order.called assert algo._metrics['entries_aborted'] == 1 assert 'order1' not in algo._pending_entries class TestSmartExecAlgorithmFeeTracking: """Tests for fee and slippage tracking.""" def test_fee_calculation_maker(self): """Test maker fee calculation (0.02%).""" algo = SmartExecAlgorithmForTesting({ 'maker_fee_rate': 0.0002, 'taker_fee_rate': 0.0005, }) # $50000 notional fee = algo._calculate_fee(50000.0, 'maker') assert fee == 10.0 # 50000 * 0.0002 def test_fee_calculation_taker(self): """Test taker fee calculation (0.05%).""" algo = SmartExecAlgorithmForTesting({ 'maker_fee_rate': 0.0002, 'taker_fee_rate': 0.0005, }) fee = algo._calculate_fee(50000.0, 'taker') assert fee == 25.0 # 50000 * 0.0005 def test_slippage_calculation(self): """Test slippage calculation in basis points.""" algo = SmartExecAlgorithmForTesting({}) # BUY: expected 50000, got 50050 (10bps worse) slippage = algo._calculate_slippage_bps(50000.0, 50050.0, OrderSide.BUY) assert slippage == 10.0 # SELL: expected 50000, got 49950 (10bps worse) slippage = algo._calculate_slippage_bps(50000.0, 49950.0, OrderSide.SELL) assert slippage == 10.0 def test_get_metrics_summary(self): """Test metrics summary generation.""" algo = SmartExecAlgorithmForTesting({}) # Add some metrics algo._metrics['entries_maker'] = 8 algo._metrics['entries_taker'] = 2 algo._metrics['total_fees'] = 100.0 algo._metrics['fill_count'] = 10 summary = algo.get_metrics_summary() assert summary['entries']['total'] == 10 assert summary['entries']['maker_pct'] == 80.0 assert summary['fees']['total'] == 100.0 def test_reset_metrics(self): """Test metrics reset functionality.""" algo = SmartExecAlgorithmForTesting({}) # Add some metrics algo._metrics['entries_maker'] = 5 algo._metrics['total_fees'] = 50.0 algo.reset_metrics() assert algo._metrics['entries_maker'] == 0 assert algo._metrics['total_fees'] == 0.0 class TestSmartExecAlgorithmIntegration: """Integration tests for fee tracking with fills.""" def test_entry_fill_tracked_correctly(self): """Test that entry fills are tracked with correct fee calculation.""" algo = SmartExecAlgorithmForTesting({ 'maker_fee_rate': 0.0002, }) order = Mock() order.side = OrderSide.BUY order.tags = {'type': 'entry', 'expected_price': 50000.0} # Fill at expected price (no slippage) algo._record_fill(order, 50000.0, 1.0, 'maker') assert algo._metrics['entries_maker'] == 1 assert algo._metrics['total_fees'] == 10.0 # 50000 * 0.0002 assert algo._metrics['fill_count'] == 1 def test_exit_fill_tracked_correctly(self): """Test that exit fills are tracked separately.""" algo = SmartExecAlgorithmForTesting({ 'maker_fee_rate': 0.0002, }) order = Mock() order.side = OrderSide.SELL order.tags = {'type': 'exit'} algo._record_fill(order, 51000.0, 1.0, 'maker') assert algo._metrics['exits_maker'] == 1 assert algo._metrics['entries_maker'] == 0 # Entry not counted