Files
aiomql/tests/live/integration/test_full_integration.py

558 lines
18 KiB
Python

"""Full integration tests for aiomql library in live mode.
This module tests the integration of all major components:
- Bot initialization and terminal connection
- Multiple strategies running concurrently on different symbols
- Position trackers and tracking functions
- Order creation and management
- State management and configuration
Note: These tests require a live MetaTrader 5 connection and should be run
with caution on a demo account.
"""
import pytest
import asyncio
import logging
from unittest.mock import MagicMock, AsyncMock, patch
from aiomql.lib.bot import Bot
from aiomql.lib.strategy import Strategy
from aiomql.lib.symbol import Symbol
from aiomql.lib.trader import Trader
from aiomql.lib.executor import Executor
from aiomql.lib.order import Order
from aiomql.lib.positions import Positions
from aiomql.lib.account import Account
from aiomql.lib.ram import RAM
from aiomql.core.config import Config
from aiomql.core.state import State
from aiomql.core.constants import OrderType, TimeFrame, TradeAction
from aiomql.contrib.symbols import ForexSymbol
from aiomql.contrib.strategies import Chaos
from aiomql.contrib.trackers import (
PositionTracker,
OpenPositionsTracker,
OpenPosition,
exit_at_profit,
extend_take_profit
)
from aiomql.contrib.utils.strategy_tracker import StrategyTracker
logger = logging.getLogger(__name__)
class SimpleTestStrategy(Strategy):
"""A simple test strategy for integration testing."""
parameters = {"interval": 1, "test_param": "value"}
def __init__(self, *, symbol: Symbol, params: dict = None, sessions=None, name="TestStrategy"):
super().__init__(symbol=symbol, params=params, sessions=sessions, name=name)
self.trade_count = 0
self.tracker = StrategyTracker()
async def trade(self):
"""Execute a simple trade iteration."""
self.trade_count += 1
self.tracker.update(trend="bullish" if self.trade_count % 2 == 0 else "bearish")
await self.sleep(secs=self.interval)
class TrendFollowerStrategy(Strategy):
"""A trend following strategy for testing multiple strategy types."""
parameters = {"timeframe": TimeFrame.M1, "period": 20}
def __init__(self, *, symbol: Symbol, params: dict = None, sessions=None, name="TrendFollower"):
super().__init__(symbol=symbol, params=params, sessions=sessions, name=name)
self.signals = []
async def trade(self):
"""Execute trend following logic."""
# Simulate checking trend
self.signals.append({"time": asyncio.get_event_loop().time(), "symbol": self.symbol.name})
await self.sleep(secs=1)
class TestBotIntegration:
"""Integration tests for Bot class with multiple strategies."""
@pytest.fixture
def mock_mt5(self):
"""Mock MetaTrader connection for testing."""
with patch("aiomql.core.meta_trader.MetaTrader") as mock:
mock_instance = MagicMock()
mock_instance.initialize = AsyncMock(return_value=True)
mock_instance.login = AsyncMock(return_value=True)
mock_instance.shutdown = AsyncMock()
mock.return_value = mock_instance
yield mock_instance
def test_bot_initialization(self):
"""Test Bot initializes with correct components."""
bot = Bot()
assert bot.config is not None
assert bot.executor is not None
assert bot.mt5 is not None
assert bot.initialized is False
assert bot.login is False
def test_bot_add_single_strategy(self):
"""Test adding a single strategy to bot."""
bot = Bot()
mock_symbol = MagicMock(spec=Symbol)
mock_symbol.name = "EURUSD"
strategy = SimpleTestStrategy(symbol=mock_symbol, name="test_simple")
bot.add_strategy(strategy=strategy)
assert len(bot.strategies) == 1
assert bot.strategies[0].name == "test_simple"
def test_bot_add_multiple_strategies(self):
"""Test adding multiple strategies to bot."""
bot = Bot()
symbols = []
for name in ["EURUSD", "GBPUSD", "USDJPY"]:
mock_symbol = MagicMock(spec=Symbol)
mock_symbol.name = name
symbols.append(mock_symbol)
strategies = [
SimpleTestStrategy(symbol=symbols[0], name="strategy_eur"),
TrendFollowerStrategy(symbol=symbols[1], name="strategy_gbp"),
SimpleTestStrategy(symbol=symbols[2], name="strategy_jpy")
]
bot.add_strategies(strategies=strategies)
assert len(bot.strategies) == 3
def test_bot_add_coroutine(self):
"""Test adding coroutine to bot."""
bot = Bot()
async def test_coro(param1="default"):
await asyncio.sleep(0.1)
bot.add_coroutine(coroutine=test_coro, param1="value")
assert len(bot.executor.coroutines) == 1
def test_bot_add_function(self):
"""Test adding synchronous function to bot."""
bot = Bot()
def test_func(param1="default"):
pass
bot.add_function(function=test_func, param1="value")
assert len(bot.executor.functions) == 1
class TestExecutorIntegration:
"""Integration tests for Executor with multiple strategies."""
def test_executor_add_strategies(self):
"""Test executor can add multiple strategies."""
executor = Executor()
mock_symbol = MagicMock(spec=Symbol)
mock_symbol.name = "EURUSD"
strategies = [
SimpleTestStrategy(symbol=mock_symbol, name=f"strategy_{i}")
for i in range(5)
]
executor.add_strategies(strategies=tuple(strategies))
assert len(executor.strategy_runners) == 5
def test_executor_mixed_tasks(self):
"""Test executor with strategies, coroutines, and functions."""
executor = Executor()
mock_symbol = MagicMock(spec=Symbol)
mock_symbol.name = "EURUSD"
# Add strategy
strategy = SimpleTestStrategy(symbol=mock_symbol, name="test")
executor.add_strategy(strategy=strategy)
# Add coroutine
async def coro():
pass
executor.add_coroutine(coroutine=coro, kwargs={})
# Add function
def func():
pass
executor.add_function(function=func, kwargs={})
assert len(executor.strategy_runners) == 1
assert len(executor.coroutines) == 1
assert len(executor.functions) == 1
class TestStrategyIntegration:
"""Integration tests for Strategy class."""
def test_strategy_initialization(self):
"""Test strategy initializes with parameters."""
mock_symbol = MagicMock(spec=Symbol)
mock_symbol.name = "EURUSD"
strategy = SimpleTestStrategy(
symbol=mock_symbol,
name="test_strategy",
params={"custom_param": 100}
)
assert strategy.name == "test_strategy"
assert strategy.symbol is mock_symbol
assert strategy.custom_param == 100
def test_strategy_tracker_integration(self):
"""Test strategy with StrategyTracker."""
mock_symbol = MagicMock(spec=Symbol)
mock_symbol.name = "EURUSD"
strategy = SimpleTestStrategy(symbol=mock_symbol)
# Simulate trade iterations
strategy.tracker.update(trend="bullish")
assert strategy.tracker.bullish is True
assert strategy.tracker.bearish is False
strategy.tracker.update(trend="bearish")
assert strategy.tracker.bearish is True
assert strategy.tracker.bullish is False
def test_multiple_strategy_types(self):
"""Test different strategy types can coexist."""
mock_symbol = MagicMock(spec=Symbol)
mock_symbol.name = "EURUSD"
simple = SimpleTestStrategy(symbol=mock_symbol, name="simple")
trend = TrendFollowerStrategy(symbol=mock_symbol, name="trend")
assert simple.name == "simple"
assert trend.name == "trend"
assert simple.parameters != trend.parameters
class TestTrackerIntegration:
"""Integration tests for position tracking components."""
def test_position_tracker_initialization(self):
"""Test PositionTracker initialization with OpenPosition."""
mock_open_position = MagicMock()
mock_open_position.add_tracker = MagicMock()
async def tracking_func(pos, **kwargs):
pass
tracking_func.__name__ = "tracking_func"
tracker = PositionTracker(
mock_open_position,
tracking_func,
name="test_tracker",
rank=1,
function_params={"param": "value"}
)
assert tracker.name == "test_tracker"
assert tracker.rank == 1
assert tracker.params == {"param": "value"}
mock_open_position.add_tracker.assert_called_once()
async def test_position_tracker_execution(self):
"""Test PositionTracker executes tracking function."""
mock_open_position = MagicMock()
mock_open_position.add_tracker = MagicMock()
call_log = []
async def tracking_func(pos, **kwargs):
call_log.append({"pos": pos, "kwargs": kwargs})
tracking_func.__name__ = "tracking_func"
tracker = PositionTracker(
mock_open_position,
tracking_func,
function_params={"sl": -10, "tp": 20}
)
await tracker()
assert len(call_log) == 1
assert call_log[0]["kwargs"]["sl"] == -10
assert call_log[0]["kwargs"]["tp"] == 20
def test_strategy_tracker_state_management(self):
"""Test StrategyTracker manages trend state correctly."""
tracker = StrategyTracker()
# Initial state
assert tracker.ranging is True
assert tracker.bullish is False
assert tracker.bearish is False
# Transition to bullish
tracker.update(trend="bullish")
assert tracker.ranging is False
assert tracker.bullish is True
assert tracker.bearish is False
# Direct to bearish
tracker.update(trend="bearish")
assert tracker.ranging is False
assert tracker.bullish is False
assert tracker.bearish is True
# Back to ranging
tracker.update(trend="ranging")
assert tracker.ranging is True
assert tracker.bullish is False
assert tracker.bearish is False
class TestTrackingFunctionsIntegration:
"""Integration tests for position tracking functions."""
async def test_exit_at_profit_integration(self):
"""Test exit_at_profit with mock position."""
mock_position = MagicMock()
mock_position.profit = 100.0
mock_pos = MagicMock()
mock_pos.symbol = MagicMock()
mock_pos.symbol.name = "EURUSD"
mock_pos.ticket = 12345
mock_pos.position = mock_position
mock_pos.update_position = AsyncMock(return_value=True)
mock_pos.close_position = AsyncMock(return_value=(True, MagicMock()))
# Should close when profit >= tp
await exit_at_profit(mock_pos, tp=50.0)
mock_pos.close_position.assert_called_once()
async def test_exit_at_profit_no_action(self):
"""Test exit_at_profit does not close when conditions not met."""
mock_position = MagicMock()
mock_position.profit = 30.0 # Below tp, above sl
mock_pos = MagicMock()
mock_pos.position = mock_position
mock_pos.update_position = AsyncMock(return_value=True)
mock_pos.close_position = AsyncMock()
await exit_at_profit(mock_pos, tp=50.0, sl=-20.0)
mock_pos.close_position.assert_not_called()
class TestStateAndConfigIntegration:
"""Integration tests for State and Config components."""
def test_config_singleton(self):
"""Test Config is singleton."""
config1 = Config()
config2 = Config()
assert config1 is config2
def test_config_shutdown_flag(self):
"""Test config shutdown flag affects all references."""
config1 = Config()
config2 = Config()
original_shutdown = config1.shutdown
config1.shutdown = True
assert config2.shutdown is True
# Restore
config1.shutdown = original_shutdown
def test_state_initialization(self):
"""Test State can be initialized with key."""
state = State()
# State should support dict-like access
assert hasattr(state, "__setitem__")
assert hasattr(state, "__getitem__")
class TestMultiSymbolIntegration:
"""Integration tests for multiple symbols."""
def test_forex_symbol_creation(self):
"""Test creating multiple forex symbols."""
symbols = ["EURUSD", "GBPUSD", "USDJPY", "AUDUSD"]
forex_symbols = [ForexSymbol(name=sym) for sym in symbols]
assert len(forex_symbols) == 4
for i, sym in enumerate(forex_symbols):
assert sym.name == symbols[i]
def test_strategies_on_different_symbols(self):
"""Test strategies assigned to different symbols."""
symbols = [
ForexSymbol(name="EURUSD"),
ForexSymbol(name="GBPUSD"),
ForexSymbol(name="USDJPY")
]
strategies = []
for i, symbol in enumerate(symbols):
strategy = SimpleTestStrategy(
symbol=symbol,
name=f"strategy_{symbol.name}",
params={"interval": i + 1}
)
strategies.append(strategy)
assert len(strategies) == 3
assert strategies[0].symbol.name == "EURUSD"
assert strategies[1].symbol.name == "GBPUSD"
assert strategies[2].symbol.name == "USDJPY"
assert strategies[0].interval == 1
assert strategies[1].interval == 2
assert strategies[2].interval == 3
class TestFullBotWorkflow:
"""Integration tests for complete bot workflow."""
async def test_bot_with_chaos_strategy(self):
"""Test bot with Chaos strategy from contrib."""
symbols = [ForexSymbol(name=sym) for sym in ["BTCUSD", "ETHUSD"]]
strategies = [
Chaos(symbol=symbol, name=f"chaos_{symbol.name}", params={"interval": 1})
for symbol in symbols
]
bot = Bot()
bot.executor.timeout = 2 # Short timeout for testing
bot.add_strategies(strategies=strategies)
assert len(bot.strategies) == 2
assert bot.executor is not None
async def test_bot_with_mixed_strategies(self):
"""Test bot with different strategy types."""
symbol1 = ForexSymbol(name="EURUSD")
symbol2 = ForexSymbol(name="GBPUSD")
symbol3 = ForexSymbol(name="USDJPY")
strategies = [
SimpleTestStrategy(symbol=symbol1, name="simple_eur"),
TrendFollowerStrategy(symbol=symbol2, name="trend_gbp"),
Chaos(symbol=symbol3, name="chaos_jpy", params={"interval": 1})
]
bot = Bot()
bot.executor.timeout = 2
bot.add_strategies(strategies=strategies)
assert len(bot.strategies) == 3
# Verify different strategy types
strategy_names = [s.name for s in bot.strategies]
assert "simple_eur" in strategy_names
assert "trend_gbp" in strategy_names
assert "chaos_jpy" in strategy_names
async def test_bot_with_coroutines_and_functions(self):
"""Test bot with strategies, coroutines and functions."""
symbol = ForexSymbol(name="EURUSD")
strategy = SimpleTestStrategy(symbol=symbol, name="test")
async def monitor_task(interval=1):
"""Async monitoring task."""
await asyncio.sleep(interval)
def sync_logger(message=""):
"""Sync logging function."""
logger.info(message)
bot = Bot()
bot.executor.timeout = 2
bot.add_strategy(strategy=strategy)
bot.add_coroutine(coroutine=monitor_task, interval=1)
bot.add_function(function=sync_logger, message="test")
assert len(bot.strategies) == 1
assert len(bot.executor.coroutines) == 1
assert len(bot.executor.functions) == 1
class TestRAMIntegration:
"""Integration tests for Risk Assessment and Management."""
def test_ram_initialization(self):
"""Test RAM can be initialized with parameters."""
ram = RAM(
risk_to_reward=2.0,
risk=2.0,
min_amount=10.0,
max_amount=100.0
)
assert ram.risk_to_reward == 2.0
assert ram.risk == 2.0
def test_ram_default_values(self):
"""Test RAM has sensible defaults."""
ram = RAM()
# RAM should have default attributes
assert hasattr(ram, "risk_to_reward")
assert hasattr(ram, "risk")
assert ram.risk_to_reward == 2
assert ram.risk == 1
class TestOrderIntegration:
"""Integration tests for Order class."""
def test_order_creation(self):
"""Test Order can be created with required fields."""
order = Order(
symbol="EURUSD",
volume=0.1,
type=OrderType.BUY,
action=TradeAction.DEAL
)
assert order.symbol == "EURUSD"
assert order.volume == 0.1
assert order.type == OrderType.BUY
assert order.action == TradeAction.DEAL
def test_order_with_stops(self):
"""Test Order can include stop levels."""
order = Order(
symbol="EURUSD",
volume=0.1,
type=OrderType.BUY,
action=TradeAction.DEAL,
sl=1.0900,
tp=1.1100,
price=1.1000
)
assert order.sl == 1.0900
assert order.tp == 1.1100
assert order.price == 1.1000