758 lines
24 KiB
Python
758 lines
24 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_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_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_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_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
|