""" Tests for MT4 Integration with LLM Agent Auto-Trading """ import pytest from unittest.mock import AsyncMock, MagicMock, patch from datetime import datetime from src.clients.mt4_client import ( MT4Client, MT4ClientError, OrderAction, TradeResult ) from src.services.auto_trade_service import AutoTradeService from src.models.auto_trade import AutoTradeConfig, TradeDecision class TestMT4Client: """Tests for MT4Client""" @pytest.fixture def client(self): return MT4Client(data_service_url="http://localhost:8001") @pytest.mark.asyncio async def test_check_connection_disconnected(self, client): """Test check_connection when not connected""" with patch.object(client, '_request', new_callable=AsyncMock) as mock_request: mock_request.return_value = {"connected": False, "account_id": ""} result = await client.check_connection() assert result is False assert client.is_connected is False @pytest.mark.asyncio async def test_check_connection_connected(self, client): """Test check_connection when connected""" with patch.object(client, '_request', new_callable=AsyncMock) as mock_request: mock_request.return_value = { "connected": True, "account_id": "test123", "login": "12345", "server": "Demo-Server", "balance": 10000.0, "currency": "USD" } result = await client.check_connection() assert result is True assert client.is_connected is True assert client.account_info is not None assert client.account_info.login == "12345" @pytest.mark.asyncio async def test_open_trade_buy(self, client): """Test opening a BUY trade""" with patch.object(client, '_request', new_callable=AsyncMock) as mock_request: mock_request.return_value = { "success": True, "order_id": "ORD123", "position_id": "POS456" } result = await client.open_trade( symbol="EURUSD", action=OrderAction.BUY, volume=0.1, stop_loss=1.0900, take_profit=1.1100, comment="Test trade" ) assert result.success is True assert result.order_id == "ORD123" assert result.position_id == "POS456" @pytest.mark.asyncio async def test_open_trade_failure(self, client): """Test handling trade failure""" with patch.object(client, '_request', new_callable=AsyncMock) as mock_request: mock_request.side_effect = MT4ClientError("Insufficient margin") result = await client.open_trade( symbol="XAUUSD", action=OrderAction.BUY, volume=1.0 ) assert result.success is False assert "Insufficient margin" in result.error @pytest.mark.asyncio async def test_close_position(self, client): """Test closing a position""" with patch.object(client, '_request', new_callable=AsyncMock) as mock_request: mock_request.return_value = { "success": True, "position_id": "POS456" } result = await client.close_position("POS456") assert result.success is True assert result.position_id == "POS456" class TestAutoTradeServiceMT4: """Tests for AutoTradeService MT4 integration""" @pytest.fixture def service(self): return AutoTradeService() @pytest.fixture def sample_decision(self): return TradeDecision( symbol="EURUSD", action="BUY", confidence=0.85, reasoning="Strong bullish signal in accumulation phase", entry_price=1.1000, take_profit=1.1100, stop_loss=1.0950, position_size=0.1, ml_signal={"direction": "bullish", "confidence": 0.87}, amd_phase="accumulation" ) @pytest.mark.asyncio async def test_execute_paper_trade(self, service, sample_decision): """Test paper trade execution""" result = await service._execute_paper_trade(sample_decision) assert result["success"] is True assert result["mode"] == "paper" assert "PAPER-" in result["order_id"] assert result["symbol"] == "EURUSD" assert result["action"] == "BUY" @pytest.mark.asyncio async def test_execute_mt4_trade_not_connected(self, service, sample_decision): """Test MT4 trade when not connected""" with patch.object( service.mt4_client, 'check_connection', new_callable=AsyncMock ) as mock_check: mock_check.return_value = False result = await service._execute_mt4_trade(sample_decision) assert result["success"] is False assert "not connected" in result["error"].lower() @pytest.mark.asyncio async def test_execute_mt4_trade_success(self, service, sample_decision): """Test successful MT4 trade execution""" with patch.object( service.mt4_client, 'check_connection', new_callable=AsyncMock ) as mock_check: mock_check.return_value = True with patch.object( service.mt4_client, 'open_trade', new_callable=AsyncMock ) as mock_trade: mock_trade.return_value = TradeResult( success=True, order_id="MT4-ORD-123", position_id="MT4-POS-456" ) result = await service._execute_mt4_trade(sample_decision) assert result["success"] is True assert result["mode"] == "live" assert result["order_id"] == "MT4-ORD-123" def test_calculate_mt4_volume(self, service): """Test volume calculation""" # Normal case volume = service._calculate_mt4_volume("EURUSD", 0.5, 1.1000) assert volume == 0.5 # Minimum lot size volume = service._calculate_mt4_volume("EURUSD", 0.001, 1.1000) assert volume == 0.01 # Maximum safety cap volume = service._calculate_mt4_volume("EURUSD", 50.0, 1.1000) assert volume == 10.0 # None position size volume = service._calculate_mt4_volume("EURUSD", None, 1.1000) assert volume == 0.01 @pytest.mark.asyncio async def test_connect_mt4(self, service): """Test MT4 connection through service""" with patch.object( service.mt4_client, 'connect', new_callable=AsyncMock ) as mock_connect: mock_connect.return_value = True # Mock account_info service.mt4_client._account_info = MagicMock() service.mt4_client._account_info.login = "12345" service.mt4_client._account_info.server = "Demo" service.mt4_client._account_info.balance = 10000.0 service.mt4_client._account_info.currency = "USD" service.mt4_client._account_info.leverage = 100 result = await service.connect_mt4("test-account-id") assert result["success"] is True assert result["connected"] is True assert result["account"]["login"] == "12345" @pytest.mark.asyncio async def test_get_mt4_positions(self, service): """Test getting MT4 positions""" from src.clients.mt4_client import MT4Position with patch.object( service.mt4_client, 'check_connection', new_callable=AsyncMock ) as mock_check: mock_check.return_value = True with patch.object( service.mt4_client, 'get_positions', new_callable=AsyncMock ) as mock_positions: mock_positions.return_value = [ MT4Position( id="POS1", symbol="EURUSD", type="BUY", volume=0.1, open_price=1.1000, current_price=1.1050, stop_loss=1.0950, take_profit=1.1100, profit=50.0, swap=-0.5, open_time=datetime.utcnow(), comment="Test" ) ] result = await service.get_mt4_positions() assert result["success"] is True assert len(result["positions"]) == 1 assert result["positions"][0]["symbol"] == "EURUSD" assert result["positions"][0]["profit"] == 50.0 class TestAutoTradeConfigModes: """Tests for paper vs live trading modes""" @pytest.fixture def service(self): return AutoTradeService() @pytest.fixture def paper_config(self): return AutoTradeConfig( user_id="test_user", enabled=True, symbols=["EURUSD"], max_risk_percent=1.0, min_confidence=0.7, paper_trading=True, # Paper mode require_confirmation=False, max_open_positions=3, check_interval_minutes=5 ) @pytest.fixture def live_config(self): return AutoTradeConfig( user_id="test_user", enabled=True, symbols=["EURUSD"], max_risk_percent=1.0, min_confidence=0.7, paper_trading=False, # Live mode require_confirmation=True, # Require confirmation for safety max_open_positions=3, check_interval_minutes=5 ) def test_paper_config_defaults(self, paper_config): """Test paper trading config""" assert paper_config.paper_trading is True assert paper_config.require_confirmation is False def test_live_config_safety(self, live_config): """Test live trading requires confirmation by default""" assert live_config.paper_trading is False assert live_config.require_confirmation is True if __name__ == "__main__": pytest.main([__file__, "-v"])