brighter-trading/tests/test_trade.py

1060 lines
35 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
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