"""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_with_stop_loss_take_profit(self): """Test trade with SL/TP fields.""" trade = Trade( target='test_exchange', symbol='BTC/USDT', side='BUY', order_price=50000.0, base_order_qty=0.1, stop_loss=45000.0, take_profit=60000.0 ) assert trade.stop_loss == 45000.0 assert trade.take_profit == 60000.0 # Verify serialization json_data = trade.to_json() assert json_data['stop_loss'] == 45000.0 assert json_data['take_profit'] == 60000.0 def test_trade_sltp_defaults_to_none(self): """Test that SL/TP default to None when not provided.""" trade = Trade( target='test_exchange', symbol='BTC/USDT', side='BUY', order_price=50000.0, base_order_qty=0.1 ) assert trade.stop_loss is None assert trade.take_profit is None 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) # Use testnet=True to bypass production safety gate and test exchange check status, msg = trades.new_trade( target='binance', symbol='BTC/USDT', price=50000.0, side='buy', order_type='MARKET', qty=0.1, user_id=1, testnet=True ) assert status == 'Error' assert 'No exchange' in msg.lower() or 'no exchange' in msg.lower() def test_new_production_trade_blocked_without_env_var(self, mock_users): """Test that production trades are blocked without ALLOW_LIVE_PRODUCTION.""" import config original_value = getattr(config, 'ALLOW_LIVE_PRODUCTION', False) try: # Ensure production is NOT allowed config.ALLOW_LIVE_PRODUCTION = False 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, testnet=False # Production mode ) assert status == 'Error' assert 'production trading is disabled' in msg.lower() finally: # Restore original value config.ALLOW_LIVE_PRODUCTION = original_value 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