brighter-trading/tests/test_trade.py

461 lines
13 KiB
Python

"""Tests for the Trade and Trades classes."""
import pytest
from unittest.mock import MagicMock, patch
import sys
import os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src'))
from trade import Trade, Trades
class TestTrade:
"""Tests for the Trade class."""
def test_trade_creation(self):
"""Test basic trade creation."""
trade = Trade(
target='test_exchange',
symbol='BTC/USDT',
side='BUY',
order_price=50000.0,
base_order_qty=0.1
)
assert trade.symbol == 'BTC/USDT'
assert trade.side == 'BUY'
assert trade.order_price == 50000.0
assert trade.base_order_qty == 0.1
assert trade.status == 'inactive'
assert trade.is_paper is False
assert trade.unique_id is not None
def test_trade_paper_flag(self):
"""Test is_paper flag."""
trade = Trade(
target='test_exchange',
symbol='BTC/USDT',
side='BUY',
order_price=50000.0,
base_order_qty=0.1,
is_paper=True
)
assert trade.is_paper is True
def test_trade_to_json(self):
"""Test trade serialization."""
trade = Trade(
target='test_exchange',
symbol='BTC/USDT',
side='BUY',
order_price=50000.0,
base_order_qty=0.1,
is_paper=True,
creator=1
)
json_data = trade.to_json()
assert json_data['symbol'] == 'BTC/USDT'
assert json_data['side'] == 'BUY'
assert json_data['is_paper'] is True
assert json_data['creator'] == 1
assert 'stats' in json_data
def test_trade_update_values(self):
"""Test P/L calculation."""
trade = Trade(
target='test_exchange',
symbol='BTC/USDT',
side='BUY',
order_price=50000.0,
base_order_qty=0.1,
fee=0.001 # 0.1% fee
)
trade.status = 'filled'
trade.stats['qty_filled'] = 0.1
# Price goes up
trade.update_values(55000.0)
assert trade.stats['current_price'] == 55000.0
assert trade.stats['current_value'] == 5500.0
# Profit should be positive (minus fees)
assert trade.stats['profit'] > 0
def test_trade_sell_side_pl(self):
"""Test P/L calculation for sell side."""
trade = Trade(
target='test_exchange',
symbol='BTC/USDT',
side='SELL',
order_price=50000.0,
base_order_qty=0.1,
fee=0.001
)
trade.status = 'filled'
trade.stats['qty_filled'] = 0.1
# Price goes down - should be profit for sell
trade.update_values(45000.0)
assert trade.stats['profit'] > 0
def test_trade_filled(self):
"""Test trade fill logic."""
trade = Trade(
target='test_exchange',
symbol='BTC/USDT',
side='BUY',
order_price=50000.0,
base_order_qty=0.1
)
trade.trade_filled(qty=0.1, price=50000.0)
assert trade.status == 'filled'
assert trade.stats['qty_filled'] == 0.1
assert trade.stats['opening_price'] == 50000.0
def test_trade_partial_fill(self):
"""Test partial fill logic."""
trade = Trade(
target='test_exchange',
symbol='BTC/USDT',
side='BUY',
order_price=50000.0,
base_order_qty=0.1
)
trade.trade_filled(qty=0.05, price=50000.0)
assert trade.status == 'part-filled'
assert trade.stats['qty_filled'] == 0.05
def test_trade_settle(self):
"""Test trade settlement."""
trade = Trade(
target='test_exchange',
symbol='BTC/USDT',
side='BUY',
order_price=50000.0,
base_order_qty=0.1
)
trade.trade_filled(qty=0.1, price=50000.0)
trade.settle(qty=0.1, price=55000.0)
assert trade.status == 'closed'
assert trade.stats['settled_price'] == 55000.0
class TestTrades:
"""Tests for the Trades class."""
@pytest.fixture
def mock_users(self):
"""Create a mock Users object."""
users = MagicMock()
users.get_username.return_value = 'test_user'
return users
@pytest.fixture
def mock_data_cache(self):
"""Create a mock DataCache object."""
dc = MagicMock()
dc.db.table_exists.return_value = True
dc.get_all_rows_from_datacache.return_value = None
dc.get_rows_from_datacache.return_value = MagicMock(empty=True)
return dc
def test_trades_creation_no_cache(self, mock_users):
"""Test Trades creation without data cache."""
trades = Trades(mock_users)
assert trades.users == mock_users
assert trades.data_cache is None
assert len(trades.active_trades) == 0
def test_trades_creation_with_cache(self, mock_users, mock_data_cache):
"""Test Trades creation with data cache."""
trades = Trades(mock_users, data_cache=mock_data_cache)
assert trades.data_cache == mock_data_cache
def test_connect_exchanges(self, mock_users):
"""Test exchange connection."""
trades = Trades(mock_users)
mock_exchange = MagicMock()
trades.connect_exchanges(mock_exchange)
assert trades.exchange_interface == mock_exchange
assert trades.exchange_connected() is True
def test_new_paper_trade(self, mock_users):
"""Test creating a paper trade."""
trades = Trades(mock_users)
status, trade_id = trades.new_trade(
target='test_exchange',
symbol='BTC/USDT',
price=50000.0,
side='buy',
order_type='MARKET',
qty=0.1,
user_id=1
)
assert status == 'Success'
assert trade_id is not None
assert trade_id in trades.active_trades
# Check trade properties
trade = trades.get_trade_by_id(trade_id)
assert trade.is_paper is True
assert trade.status == 'filled'
assert trade.creator == 1
def test_new_live_trade_no_exchange(self, mock_users):
"""Test creating a live trade without exchange connected."""
trades = Trades(mock_users)
status, msg = trades.new_trade(
target='binance',
symbol='BTC/USDT',
price=50000.0,
side='buy',
order_type='MARKET',
qty=0.1,
user_id=1
)
assert status == 'Error'
assert 'No exchange connected' in msg
def test_get_trades_json(self, mock_users):
"""Test getting trades in JSON format."""
trades = Trades(mock_users)
# Create a paper trade
trades.new_trade(
target='test_exchange',
symbol='BTC/USDT',
price=50000.0,
side='buy',
order_type='MARKET',
qty=0.1
)
result = trades.get_trades('json')
assert len(result) == 1
assert result[0]['symbol'] == 'BTC/USDT'
def test_get_trades_for_user(self, mock_users):
"""Test filtering trades by user."""
trades = Trades(mock_users)
# Create trades for different users
trades.new_trade(
target='test_exchange',
symbol='BTC/USDT',
price=50000.0,
side='buy',
order_type='MARKET',
qty=0.1,
user_id=1
)
trades.new_trade(
target='test_exchange',
symbol='ETH/USDT',
price=3000.0,
side='buy',
order_type='MARKET',
qty=1.0,
user_id=2
)
# Filter for user 1
user1_trades = trades.get_trades_for_user(1, 'json')
assert len(user1_trades) == 1
assert user1_trades[0]['symbol'] == 'BTC/USDT'
def test_close_paper_trade(self, mock_users):
"""Test closing a paper trade."""
trades = Trades(mock_users)
# Create a paper trade
status, trade_id = trades.new_trade(
target='test_exchange',
symbol='BTC/USDT',
price=50000.0,
side='buy',
order_type='MARKET',
qty=0.1
)
assert trade_id in trades.active_trades
# Close the trade
result = trades.close_trade(trade_id, current_price=55000.0)
assert result['success'] is True
assert trade_id not in trades.active_trades
assert trade_id in trades.settled_trades
def test_close_nonexistent_trade(self, mock_users):
"""Test closing a trade that doesn't exist."""
trades = Trades(mock_users)
result = trades.close_trade('nonexistent_id')
assert result['success'] is False
assert 'not found' in result['message']
def test_is_valid_trade_id(self, mock_users):
"""Test trade ID validation."""
trades = Trades(mock_users)
_, trade_id = trades.new_trade(
target='test_exchange',
symbol='BTC/USDT',
price=50000.0,
side='buy',
order_type='MARKET',
qty=0.1
)
assert trades.is_valid_trade_id(trade_id) is True
assert trades.is_valid_trade_id('invalid_id') is False
def test_update_trades(self, mock_users):
"""Test updating trade P/L with price changes."""
trades = Trades(mock_users)
# Create and fill a trade
_, trade_id = trades.new_trade(
target='test_exchange',
symbol='BTC/USDT',
price=50000.0,
side='buy',
order_type='MARKET',
qty=0.1
)
# Update with new price
price_updates = {'BTC/USDT': 55000.0}
updates = trades.update(price_updates)
assert len(updates) > 0
# Find our trade in updates
trade_update = next((u for u in updates if u['id'] == trade_id), None)
assert trade_update is not None
assert trade_update['pl'] != 0 # Should have some P/L
def test_buy_method_paper(self, mock_users):
"""Test buy method creates a BUY paper trade using new_trade."""
trades = Trades(mock_users)
# Use new_trade for paper trades (buy/sell methods are for live trading)
status, trade_id = trades.new_trade(
target='test_exchange',
symbol='BTC/USDT',
price=50000.0,
side='buy',
order_type='MARKET',
qty=0.1,
user_id=1
)
assert status == 'Success'
trade = trades.get_trade_by_id(trade_id)
assert trade.side == 'BUY'
def test_sell_method_paper(self, mock_users):
"""Test sell method creates a SELL paper trade using new_trade."""
trades = Trades(mock_users)
# Use new_trade for paper trades (buy/sell methods are for live trading)
status, trade_id = trades.new_trade(
target='test_exchange',
symbol='BTC/USDT',
price=50000.0,
side='sell',
order_type='MARKET',
qty=0.1,
user_id=1
)
assert status == 'Success'
trade = trades.get_trade_by_id(trade_id)
assert trade.side == 'SELL'
class TestTradeIntegration:
"""Integration tests for Trade system."""
@pytest.fixture
def mock_users(self):
users = MagicMock()
users.get_username.return_value = 'test_user'
return users
def test_full_trade_lifecycle(self, mock_users):
"""Test complete lifecycle: create -> update -> close."""
trades = Trades(mock_users)
# Create trade
status, trade_id = trades.new_trade(
target='test_exchange',
symbol='BTC/USDT',
price=50000.0,
side='buy',
order_type='MARKET',
qty=0.1,
user_id=1
)
assert status == 'Success'
trade = trades.get_trade_by_id(trade_id)
# Set a more realistic fee (0.1% instead of default 10%)
trade.fee = 0.001
trade.stats['fee_paid'] = trade.stats['opening_value'] * trade.fee
# Update with higher price (20% increase should exceed fees)
trades.update({'BTC/USDT': 60000.0})
trade = trades.get_trade_by_id(trade_id)
assert trade.stats['profit'] > 0 # Should be in profit
# Close trade
result = trades.close_trade(trade_id, current_price=60000.0)
assert result['success'] is True
assert result['final_pl'] > 0
def test_multiple_trades(self, mock_users):
"""Test managing multiple trades."""
trades = Trades(mock_users)
# Create multiple trades
trade_ids = []
for i in range(3):
_, trade_id = trades.new_trade(
target='test_exchange',
symbol='BTC/USDT',
price=50000.0 + (i * 100),
side='buy',
order_type='MARKET',
qty=0.1
)
trade_ids.append(trade_id)
assert len(trades.active_trades) == 3
# Close one trade
trades.close_trade(trade_ids[1])
assert len(trades.active_trades) == 2
assert trade_ids[1] not in trades.active_trades