mirror of
https://github.com/Ichinga-Samuel/aiomql.git
synced 2026-08-22 16:28:08 +00:00
v4.0.17 no-backtest
This commit is contained in:
@@ -1,23 +0,0 @@
|
||||
import logging
|
||||
|
||||
from aiomql.lib.bot import Bot
|
||||
from aiomql.contrib.strategies import Chaos
|
||||
from aiomql.contrib.symbols import ForexSymbol
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
async def test_bot():
|
||||
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s")
|
||||
syms = ["BTCUSD", "SOLUSD", "ETHUSD"]
|
||||
symbols = [ForexSymbol(name=sym) for sym in syms]
|
||||
strategies = [Chaos(symbol=symbol, name="test_chaos", params={"interval": 3}) for symbol in symbols]
|
||||
bot = Bot()
|
||||
bot.executor.timeout = 10
|
||||
bot.add_strategies(strategies=strategies)
|
||||
await bot.initialize()
|
||||
bot.executor.execute()
|
||||
assert len(bot.executor.coroutines) == 1
|
||||
assert len(bot.executor.coroutine_threads) == 1
|
||||
assert len(bot.executor.strategy_runners) == 3
|
||||
|
||||
assert bot.config.shutdown is True
|
||||
@@ -1,20 +0,0 @@
|
||||
import logging
|
||||
|
||||
from aiomql.lib.bot import Bot
|
||||
from aiomql.contrib.strategies import Chaos
|
||||
from aiomql.contrib.symbols import ForexSymbol
|
||||
|
||||
|
||||
def test_bot_sync():
|
||||
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s")
|
||||
syms = ["BTCUSD", "SOLUSD", "ETHUSD"]
|
||||
symbols = [ForexSymbol(name=sym) for sym in syms]
|
||||
strategies = [Chaos(symbol=symbol, name="test_chaos", params={"interval": 3}) for symbol in symbols]
|
||||
bot = Bot()
|
||||
bot.executor.timeout = 10
|
||||
bot.add_strategies(strategies=strategies)
|
||||
bot.initialize_sync()
|
||||
bot.executor.execute()
|
||||
assert len(bot.executor.strategy_runners) == 3
|
||||
assert len(bot.executor.coroutines) == 1
|
||||
assert len(bot.executor.coroutine_threads) == 1
|
||||
@@ -1,110 +0,0 @@
|
||||
import asyncio
|
||||
import json
|
||||
from csv import DictReader
|
||||
|
||||
import pytest
|
||||
|
||||
from aiomql.lib.result import Result
|
||||
from aiomql.core.models import OrderSendResult
|
||||
from aiomql.lib.trade_records import TradeRecords
|
||||
from aiomql.lib.positions import Positions
|
||||
|
||||
|
||||
class TestRecordsAndResults:
|
||||
@classmethod
|
||||
def setup_class(cls):
|
||||
cls.trade_records = TradeRecords()
|
||||
|
||||
@pytest.fixture(scope="class")
|
||||
async def buy(self, mt):
|
||||
sym = "BTCUSD"
|
||||
sym_info = await mt.symbol_info(sym)
|
||||
dsl = (sym_info.trade_stops_level + sym_info.spread) * 2 * sym_info.point
|
||||
sl = sym_info.ask - dsl
|
||||
tp = sym_info.ask + dsl
|
||||
return {
|
||||
"action": mt.TRADE_ACTION_DEAL,
|
||||
"symbol": sym,
|
||||
"volume": sym_info.volume_min,
|
||||
"type": mt.ORDER_TYPE_BUY,
|
||||
"price": sym_info.ask,
|
||||
"sl": sl,
|
||||
"tp": tp,
|
||||
}
|
||||
|
||||
@pytest.fixture(scope="class")
|
||||
async def sell(self, mt):
|
||||
sym = "BTCUSD"
|
||||
sym_info = await mt.symbol_info(sym)
|
||||
return {
|
||||
"action": mt.TRADE_ACTION_DEAL,
|
||||
"symbol": sym,
|
||||
"volume": sym_info.volume_min,
|
||||
"type": mt.ORDER_TYPE_SELL,
|
||||
"price": sym_info.bid,
|
||||
}
|
||||
|
||||
@pytest.fixture(scope="class", autouse=True)
|
||||
async def setup(self, sell, buy, mt):
|
||||
buy_res = await mt.order_send(buy)
|
||||
buy_res_2 = await mt.order_send(buy)
|
||||
sell_res = await mt.order_send(sell)
|
||||
sell_res_2 = await mt.order_send(sell)
|
||||
buy_res = Result(result=OrderSendResult(**buy_res._asdict()), name="test_result")
|
||||
sell_res = Result(result=OrderSendResult(**sell_res._asdict()), name="test_result")
|
||||
sell_res_2 = Result(result=OrderSendResult(**sell_res_2._asdict()), name="test_result")
|
||||
buy_res_2 = Result(result=OrderSendResult(**buy_res_2._asdict()), name="test_result")
|
||||
await asyncio.gather(
|
||||
buy_res.save(),
|
||||
sell_res.save(),
|
||||
buy_res_2.save(trade_record_mode="json"),
|
||||
sell_res_2.save(trade_record_mode="json"),
|
||||
)
|
||||
await Positions().close_all()
|
||||
|
||||
def test_records_dir(self):
|
||||
records_dir = self.trade_records.records_dir
|
||||
assert records_dir.is_dir()
|
||||
recs = list(records_dir.iterdir())
|
||||
assert len(recs) >= 2
|
||||
csvs, jsons = [], []
|
||||
matched_recs = list(records_dir.glob("test_result.*"))
|
||||
assert len(matched_recs) == 2
|
||||
for rec in matched_recs:
|
||||
if rec.match("test_result.json"):
|
||||
jsons.append(rec)
|
||||
elif rec.match("test_result.csv"):
|
||||
csvs.append(rec)
|
||||
else:
|
||||
continue
|
||||
assert len(csvs) == 1
|
||||
assert len(jsons) == 1
|
||||
|
||||
async def test_json_records(self):
|
||||
json_records = self.trade_records.get_json_records()
|
||||
matched_recs = [record for record in json_records if record.match("test_result.json")]
|
||||
assert len(matched_recs) == 1
|
||||
record = matched_recs[0]
|
||||
record_data = json.load(record.open())
|
||||
assert isinstance(record_data, list)
|
||||
assert len(record_data) == 2
|
||||
is_open = [data["closed"] is False for data in record_data]
|
||||
assert all(is_open)
|
||||
await self.trade_records.update_json_records()
|
||||
is_close = [data["closed"] is True for data in record_data]
|
||||
assert len(is_close) == 2
|
||||
|
||||
async def test_csv_records(self):
|
||||
csv_records = self.trade_records.get_csv_records()
|
||||
matched_recs = [record for record in csv_records if record.match("test_result.csv")]
|
||||
assert len(matched_recs) == 1
|
||||
record = matched_recs[0]
|
||||
record_data = DictReader(record.open())
|
||||
record_data = [row for row in record_data]
|
||||
assert isinstance(record_data, list)
|
||||
assert len(record_data) == 2
|
||||
is_open = [data["closed"].title() == "False" for data in record_data]
|
||||
assert all(is_open)
|
||||
await self.trade_records.update_json_records()
|
||||
is_close = [data["closed"].title() == "True" for data in record_data]
|
||||
assert len(is_close) == 2
|
||||
@@ -0,0 +1,421 @@
|
||||
"""Full integration tests for the Bot class with live MetaTrader 5 connection.
|
||||
|
||||
Tests cover the complete Bot lifecycle including:
|
||||
- Bot initialization and default state
|
||||
- Strategy management (add_strategy, add_strategies, add_strategy_all)
|
||||
- Terminal connection (async and sync)
|
||||
- Strategy initialization (async and sync, including failures)
|
||||
- Full execution lifecycle with Executor timeout
|
||||
- add_function and add_coroutine integration
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import time
|
||||
import threading
|
||||
import pytest
|
||||
|
||||
from aiomql.lib.bot import Bot
|
||||
from aiomql.lib.strategy import Strategy
|
||||
from aiomql.lib.executor import Executor
|
||||
from aiomql.lib.symbol import Symbol
|
||||
from aiomql.core.config import Config
|
||||
from aiomql.core.meta_trader import MetaTrader
|
||||
from aiomql.core.exceptions import StopTrading
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Test Strategies
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TickLoggerStrategy(Strategy):
|
||||
"""Strategy that logs the current tick and self-terminates."""
|
||||
|
||||
async def trade(self):
|
||||
tick = await self.mt5.symbol_info_tick(self.symbol.name)
|
||||
if tick is not None:
|
||||
self.parameters["last_bid"] = tick.bid
|
||||
self.parameters["last_ask"] = tick.ask
|
||||
self.parameters["executed"] = True
|
||||
self.running = False
|
||||
|
||||
|
||||
class CandleFetchStrategy(Strategy):
|
||||
"""Strategy that fetches the last 5 candles and self-terminates."""
|
||||
|
||||
async def trade(self):
|
||||
rates = await self.mt5.copy_rates_from_pos(self.symbol.name, self.mt5.TIMEFRAME_M1, 0, 5)
|
||||
if rates is not None:
|
||||
self.parameters["candle_count"] = len(rates)
|
||||
self.parameters["executed"] = True
|
||||
self.running = False
|
||||
|
||||
|
||||
class FailingStrategy(Strategy):
|
||||
"""Strategy that raises StopTrading to test graceful shutdown."""
|
||||
|
||||
async def trade(self):
|
||||
self.parameters["executed"] = True
|
||||
raise StopTrading("Intentional stop for testing")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Bot Initialization
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestBotInitialization:
|
||||
"""Test Bot class creation and default state."""
|
||||
|
||||
def test_bot_creates_executor(self):
|
||||
"""Test Bot creates an Executor on init."""
|
||||
bot = Bot()
|
||||
assert isinstance(bot.executor, Executor)
|
||||
|
||||
def test_bot_default_flags(self):
|
||||
"""Test Bot has correct default flags."""
|
||||
bot = Bot()
|
||||
assert bot.initialized is False
|
||||
assert bot.login is False
|
||||
assert bot.strategies == []
|
||||
|
||||
def test_bot_config_reference(self):
|
||||
"""Test Bot holds a Config reference pointing back to itself."""
|
||||
bot = Bot()
|
||||
assert isinstance(bot.config, Config)
|
||||
assert bot.config.bot is bot
|
||||
|
||||
def test_bot_has_mt5(self):
|
||||
"""Test Bot creates a MetaTrader instance."""
|
||||
bot = Bot()
|
||||
assert isinstance(bot.mt5, MetaTrader)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Strategy Management
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestBotStrategyManagement:
|
||||
"""Test adding strategies to the Bot."""
|
||||
|
||||
def test_add_strategy(self):
|
||||
"""Test adding a single strategy."""
|
||||
bot = Bot()
|
||||
sym = Symbol(name="BTCUSD")
|
||||
strategy = TickLoggerStrategy(symbol=sym)
|
||||
bot.add_strategy(strategy=strategy)
|
||||
assert len(bot.strategies) == 1
|
||||
assert bot.strategies[0] is strategy
|
||||
|
||||
def test_add_strategies(self):
|
||||
"""Test adding multiple strategies at once."""
|
||||
bot = Bot()
|
||||
strategies = [
|
||||
TickLoggerStrategy(symbol=Symbol(name="BTCUSD")),
|
||||
CandleFetchStrategy(symbol=Symbol(name="ETHUSD")),
|
||||
]
|
||||
bot.add_strategies(strategies=strategies)
|
||||
assert len(bot.strategies) == 2
|
||||
|
||||
def test_add_strategy_all(self):
|
||||
"""Test adding one strategy type across multiple symbols."""
|
||||
bot = Bot()
|
||||
symbols = [Symbol(name="BTCUSD"), Symbol(name="ETHUSD")]
|
||||
bot.add_strategy_all(strategy=TickLoggerStrategy, symbols=symbols)
|
||||
assert len(bot.strategies) == 2
|
||||
assert all(isinstance(s, TickLoggerStrategy) for s in bot.strategies)
|
||||
names = {s.symbol.name for s in bot.strategies}
|
||||
assert names == {"BTCUSD", "ETHUSD"}
|
||||
|
||||
def test_add_strategy_all_with_params(self):
|
||||
"""Test add_strategy_all passes params to each instance."""
|
||||
bot = Bot()
|
||||
symbols = [Symbol(name="BTCUSD")]
|
||||
bot.add_strategy_all(
|
||||
strategy=TickLoggerStrategy,
|
||||
symbols=symbols,
|
||||
params={"risk": 0.02},
|
||||
)
|
||||
assert bot.strategies[0].parameters["risk"] == 0.02
|
||||
|
||||
def test_add_strategy_preserves_order(self):
|
||||
"""Test strategies are added in order."""
|
||||
bot = Bot()
|
||||
s1 = TickLoggerStrategy(symbol=Symbol(name="BTCUSD"), name="first")
|
||||
s2 = CandleFetchStrategy(symbol=Symbol(name="ETHUSD"), name="second")
|
||||
bot.add_strategy(strategy=s1)
|
||||
bot.add_strategy(strategy=s2)
|
||||
assert bot.strategies[0].name == "first"
|
||||
assert bot.strategies[1].name == "second"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Terminal Connection
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestBotTerminalConnection:
|
||||
"""Test terminal initialization and login."""
|
||||
|
||||
async def test_start_terminal_async(self):
|
||||
"""Test async terminal start sets initialized and login flags."""
|
||||
bot = Bot()
|
||||
result = await bot.start_terminal()
|
||||
assert result is True
|
||||
assert bot.initialized is True
|
||||
assert bot.login is True
|
||||
|
||||
def test_start_terminal_sync(self):
|
||||
"""Test sync terminal start sets initialized and login flags."""
|
||||
bot = Bot()
|
||||
result = bot.start_terminal_sync()
|
||||
assert result is True
|
||||
assert bot.initialized is True
|
||||
assert bot.login is True
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Strategy Initialization
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestBotStrategyInitialization:
|
||||
"""Test strategy initialization through the Bot."""
|
||||
|
||||
async def test_init_strategy_async(self):
|
||||
"""Test async init_strategy initializes and registers a strategy."""
|
||||
bot = Bot()
|
||||
await bot.start_terminal()
|
||||
strategy = TickLoggerStrategy(symbol=Symbol(name="BTCUSD"))
|
||||
result = await bot.init_strategy(strategy=strategy)
|
||||
assert result is True
|
||||
assert strategy in bot.executor.strategy_runners
|
||||
|
||||
async def test_init_strategies_async(self):
|
||||
"""Test async init_strategies initializes all strategies."""
|
||||
bot = Bot()
|
||||
await bot.start_terminal()
|
||||
s1 = TickLoggerStrategy(symbol=Symbol(name="BTCUSD"))
|
||||
s2 = CandleFetchStrategy(symbol=Symbol(name="ETHUSD"))
|
||||
bot.add_strategy(strategy=s1)
|
||||
bot.add_strategy(strategy=s2)
|
||||
await bot.init_strategies()
|
||||
assert len(bot.executor.strategy_runners) == 2
|
||||
|
||||
def test_init_strategy_sync(self):
|
||||
"""Test sync init_strategy_sync initializes and registers a strategy."""
|
||||
bot = Bot()
|
||||
bot.start_terminal_sync()
|
||||
strategy = TickLoggerStrategy(symbol=Symbol(name="BTCUSD"))
|
||||
result = bot.init_strategy_sync(strategy=strategy)
|
||||
assert result is True
|
||||
assert strategy in bot.executor.strategy_runners
|
||||
|
||||
async def test_init_strategy_invalid_symbol(self):
|
||||
"""Test init_strategy with an invalid symbol returns False."""
|
||||
bot = Bot()
|
||||
await bot.start_terminal()
|
||||
strategy = TickLoggerStrategy(symbol=Symbol(name="INVALID_SYMBOL_XYZ"))
|
||||
result = await bot.init_strategy(strategy=strategy)
|
||||
assert result is False
|
||||
assert strategy not in bot.executor.strategy_runners
|
||||
|
||||
async def test_init_strategies_partial_failure(self):
|
||||
"""Test init_strategies handles mix of valid and invalid symbols."""
|
||||
bot = Bot()
|
||||
await bot.start_terminal()
|
||||
s_good = TickLoggerStrategy(symbol=Symbol(name="BTCUSD"))
|
||||
s_bad = CandleFetchStrategy(symbol=Symbol(name="INVALID_SYMBOL_XYZ"))
|
||||
bot.add_strategy(strategy=s_good)
|
||||
bot.add_strategy(strategy=s_bad)
|
||||
await bot.init_strategies()
|
||||
# Only the valid strategy should be in the executor
|
||||
assert len(bot.executor.strategy_runners) == 1
|
||||
assert s_good in bot.executor.strategy_runners
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Full Lifecycle – Synchronous (execute)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestBotFullLifecycle:
|
||||
"""Test full bot execution lifecycle using executor.timeout."""
|
||||
|
||||
def test_execute_with_tick_logger(self):
|
||||
"""Test execute() runs a TickLoggerStrategy to completion."""
|
||||
bot = Bot()
|
||||
strategy = TickLoggerStrategy(
|
||||
symbol=Symbol(name="BTCUSD"),
|
||||
params={"executed": False},
|
||||
)
|
||||
bot.add_strategy(strategy=strategy)
|
||||
bot.executor.timeout = 3
|
||||
bot.execute()
|
||||
assert strategy.parameters["executed"] is True
|
||||
assert "last_bid" in strategy.parameters
|
||||
assert "last_ask" in strategy.parameters
|
||||
|
||||
def test_execute_with_multiple_strategies(self):
|
||||
"""Test execute() runs multiple strategies."""
|
||||
bot = Bot()
|
||||
s1 = TickLoggerStrategy(
|
||||
symbol=Symbol(name="BTCUSD"),
|
||||
params={"executed": False},
|
||||
)
|
||||
s2 = CandleFetchStrategy(
|
||||
symbol=Symbol(name="ETHUSD"),
|
||||
params={"executed": False},
|
||||
)
|
||||
bot.add_strategy(strategy=s1)
|
||||
bot.add_strategy(strategy=s2)
|
||||
bot.executor.timeout = 5
|
||||
bot.execute()
|
||||
assert s1.parameters["executed"] is True
|
||||
assert s2.parameters["executed"] is True
|
||||
assert s2.parameters["candle_count"] == 5
|
||||
|
||||
def test_execute_with_failing_strategy(self):
|
||||
"""Test execute() handles a strategy that raises StopTrading."""
|
||||
bot = Bot()
|
||||
strategy = FailingStrategy(
|
||||
symbol=Symbol(name="BTCUSD"),
|
||||
params={"executed": False},
|
||||
)
|
||||
bot.add_strategy(strategy=strategy)
|
||||
bot.executor.timeout = 3
|
||||
bot.execute()
|
||||
# Strategy should have run and set executed before raising
|
||||
assert strategy.parameters["executed"] is True
|
||||
# Strategy should have stopped running
|
||||
assert strategy.running is False
|
||||
|
||||
def test_execute_no_strategies_sets_shutdown(self):
|
||||
"""Test execute() with no strategies triggers shutdown flag."""
|
||||
bot = Bot()
|
||||
bot.executor.timeout = 2
|
||||
bot.execute()
|
||||
assert bot.config.shutdown is True
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Full Lifecycle – Asynchronous (start)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestBotAsyncLifecycle:
|
||||
"""Test full async bot lifecycle using executor.timeout."""
|
||||
|
||||
async def test_start_with_tick_logger(self):
|
||||
"""Test start() runs a TickLoggerStrategy to completion."""
|
||||
bot = Bot()
|
||||
strategy = TickLoggerStrategy(
|
||||
symbol=Symbol(name="BTCUSD"),
|
||||
params={"executed": False},
|
||||
)
|
||||
bot.add_strategy(strategy=strategy)
|
||||
bot.executor.timeout = 3
|
||||
await bot.start()
|
||||
assert strategy.parameters["executed"] is True
|
||||
|
||||
async def test_start_with_candle_fetcher(self):
|
||||
"""Test start() runs a CandleFetchStrategy to completion."""
|
||||
bot = Bot()
|
||||
strategy = CandleFetchStrategy(
|
||||
symbol=Symbol(name="BTCUSD"),
|
||||
params={"executed": False},
|
||||
)
|
||||
bot.add_strategy(strategy=strategy)
|
||||
bot.executor.timeout = 3
|
||||
await bot.start()
|
||||
assert strategy.parameters["executed"] is True
|
||||
assert strategy.parameters["candle_count"] == 5
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# add_function / add_coroutine
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestBotAddFunctionCoroutine:
|
||||
"""Test add_function and add_coroutine execute during bot lifecycle."""
|
||||
|
||||
def test_add_function_runs(self):
|
||||
"""Test a function added via add_function is executed."""
|
||||
result_holder = {"called": False}
|
||||
|
||||
def mark_called():
|
||||
result_holder["called"] = True
|
||||
|
||||
bot = Bot()
|
||||
strategy = TickLoggerStrategy(
|
||||
symbol=Symbol(name="BTCUSD"),
|
||||
params={"executed": False},
|
||||
)
|
||||
bot.add_strategy(strategy=strategy)
|
||||
bot.add_function(function=mark_called)
|
||||
bot.executor.timeout = 3
|
||||
bot.execute()
|
||||
assert result_holder["called"] is True
|
||||
|
||||
def test_add_coroutine_runs(self):
|
||||
"""Test a coroutine added via add_coroutine is executed."""
|
||||
result_holder = {"called": False}
|
||||
|
||||
async def async_mark():
|
||||
result_holder["called"] = True
|
||||
|
||||
bot = Bot()
|
||||
strategy = TickLoggerStrategy(
|
||||
symbol=Symbol(name="BTCUSD"),
|
||||
params={"executed": False},
|
||||
)
|
||||
bot.add_strategy(strategy=strategy)
|
||||
bot.add_coroutine(coroutine=async_mark, on_separate_thread=True)
|
||||
bot.executor.timeout = 3
|
||||
bot.execute()
|
||||
assert result_holder["called"] is True
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Mixed Strategy Types
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestBotMixedStrategies:
|
||||
"""Test Bot with a mix of strategies on different symbols."""
|
||||
|
||||
def test_execute_three_strategies_different_symbols(self):
|
||||
"""Test execute with tick, candle, and failing strategies on different symbols."""
|
||||
bot = Bot()
|
||||
s1 = TickLoggerStrategy(
|
||||
symbol=Symbol(name="BTCUSD"),
|
||||
params={"executed": False},
|
||||
)
|
||||
s2 = CandleFetchStrategy(
|
||||
symbol=Symbol(name="ETHUSD"),
|
||||
params={"executed": False},
|
||||
)
|
||||
s3 = FailingStrategy(
|
||||
symbol=Symbol(name="BTCUSD"),
|
||||
params={"executed": False},
|
||||
)
|
||||
bot.add_strategies(strategies=[s1, s2, s3])
|
||||
bot.executor.timeout = 5
|
||||
bot.execute()
|
||||
# All strategies should have executed
|
||||
assert s1.parameters["executed"] is True
|
||||
assert s2.parameters["executed"] is True
|
||||
assert s3.parameters["executed"] is True
|
||||
# Verify specific strategy results
|
||||
assert "last_bid" in s1.parameters
|
||||
assert s2.parameters["candle_count"] == 5
|
||||
# Failing strategy should have stopped
|
||||
assert s3.running is False
|
||||
|
||||
def test_same_strategy_on_multiple_symbols(self):
|
||||
"""Test add_strategy_all runs the same strategy type on multiple symbols."""
|
||||
bot = Bot()
|
||||
symbols = [Symbol(name="BTCUSD"), Symbol(name="ETHUSD")]
|
||||
bot.add_strategy_all(
|
||||
strategy=TickLoggerStrategy,
|
||||
symbols=symbols,
|
||||
params={"executed": False},
|
||||
)
|
||||
bot.executor.timeout = 5
|
||||
bot.execute()
|
||||
for s in bot.strategies:
|
||||
assert s.parameters["executed"] is True
|
||||
assert "last_bid" in s.parameters
|
||||
@@ -1,557 +0,0 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user