112 lines
3.7 KiB
Python
112 lines
3.7 KiB
Python
|
|
"""Tests for PositionManager."""
|
||
|
|
|
||
|
|
import pytest
|
||
|
|
from unittest.mock import Mock
|
||
|
|
from nautilus_dolphin.nautilus.position_manager import PositionManager
|
||
|
|
from nautilus_trader.model.enums import PositionSide
|
||
|
|
|
||
|
|
|
||
|
|
class TestPositionManager:
|
||
|
|
|
||
|
|
def test_on_position_opened_calculates_tp(self):
|
||
|
|
"""Test TP price calculation on position open."""
|
||
|
|
strategy = Mock()
|
||
|
|
pm = PositionManager(strategy, tp_bps=99, max_hold_bars=120)
|
||
|
|
|
||
|
|
position = Mock()
|
||
|
|
position.id = "POS-001"
|
||
|
|
position.instrument_id = "BTCUSDT.BINANCE"
|
||
|
|
position.avg_px_open = 50000.0
|
||
|
|
position.side = PositionSide.SHORT
|
||
|
|
|
||
|
|
pm.on_position_opened(position)
|
||
|
|
|
||
|
|
assert "BTCUSDT.BINANCE" in pm.positions
|
||
|
|
metadata = pm.positions["BTCUSDT.BINANCE"]
|
||
|
|
|
||
|
|
# TP should be entry * (1 - 99/10000) = 50000 * 0.9901
|
||
|
|
expected_tp = 50000.0 * 0.9901
|
||
|
|
assert abs(metadata['tp_price'] - expected_tp) < 0.01
|
||
|
|
|
||
|
|
def test_on_bar_increments_bars_held(self):
|
||
|
|
"""Test bars_held counter increments."""
|
||
|
|
strategy = Mock()
|
||
|
|
strategy.cache = Mock()
|
||
|
|
pm = PositionManager(strategy)
|
||
|
|
|
||
|
|
position = Mock()
|
||
|
|
position.id = "POS-001"
|
||
|
|
position.instrument_id = "BTCUSDT.BINANCE"
|
||
|
|
position.avg_px_open = 50000.0
|
||
|
|
position.side = PositionSide.SHORT
|
||
|
|
position.is_closed = False
|
||
|
|
|
||
|
|
pm.on_position_opened(position)
|
||
|
|
strategy.cache.position.return_value = position
|
||
|
|
|
||
|
|
bar = Mock()
|
||
|
|
bar.instrument_id = "BTCUSDT.BINANCE"
|
||
|
|
bar.close = 50000.0
|
||
|
|
|
||
|
|
pm.on_bar(bar)
|
||
|
|
assert pm.positions["BTCUSDT.BINANCE"]['bars_held'] == 1
|
||
|
|
|
||
|
|
pm.on_bar(bar)
|
||
|
|
assert pm.positions["BTCUSDT.BINANCE"]['bars_held'] == 2
|
||
|
|
|
||
|
|
def test_tp_exit_triggered(self):
|
||
|
|
"""Test TP exit when price hits target."""
|
||
|
|
strategy = Mock()
|
||
|
|
strategy.cache = Mock()
|
||
|
|
strategy.order_factory = Mock()
|
||
|
|
pm = PositionManager(strategy, tp_bps=99)
|
||
|
|
|
||
|
|
position = Mock()
|
||
|
|
position.id = "POS-001"
|
||
|
|
position.instrument_id = "BTCUSDT.BINANCE"
|
||
|
|
position.avg_px_open = 50000.0
|
||
|
|
position.side = PositionSide.SHORT
|
||
|
|
position.is_closed = False
|
||
|
|
position.quantity = 1.0
|
||
|
|
|
||
|
|
pm.on_position_opened(position)
|
||
|
|
strategy.cache.position.return_value = position
|
||
|
|
|
||
|
|
bar = Mock()
|
||
|
|
bar.instrument_id = "BTCUSDT.BINANCE"
|
||
|
|
bar.close = 49505.0 # Below TP
|
||
|
|
|
||
|
|
pm.on_bar(bar)
|
||
|
|
|
||
|
|
# Should have called order_factory.market
|
||
|
|
strategy.order_factory.market.assert_called_once()
|
||
|
|
|
||
|
|
def test_max_hold_exit_triggered(self):
|
||
|
|
"""Test max hold exit after 120 bars."""
|
||
|
|
strategy = Mock()
|
||
|
|
strategy.cache = Mock()
|
||
|
|
strategy.order_factory = Mock()
|
||
|
|
pm = PositionManager(strategy, max_hold_bars=3)
|
||
|
|
|
||
|
|
position = Mock()
|
||
|
|
position.id = "POS-001"
|
||
|
|
position.instrument_id = "BTCUSDT.BINANCE"
|
||
|
|
position.avg_px_open = 50000.0
|
||
|
|
position.side = PositionSide.SHORT
|
||
|
|
position.is_closed = False
|
||
|
|
position.quantity = 1.0
|
||
|
|
|
||
|
|
pm.on_position_opened(position)
|
||
|
|
strategy.cache.position.return_value = position
|
||
|
|
|
||
|
|
bar = Mock()
|
||
|
|
bar.instrument_id = "BTCUSDT.BINANCE"
|
||
|
|
bar.close = 50100.0 # Above TP
|
||
|
|
|
||
|
|
# Trigger max hold
|
||
|
|
for _ in range(3):
|
||
|
|
pm.on_bar(bar)
|
||
|
|
|
||
|
|
# Should have called order_factory.market
|
||
|
|
strategy.order_factory.market.assert_called_once()
|