Files
DOLPHIN/nautilus_dolphin/tests/test_smart_exec_algorithm.py

180 lines
6.4 KiB
Python
Raw Normal View History

"""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