stock/tests/test_order_manager.py
ZhangPeng 9aecdd036c Initial commit: OpenClaw Trading - AI多智能体量化交易系统
- 添加项目核心代码和配置
- 添加前端界面 (Next.js)
- 添加单元测试
- 更新 .gitignore 排除缓存和依赖
2026-02-27 03:47:40 +08:00

520 lines
17 KiB
Python

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