"""Tests for ExchangeOrderManager. This module contains tests for the ExchangeOrderManager class which provides a unified interface for order management with support for both paper trading and live exchange trading. """ import asyncio import pytest from datetime import datetime from typing import Optional from openclaw.exchange.order_manager import ( ExchangeOrderManager, CreateOrderRequest, ClosePositionRequest, OrderSummary, PositionSummary, TradingModeEnum, TradingModeStatus, ) from openclaw.exchange.models import OrderSide, OrderStatus, OrderType class TestExchangeOrderManager: """Tests for ExchangeOrderManager class.""" @pytest.fixture async def manager(self): """Fixture to create an ExchangeOrderManager instance.""" manager = ExchangeOrderManager(paper_trading=True, fee_rate=0.001) await manager.start() yield manager await manager.stop() def test_initialization_paper_mode(self): """Test ExchangeOrderManager initialization in paper mode.""" manager = ExchangeOrderManager(paper_trading=True) assert manager.paper_trading is True assert manager.is_paper_trading is True assert manager.is_live_trading is False assert manager.trading_mode == TradingModeEnum.PAPER def test_initialization_live_mode(self): """Test ExchangeOrderManager initialization in live mode.""" manager = ExchangeOrderManager(paper_trading=False) assert manager.paper_trading is False assert manager.is_live_trading is True assert manager.trading_mode == TradingModeEnum.LIVE def test_fee_rate_configuration(self): """Test custom fee rate configuration.""" manager = ExchangeOrderManager(fee_rate=0.002) assert manager.fee_rate == 0.002 @pytest.mark.asyncio async def test_create_market_buy_order(self, manager): """Test creating a market buy order.""" request = CreateOrderRequest( symbol="BTC/USDT", side=OrderSide.BUY, order_type=OrderType.MARKET, amount=0.1, ) order = await manager.create_order(request) assert order.symbol == "BTC/USDT" assert order.side == OrderSide.BUY assert order.order_type == OrderType.MARKET assert order.amount == 0.1 assert order.status in [OrderStatus.FILLED, OrderStatus.PENDING] assert order.order_id is not None @pytest.mark.asyncio async def test_create_market_sell_order(self, manager): """Test creating a market sell order.""" request = CreateOrderRequest( symbol="ETH/USDT", side=OrderSide.SELL, order_type=OrderType.MARKET, amount=1.0, ) order = await manager.create_order(request) assert order.symbol == "ETH/USDT" assert order.side == OrderSide.SELL assert order.order_type == OrderType.MARKET assert order.amount == 1.0 @pytest.mark.asyncio async def test_create_limit_order(self, manager): """Test creating a limit order.""" request = CreateOrderRequest( symbol="BTC/USDT", side=OrderSide.BUY, order_type=OrderType.LIMIT, amount=0.1, price=50000.0, ) order = await manager.create_order(request) assert order.order_type == OrderType.LIMIT assert order.price == 50000.0 assert order.status in [OrderStatus.OPEN, OrderStatus.PENDING] @pytest.mark.asyncio async def test_get_order(self, manager): """Test getting an order by ID.""" request = CreateOrderRequest( symbol="BTC/USDT", side=OrderSide.BUY, order_type=OrderType.MARKET, amount=0.1, ) created = await manager.create_order(request) retrieved = await manager.get_order(created.order_id) assert retrieved is not None assert retrieved.order_id == created.order_id assert retrieved.symbol == created.symbol @pytest.mark.asyncio async def test_get_order_not_found(self, manager): """Test getting a non-existent order.""" order = await manager.get_order("non_existent_order_id") assert order is None @pytest.mark.asyncio async def test_list_orders(self, manager): """Test listing orders.""" # Create a few orders for i in range(3): request = CreateOrderRequest( symbol=f"SYM{i}/USDT", side=OrderSide.BUY, order_type=OrderType.MARKET, amount=0.1, ) await manager.create_order(request) orders = await manager.list_orders() assert len(orders) >= 3 @pytest.mark.asyncio async def test_list_orders_with_filter(self, manager): """Test listing orders with filters.""" # Create orders with different symbols request1 = CreateOrderRequest( symbol="BTC/USDT", side=OrderSide.BUY, order_type=OrderType.MARKET, amount=0.1, ) request2 = CreateOrderRequest( symbol="ETH/USDT", side=OrderSide.SELL, order_type=OrderType.MARKET, amount=1.0, ) await manager.create_order(request1) await manager.create_order(request2) # Filter by symbol btc_orders = await manager.list_orders(symbol="BTC/USDT") assert all(o.symbol == "BTC/USDT" for o in btc_orders) # Filter by side buy_orders = await manager.list_orders(side=OrderSide.BUY) assert all(o.side == OrderSide.BUY for o in buy_orders) @pytest.mark.asyncio async def test_cancel_order(self, manager): """Test cancelling an order.""" # Create a limit order (market orders are filled immediately) request = CreateOrderRequest( symbol="BTC/USDT", side=OrderSide.BUY, order_type=OrderType.LIMIT, amount=0.1, price=10000.0, # Low price so it doesn't fill immediately ) order = await manager.create_order(request) cancelled = await manager.cancel_order(order.order_id) assert cancelled is not None assert cancelled.status == OrderStatus.CANCELLED @pytest.mark.asyncio async def test_cancel_order_not_found(self, manager): """Test cancelling a non-existent order.""" result = await manager.cancel_order("non_existent_id") assert result is None @pytest.mark.asyncio async def test_cancel_already_filled_order(self, manager): """Test cancelling an already filled order.""" request = CreateOrderRequest( symbol="BTC/USDT", side=OrderSide.BUY, order_type=OrderType.MARKET, amount=0.1, ) order = await manager.create_order(request) # Try to cancel the filled order cancelled = await manager.cancel_order(order.order_id) assert cancelled is None # Can't cancel filled orders @pytest.mark.asyncio async def test_get_position(self, manager): """Test getting a position.""" # First create an order to establish a position request = CreateOrderRequest( symbol="BTC/USDT", side=OrderSide.BUY, order_type=OrderType.MARKET, amount=0.1, ) await manager.create_order(request) position = await manager.get_position("BTC/USDT") assert position is not None assert position.symbol == "BTC/USDT" assert position.side == OrderSide.BUY assert position.amount == 0.1 @pytest.mark.asyncio async def test_get_position_not_found(self, manager): """Test getting a non-existent position.""" position = await manager.get_position("NONEXISTENT/USD") assert position is None @pytest.mark.asyncio async def test_list_positions(self, manager): """Test listing all positions.""" # Create positions in different symbols symbols = ["BTC/USDT", "ETH/USDT"] for symbol in symbols: request = CreateOrderRequest( symbol=symbol, side=OrderSide.BUY, order_type=OrderType.MARKET, amount=0.1, ) await manager.create_order(request) positions = await manager.list_positions() assert len(positions) == len(symbols) @pytest.mark.asyncio async def test_close_position(self, manager): """Test closing a position.""" # First create a position request = CreateOrderRequest( symbol="BTC/USDT", side=OrderSide.BUY, order_type=OrderType.MARKET, amount=0.1, ) await manager.create_order(request) # Close the position close_request = ClosePositionRequest(symbol="BTC/USDT") close_order = await manager.close_position(close_request) assert close_order is not None assert close_order.side == OrderSide.SELL # Opposite of BUY assert close_order.amount == 0.1 @pytest.mark.asyncio async def test_close_position_not_found(self, manager): """Test closing a non-existent position.""" close_request = ClosePositionRequest(symbol="NONEXISTENT/USD") result = await manager.close_position(close_request) assert result is None @pytest.mark.asyncio async def test_close_partial_position(self, manager): """Test closing a partial position.""" # Create a position request = CreateOrderRequest( symbol="BTC/USDT", side=OrderSide.BUY, order_type=OrderType.MARKET, amount=1.0, ) await manager.create_order(request) # Close half the position close_request = ClosePositionRequest(symbol="BTC/USDT", amount=0.5) close_order = await manager.close_position(close_request) assert close_order is not None assert close_order.amount == 0.5 @pytest.mark.asyncio async def test_get_order_summary(self, manager): """Test getting order summary.""" # Create some orders for _ in range(3): request = CreateOrderRequest( symbol="BTC/USDT", side=OrderSide.BUY, order_type=OrderType.MARKET, amount=0.1, ) await manager.create_order(request) summary = await manager.get_order_summary() assert isinstance(summary, OrderSummary) assert summary.total_orders >= 3 assert summary.filled_orders >= 3 @pytest.mark.asyncio async def test_get_position_summaries(self, manager): """Test getting position summaries.""" # Create a position request = CreateOrderRequest( symbol="BTC/USDT", side=OrderSide.BUY, order_type=OrderType.MARKET, amount=0.1, ) await manager.create_order(request) summaries = await manager.get_position_summaries() assert len(summaries) >= 1 assert isinstance(summaries[0], PositionSummary) assert summaries[0].symbol == "BTC/USDT" def test_get_trading_mode_status(self, manager): """Test getting trading mode status.""" status = manager.get_trading_mode_status() assert isinstance(status, TradingModeStatus) assert status.mode in ["simulated", "live"] assert isinstance(status.is_live, bool) assert isinstance(status.daily_limit_usd, float) assert isinstance(status.trade_count_today, int) def test_set_trading_mode(self, manager): """Test setting trading mode.""" # Start in paper mode assert manager.is_paper_trading is True # Switch to live mode (may fail if not configured) result = manager.set_trading_mode(TradingModeEnum.LIVE) # Result depends on configuration # Switch back to paper result = manager.set_trading_mode(TradingModeEnum.PAPER) assert result is True assert manager.is_paper_trading is True def test_toggle_trading_mode(self, manager): """Test toggling trading mode.""" initial_mode = manager.trading_mode status = manager.toggle_trading_mode() assert isinstance(status, TradingModeStatus) # Mode should have changed assert status.mode != initial_mode.value @pytest.mark.asyncio async def test_get_account_summary(self, manager): """Test getting complete account summary.""" # Create an order to have some data request = CreateOrderRequest( symbol="BTC/USDT", side=OrderSide.BUY, order_type=OrderType.MARKET, amount=0.1, ) await manager.create_order(request) summary = await manager.get_account_summary() assert "orders" in summary assert "positions" in summary assert "trading_mode" in summary assert isinstance(summary["total_position_value"], float) assert isinstance(summary["total_unrealized_pnl"], float) assert isinstance(summary["positions_count"], int) class TestCreateOrderRequest: """Tests for CreateOrderRequest model.""" def test_create_request(self): """Test creating a valid order request.""" request = CreateOrderRequest( symbol="BTC/USDT", side=OrderSide.BUY, order_type=OrderType.LIMIT, amount=0.1, price=50000.0, ) assert request.symbol == "BTC/USDT" assert request.side == OrderSide.BUY assert request.order_type == OrderType.LIMIT assert request.amount == 0.1 assert request.price == 50000.0 def test_create_market_request_without_price(self): """Test creating a market order request without price.""" request = CreateOrderRequest( symbol="BTC/USDT", side=OrderSide.BUY, order_type=OrderType.MARKET, amount=0.1, ) assert request.price is None class TestClosePositionRequest: """Tests for ClosePositionRequest model.""" def test_close_request_full(self): """Test creating a full close request.""" request = ClosePositionRequest(symbol="BTC/USDT") assert request.symbol == "BTC/USDT" assert request.amount is None # Full close assert request.order_type == OrderType.MARKET def test_close_request_partial(self): """Test creating a partial close request.""" request = ClosePositionRequest( symbol="BTC/USDT", amount=0.5, order_type=OrderType.LIMIT, price=60000.0, ) assert request.symbol == "BTC/USDT" assert request.amount == 0.5 assert request.order_type == OrderType.LIMIT assert request.price == 60000.0 class TestOrderSummary: """Tests for OrderSummary model.""" def test_default_values(self): """Test default values.""" summary = OrderSummary() assert summary.total_orders == 0 assert summary.open_orders == 0 assert summary.filled_orders == 0 assert summary.cancelled_orders == 0 assert summary.total_volume == 0.0 assert summary.total_fees == 0.0 def test_custom_values(self): """Test custom values.""" summary = OrderSummary( total_orders=10, open_orders=2, filled_orders=7, cancelled_orders=1, total_volume=100000.0, total_fees=100.0, ) assert summary.total_orders == 10 assert summary.open_orders == 2 assert summary.filled_orders == 7 assert summary.cancelled_orders == 1 assert summary.total_volume == 100000.0 assert summary.total_fees == 100.0 class TestPositionSummary: """Tests for PositionSummary model.""" def test_position_summary(self): """Test position summary creation.""" summary = PositionSummary( symbol="BTC/USDT", side=OrderSide.BUY, amount=0.5, entry_price=50000.0, current_price=55000.0, unrealized_pnl=2500.0, unrealized_pnl_pct=10.0, market_value=27500.0, ) assert summary.symbol == "BTC/USDT" assert summary.side == OrderSide.BUY assert summary.amount == 0.5 assert summary.entry_price == 50000.0 assert summary.current_price == 55000.0 assert summary.unrealized_pnl == 2500.0 assert summary.unrealized_pnl_pct == 10.0 assert summary.market_value == 27500.0 def test_position_summary_optional_price(self): """Test position summary without current price.""" summary = PositionSummary( symbol="BTC/USDT", side=OrderSide.BUY, amount=0.5, entry_price=50000.0, ) assert summary.current_price is None class TestTradingModeEnum: """Tests for TradingModeEnum.""" def test_enum_values(self): """Test enum values.""" assert TradingModeEnum.PAPER == "paper" assert TradingModeEnum.LIVE == "live" def test_enum_comparison(self): """Test enum comparison.""" assert TradingModeEnum.PAPER != TradingModeEnum.LIVE mode = TradingModeEnum.PAPER assert mode == TradingModeEnum.PAPER