305 lines
10 KiB
Python
305 lines
10 KiB
Python
"""
|
|
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"])
|