"""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 from manual_trading_broker import ManualTradingBrokerManager 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_short_side_pl(self): """Margin SHORT trades should profit when price drops.""" trade = Trade( target='test_exchange', symbol='BTC/USDT', side='SHORT', order_price=50000.0, base_order_qty=0.1, fee=0.001 ) trade.status = 'filled' trade.stats['qty_filled'] = 0.1 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_first_fill_from_open_order_uses_fill_price(self): """First broker fill should not average against the original unfilled order notional.""" trade = Trade( target='kucoin', symbol='BTC/USDT', side='BUY', order_price=69394.3, base_order_qty=0.0001 ) trade.status = 'open' trade.trade_filled(qty=5.28e-06, price=69340.8) assert trade.status == 'part-filled' assert trade.stats['qty_filled'] == pytest.approx(5.28e-06) assert trade.stats['opening_price'] == pytest.approx(69340.8) assert trade.stats['opening_value'] == pytest.approx(5.28e-06 * 69340.8) def test_trade_update_values_uses_filled_quantity_for_pl(self): """Unrealized P/L should be based on filled exposure, not the original order size.""" trade = Trade( target='test_exchange', symbol='BTC/USDT', side='BUY', order_price=50000.0, base_order_qty=0.1, fee=0.001 ) trade.trade_filled(qty=0.05, price=50000.0) trade.update_values(55000.0) assert trade.stats['current_value'] == pytest.approx(2750.0) assert trade.stats['profit'] == pytest.approx(244.75) assert trade.stats['profit_pct'] == pytest.approx(9.79) 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 assert trade.stats['profit'] == pytest.approx(489.5) assert trade.stats['profit_pct'] == pytest.approx(9.79) 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_recover_missing_paper_margin_state_rebuilds_latest_and_prunes_duplicates(self, mock_users): """Restart recovery should rebuild a missing paper margin position and prune duplicate ghost trades.""" trades = Trades(mock_users) trades.manual_broker_manager = ManualTradingBrokerManager(data_cache=None, exchange_interface=None, users=mock_users) def make_margin_trade(unique_id, created_at, collateral, leverage, qty, price): trade = Trade( target='test_exchange', symbol='BTC/USDT', side='LONG', order_price=price, base_order_qty=qty, unique_id=unique_id, status='filled', stats={ 'opening_price': price, 'opening_value': qty * price, 'current_price': price, 'current_value': qty * price, 'settled_price': 0.0, 'settled_value': 0.0, 'qty_filled': qty, 'qty_settled': 0.0, 'profit': 0.0, 'profit_pct': 0.0, 'fee_paid': 0.0, 'realized_profit': 0.0, 'unrealized_profit': 0.0 }, is_paper=True, creator=1, created_at=created_at, broker_kind='paper', broker_mode='paper', broker_order_id=f'margin_{unique_id}', collateral=collateral, leverage=leverage ) trades.active_trades[unique_id] = trade make_margin_trade('older', '2026-03-12T01:00:00+00:00', 100.0, 3.0, 0.004, 70000.0) make_margin_trade('newer', '2026-03-12T02:00:00+00:00', 150.0, 5.0, 0.010, 71000.0) recovered = trades.recover_brokers() broker = trades.manual_broker_manager.get_paper_margin_broker(1) position = broker.get_position('BTC/USDT') assert recovered >= 1 assert position is not None assert position.entry_price == 71000.0 assert position.collateral == 150.0 assert position.leverage == 5.0 assert set(trades.active_trades.keys()) == {'newer'} assert 'older' in trades.settled_trades assert trades.settled_trades['older'].status == 'cancelled' assert trades.settled_trades['older'].stats['close_reason'] == 'paper_margin_recovery_pruned' def test_close_all_paper_trades_closes_spot_and_margin(self, mock_users): """Paper reset cleanup should remove both spot and margin active trades.""" trades = Trades(mock_users) spot_trade = Trade( target='test_exchange', symbol='BTC/USDT', side='BUY', order_price=50000.0, base_order_qty=0.1, unique_id='spot_trade', status='filled', stats={'qty_filled': 0.1}, is_paper=True, creator=1 ) margin_trade = Trade( target='test_exchange', symbol='ETH/USDT', side='LONG', order_price=2500.0, base_order_qty=0.2, unique_id='margin_trade', status='filled', stats={'qty_filled': 0.2}, is_paper=True, creator=1, broker_kind='paper', broker_mode='paper', broker_order_id='margin_margin_trade', collateral=100.0, leverage=3.0 ) trades.active_trades[spot_trade.unique_id] = spot_trade trades.active_trades[margin_trade.unique_id] = margin_trade closed_ids = trades.close_all_paper_trades(1) assert set(closed_ids) == {'spot_trade', 'margin_trade'} assert not trades.active_trades assert trades.settled_trades['spot_trade'].status == 'cancelled' assert trades.settled_trades['margin_trade'].status == 'cancelled' 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_paper_trade_persists_time_in_force(self, mock_users): """Manual trades should keep the selected time-in-force on the Trade object.""" trades = Trades(mock_users) status, trade_id = trades.new_trade( target='test_exchange', symbol='BTC/USDT', price=50000.0, side='buy', order_type='LIMIT', qty=0.1, user_id=1, time_in_force='IOC' ) assert status == 'Success' trade = trades.get_trade_by_id(trade_id) assert trade.time_in_force == 'IOC' 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_live_trade_rejects_manual_sltp(self, mock_users): """Manual live SL/TP should fail fast until exchange-native support exists.""" trades = Trades(mock_users) mock_exchange_interface = MagicMock() mock_exchange_interface.get_exchange.return_value = MagicMock(configured=True) trades.connect_exchanges(mock_exchange_interface) 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, stop_loss=45000.0 ) assert status == 'Error' assert 'not supported yet' 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_trade_recomputes_final_pl_from_close_price(self, mock_users): """Closing should use the settlement price, not the last cached unrealized P/L.""" 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 ) assert status == 'Success' trade = trades.get_trade_by_id(trade_id) trade.fee = 0.001 trade.update_values(45000.0) assert trade.stats['profit'] < 0 result = trades.close_trade(trade_id, current_price=55000.0) assert result['success'] is True assert result['final_pl'] == pytest.approx(489.5) assert result['final_pl_pct'] == pytest.approx(9.79) 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_settle_broker_closed_position_filters_by_user_and_status(self, mock_users): """Broker-side closes should only settle filled trades for the matching user/broker.""" trades = Trades(mock_users) filled_trade = Trade( target='test_exchange', symbol='BTC/USDT', side='BUY', order_price=50000.0, base_order_qty=0.1, is_paper=True, creator=1, broker_kind='paper', broker_mode='paper' ) filled_trade.trade_filled(qty=0.1, price=50000.0) other_user_trade = Trade( target='test_exchange', symbol='BTC/USDT', side='BUY', order_price=50000.0, base_order_qty=0.1, is_paper=True, creator=2, broker_kind='paper', broker_mode='paper' ) other_user_trade.trade_filled(qty=0.1, price=50000.0) open_trade = Trade( target='test_exchange', symbol='BTC/USDT', side='BUY', order_price=49000.0, base_order_qty=0.1, is_paper=True, creator=1, broker_kind='paper', broker_mode='paper' ) open_trade.status = 'open' trades.active_trades[filled_trade.unique_id] = filled_trade trades.active_trades[other_user_trade.unique_id] = other_user_trade trades.active_trades[open_trade.unique_id] = open_trade trades.stats['num_trades'] = 3 settled_ids = trades.settle_broker_closed_position( user_id=1, symbol='BTC/USDT', broker_key='paper', close_price=44000.0 ) assert settled_ids == [filled_trade.unique_id] assert filled_trade.unique_id not in trades.active_trades assert filled_trade.unique_id in trades.settled_trades assert trades.settled_trades[filled_trade.unique_id].status == 'closed' assert other_user_trade.unique_id in trades.active_trades assert open_trade.unique_id in trades.active_trades def test_close_position_returns_closed_trade_ids(self, mock_users): """Close-position API flow should report which trade IDs were removed locally.""" trades = Trades(mock_users) trades.manual_broker_manager = MagicMock() trades.manual_broker_manager.close_position.return_value = { 'success': True, 'status': 'filled', 'filled_price': 51000.0 } trade = Trade( target='kucoin', symbol='BTC/USDT', side='BUY', order_price=50000.0, base_order_qty=0.1, creator=1, broker_kind='live', broker_mode='production', broker_exchange='kucoin' ) trade.trade_filled(qty=0.1, price=50000.0) trades.active_trades[trade.unique_id] = trade trades.stats['num_trades'] = 1 result = trades.close_position(1, 'BTC/USDT', 'kucoin_production') assert result['success'] is True assert result['trades_closed'] == 1 assert result['closed_trades'] == [trade.unique_id] assert trade.unique_id not in trades.active_trades assert trade.unique_id in trades.settled_trades def test_close_position_leaves_trade_active_when_close_order_is_still_open(self, mock_users): """Live close-position should not settle/remove the local trade until the close order fills.""" trades = Trades(mock_users) trades.manual_broker_manager = MagicMock() trades.manual_broker_manager.close_position.return_value = { 'success': True, 'status': 'open', 'order_id': 'close123', 'filled_price': 0.0, 'message': 'Close order submitted' } trade = Trade( target='kucoin', symbol='BTC/USDT', side='BUY', order_price=50000.0, base_order_qty=0.1, creator=1, broker_kind='live', broker_mode='production', broker_exchange='kucoin' ) trade.trade_filled(qty=0.1, price=50000.0) trades.active_trades[trade.unique_id] = trade trades.stats['num_trades'] = 1 result = trades.close_position(1, 'BTC/USDT', 'kucoin_production') assert result['success'] is True assert result['trades_closed'] == 0 assert result['closed_trades'] == [] assert trade.unique_id in trades.active_trades assert trade.unique_id not in trades.settled_trades def test_paper_margin_trade_requires_risk_ack(self, mock_users): """Paper margin trades should honor the risk-ack gate before order placement.""" trades = Trades(mock_users) with patch('Configuration.Configuration') as mock_config_cls: mock_config = mock_config_cls.return_value mock_config.is_margin_risk_ack_required.return_value = True mock_config.can_trade_margin.return_value = (False, 'risk_not_acknowledged') status, message = trades.new_trade( target='test_exchange', symbol='BTC/USDT', price=50000.0, side='buy', order_type='MARKET', qty=0.1, user_id=1, broker_key='paper_margin_isolated' ) assert status == 'Error' assert 'acknowledge margin trading risks' in message def test_paper_margin_trade_opens_long_position(self, mock_users): """Paper margin trades should open a position via PaperMarginBroker.""" from manual_trading_broker import ManualTradingBrokerManager trades = Trades(mock_users) # Set up real broker manager trades.manual_broker_manager = ManualTradingBrokerManager() with patch('Configuration.Configuration') as mock_config_cls: mock_config = mock_config_cls.return_value mock_config.is_margin_risk_ack_required.return_value = False mock_config.can_trade_margin.return_value = (True, None) # Open a LONG position with 100 USDT collateral at 3x leverage status, result = trades.new_trade( target='test_exchange', symbol='BTC/USDT', price=50000.0, side='LONG', order_type='MARKET', qty=0.1, # Ignored for margin, collateral is used user_id=1, broker_key='paper_margin_isolated', collateral=100.0, leverage=3.0 ) assert status == 'Success', f"Expected Success, got {status}: {result}" assert result is not None # Verify position was created in margin broker margin_broker = trades.manual_broker_manager.get_paper_margin_broker(1) position = margin_broker.get_position('BTC/USDT') assert position is not None assert position.side == 'long' assert position.collateral == 100.0 assert position.leverage == 3.0 trade = trades.get_trade_by_id(result) assert trade is not None assert trade.collateral == 100.0 assert trade.leverage == 3.0 assert trade.base_order_qty == pytest.approx(position.size) assert trade.side == 'LONG' def test_paper_margin_trade_validates_side(self, mock_users): """Paper margin trades require LONG or SHORT side.""" trades = Trades(mock_users) with patch('Configuration.Configuration') as mock_config_cls: mock_config = mock_config_cls.return_value mock_config.is_margin_risk_ack_required.return_value = False mock_config.can_trade_margin.return_value = (True, None) # Try to open with BUY side (invalid for margin) status, message = trades.new_trade( target='test_exchange', symbol='BTC/USDT', price=50000.0, side='BUY', # Should be LONG or SHORT order_type='MARKET', qty=0.1, user_id=1, broker_key='paper_margin_isolated', collateral=100.0, leverage=3.0 ) assert status == 'Error' assert 'LONG' in message or 'SHORT' in message def test_live_margin_trade_uses_live_margin_broker(self, mock_users): """Live margin trades should use the live margin broker path instead of the spot live broker.""" trades = Trades(mock_users) trades.connect_exchanges(MagicMock()) trades.exchange_connected = MagicMock(return_value=True) trades.exchange_interface.get_exchange.return_value = MagicMock(configured=True) trades.exchange_interface.get_price.return_value = 50000.0 trades.exchange_interface.get_trading_fees.return_value = {'taker': 0.001, 'source': 'mock'} mock_manager = MagicMock() mock_live_margin_broker = MagicMock() mock_live_margin_broker.open_position.return_value = { 'success': True, 'order_id': 'live-margin-1', 'filled_amount': 0.006, 'average_price': 50000.0, 'position': { 'symbol': 'BTC/USDT', 'size': 0.006, 'entry_price': 50000.0, 'collateral': 100.0, 'leverage': 3.0 } } mock_manager.get_live_margin_broker.return_value = mock_live_margin_broker mock_manager.get_live_broker.return_value = None trades.manual_broker_manager = mock_manager with patch('Configuration.Configuration') as mock_config_cls: mock_config = mock_config_cls.return_value mock_config.is_margin_risk_ack_required.return_value = False mock_config.can_trade_margin.return_value = (True, None) status, trade_id = trades.new_trade( target='kucoin', symbol='BTC/USDT', price=50000.0, side='LONG', order_type='MARKET', qty=0.0, user_id=1, testnet=True, broker_key='kucoin_margin_isolated_testnet', collateral=100.0, leverage=3.0 ) assert status == 'Success' mock_manager.get_live_margin_broker.assert_called_once() mock_live_margin_broker.open_position.assert_called_once() trade = trades.get_trade_by_id(trade_id) assert trade is not None assert trade.side == 'LONG' assert trade.base_order_qty == pytest.approx(0.006) assert trade.collateral == pytest.approx(100.0) assert trade.leverage == pytest.approx(3.0) assert trades._trade_broker_key(trade) == 'kucoin_margin_isolated_testnet' def test_settle_broker_closed_position_matches_paper_margin(self, mock_users): """Paper margin broker closes should reconcile only margin trades for that symbol.""" trades = Trades(mock_users) trade = Trade( target='test_exchange', symbol='BTC/USDT', side='LONG', order_price=50000.0, base_order_qty=0.006, is_paper=True, creator=1, broker_kind='paper', broker_mode='paper', collateral=100.0, leverage=3.0 ) trade.trade_filled(qty=0.006, price=50000.0) trades.active_trades[trade.unique_id] = trade trades.stats['num_trades'] = 1 settled = trades.settle_broker_closed_position( user_id=1, symbol='BTC/USDT', broker_key='paper_margin_isolated', close_price=51000.0 ) assert settled == [trade.unique_id] assert trade.unique_id not in trades.active_trades assert trade.unique_id in trades.settled_trades 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