diff --git a/docs/lib/account.md b/docs/lib/account.md
index 8fc81df..5e0a11b 100644
--- a/docs/lib/account.md
+++ b/docs/lib/account.md
@@ -2,66 +2,21 @@
## Table of Contents
- [Account](#account.Account)
-- [\_\_init\_\_](#account.__init__)
-- [\_\_aenter\_\_](#account.__aenter__)
-- [\_\_aexit\_\_](#account.__aexit__)
-- [sign_in](#account.sign_in)
- [refresh](#account.refresh)
-- [has_symbol](#account.has_symbol)
-- [symbols_get](#account.symbols_get)
-
+
+
### Account
```python
-class Account(AccountInfo)
+class Account(_Base, AccountInfo)
```
-Singleton class for managing a trading account. A subclass of AccountInfo.
-All AccountInfo attributes are available in this class.
+A singleton class for managing a trading account. A subclass of _Base and AccountInfo. It supports asynchronous context
+management protocol.
+
#### Attributes
| Name | Type | Description | Default |
|-------------|-------------------|------------------------------------------------------|---------|
| `connected` | `bool` | Status of connection to MetaTrader 5 Terminal | False |
-| `symbols` | `set[SymbolInfo]` | A set of available symbols for the financial market. | set() |
-
-
-#### \_\_init\_\_
-```python
-def __init__(self, *args, **kwargs)
-```
-Initializes the Account class. Inherits all attributes from the AccountInfo class.
-
-
-### __aenter__
-```python
-async def __aenter__() -> 'Account'
-```
-Async context manager for the Account class. Connects to a trading account and returns the account instance.
-#### Returns:
-| Type | Description |
-|-----------|----------------------------------|
-| `Account` | An instance of the Account class |
-#### Raises:
-| Exception | Description |
-|--------------|----------------|
-| `LoginError` | If login fails |
-
-
-### __aexit__
-```python
-async def __aexit__(exc_type, exc_value, traceback)
-```
-Async context manager for the Account class. Disconnects from the trading account.
-
-
-### sign_in
-```python
-async def sign_in() -> bool
-```
-Connect to a trading account.
-#### Returns:
-| Type | Description |
-|--------|-----------------------------------------|
-| `bool` | True if login was successful else False |
### refresh
@@ -69,29 +24,3 @@ Connect to a trading account.
async def refresh()
```
Refreshes the account instance with the latest data from the MetaTrader 5 terminal
-
-
-### has_symbol
-```python
-def has_symbol(symbol: str | Type[SymbolInfo])
-```
-Checks to see if a symbol is available for a trading account
-#### Parameters:
-| Name | Type | Description |
-|----------|---------------------|--------------------------------------|
-| `symbol` | `str`\|`SymbolInfo` | A symbol name or SymbolInfo instance |
-#### Returns:
-| Type | Description |
-|--------|----------------------------------------|
-| `bool` | True if symbol is available else False |
-
-
-### symbols_get
-```python
-async def symbols_get() -> set[SymbolInfo]
-```
-Get all financial instruments from the MetaTrader 5 terminal available for the current account.
-#### Returns:
-| Type | Description |
-|-------------------|-------------------------------|
-| `set[SymbolInfo]` | A set of SymbolInfo instances |
diff --git a/docs/lib/bot.md b/docs/lib/bot.md
new file mode 100644
index 0000000..8415289
--- /dev/null
+++ b/docs/lib/bot.md
@@ -0,0 +1,153 @@
+# Bot
+
+## Table of Contents
+- [Bot](#bot.Bot)
+- [\_\_init\_\_](#bot.init)
+- [initialize](#bot.initialize)
+- [execute](#bot.execute)
+- [start](#bot.start)
+- [add_coroutine](#bot.add_coroutine)
+- [add_function](#bot.add_function)
+- [add_strategy](#bot.add_strategy)
+- [add_strategies](#bot.add_strategies)
+- [add_strategy_all](#bot.add_strategy_all)
+- [process_pool](#bot.run_bots)
+
+
+### Bot
+```python
+class Bot
+```
+"""The bot class. Create a bot instance to run strategies.
+
+#### Attributes.
+| Name | Type | Description | Default |
+|--------------|--------------------|--------------------------------------------|--------------|
+| `account` | `Account` | Account Object. | None |
+| `executor` | `Executor` | The executor. | None |
+| `strategies` | `List[Strategies]` | A list of strategies to initialize and run | list() |
+| `mt5` | `MetaTrader` | `A MetaTrader Instance` | MetaTrader() |
+| `config` | `Config` | A Config instance | Config() |
+
+
+### \_\_init\_\_
+```python
+def __init__()
+```
+Initializes the Bot class.
+
+
+### initialize
+```python
+async def initialize(self)
+```
+Prepares the bot by signing in to the trading account and initializing the symbols for each strategy.
+Only strategies with successfully initialized symbols will be added to the executor. Starts the global task queue.
+
+Note: *initialize_sync* is a synchronous version of this method.
+
+#### Raises:
+| Exception | Description |
+|--------------|-------------------------------|
+| `SystemExit` | If sign in was not successful |
+
+
+
+
+### execute
+```python
+def execute()
+```
+Executes the bot. Use this method to run the bot in a synchronous manner.
+This method is blocking and will not return until the bot is done running.
+
+
+### start
+```python
+async def start()
+```
+Initialize the bot and execute it. Similar to calling **execute** method but is asynchronous.
+
+
+### add_coroutine
+```python
+def add_coroutine(self, coroutine: Coroutine, on_separate_thread=False, **kwargs)
+```
+Add a coroutine to the executor. By default, all coroutines added to the executor run on this same thread,
+using _asyncio.gather_, but if _on_separate_thread_ is true then the coroutine is given it's own thread.
+
+#### Parameters:
+| Name | Type | Description |
+|----------------------|-------------|----------------------------------------------------|
+| `coroutine` | `Coroutine` | A coroutine to run in the executor |
+| `on_separate_thread` | `bool` | Run coroutine on a separate thread in the executor |
+| `kwargs` | `Any` | Keyword arguments to pass to the coroutine |
+
+
+### add_function
+```python
+def add_function(self, function: Callable, **kwargs)
+```
+Add a function to the executor.
+#### Parameters:
+| Name | Type | Description |
+|------------|------------|-------------------------------------------|
+| `function` | `Callable` | A function to run in the executor |
+| `kwargs` | `Any` | Keyword arguments to pass to the function |
+
+
+### add_strategy
+```python
+def add_strategy(self, strategy: Strategy)
+```
+Add a strategy to the list of strategies.
+
+#### Parameters:
+| Name | Type | Description |
+|------------|------------|-----------------------------------|
+| `strategy` | `Strategy` | A Strategy instance to run on bot |
+
+
+### add_strategies
+```python
+def add_strategies(strategies: Iterable[Strategy])
+```
+Add multiple strategies at the same time
+#### Parameters:
+| Name | Type | Description |
+|--------------|----------------------|-----------------------------------|
+| `strategies` | `Iterable[Strategy]` | An iterable of Strategy instances |
+
+
+### add_strategy_all
+```python
+def add_strategy_all(*, strategy: Type[Strategy], params: dict | None = None, symbols: list[Symbol] = None, **kwargs)
+```
+Use this to run a single strategy on multiple symbols with the same parameters and keyword arguments.
+#### Parameters:
+| Name | Type | Description |
+|------------|------------------|---------------------------------------------|
+| `strategy` | `Type[Strategy]` | A Strategy class |
+| `params` | `dict` or `None` | A dictionary of parameters for the strategy |
+| `symbols` | `list[Symbol]` | A list of symbols to run the strategy on |
+| `**kwargs` | `Any` | Keyword arguments |
+
+
+
+```python
+@classmethod
+def process_pool(cls, bots: dict[Callable: dict] = None, num_workers: int = None):
+```
+Run multiple functions (scripts, bots) at the same time in parallel with different accounts.
+Running multiple functions is useful when you want to run different strategies on different accounts.
+The callable can for example be a bot instance that defines its own Config instance within the function scope.
+The dictionary should contain the callable as the key and the dictionary of keyword arguments to pass to the callable as
+the value. Use the path attribute of the config instance to specify the terminal path of each account.
+The num_workers parameter specifies the number of workers to use. If not specified, the number of workers will be the
+number of bots.
+
+#### Parameters
+| Name | Type | Description |
+|--------------|------------------------|---------------------------------------------------------------------------------|
+| `bots` | `dict[Callable: dict]` | A dictionary of callables and their keyword arguments to run as bots |
+| `num_workers` | `int` | The number of workers to use. If not specified, the number of bots will be used |
diff --git a/docs/lib/bot_builder.md b/docs/lib/bot_builder.md
deleted file mode 100644
index e7bdfed..0000000
--- a/docs/lib/bot_builder.md
+++ /dev/null
@@ -1,162 +0,0 @@
-# Bot
-
-## Table of Contents
-- [Bot](#bb.Bot)
-- [\_\_init\_\_](#bb.__init__)
-- [initialize](#bb.initialize)
-- [execute](#bb.execute)
-- [start](#bb.start)
-- [add_coroutine](#bb.add_coroutine)
-- [add_function](#bb.add_function)
-- [add_strategy](#bb.add_strategy)
-- [add_strategies](#bb.add_strategies)
-- [add_strategy_all](#bb.add_strategy_all)
-- [init_symbols](#bb.init_symbols)
-- [init_symbol](#bb.init_symbol]())
-- [run_bots](#bb.run_bots)
-
-
-### Bot
-```python
-class Bot
-```
-The bot class. Create a bot instance to run your strategies.
-#### Attributes:
-| Name | Type | Description | Default |
-|------------|----------------------|------------------------------------------|----------|
-| `account` | `Account` | Account Object. | None |
-| `executor` | `ThreadPoolExecutor` | The default thread executor. | None |
-| `symbols` | `set[Symbols]` | A set of symbols for the trading session | set() |
-| `config` | `Config` | A Config instance | Config() |
-
-
-### \_\_init\_\_
-```python
-def __init__()
-```
-Initializes the Bot class.
-
-
-### initialize
-```python
-async def initialize()
-```
-Prepares the bot by signing in to the trading account and initializing the symbols for the trading session.
-#### Raises:
-| Exception | Description |
-|--------------|-------------------------------|
-| `SystemExit` | If sign in was not successful |
-
-
-### execute
-```python
-def execute()
-```
-Execute the bot. Use this method to run the bot.
-
-
-### start
-```python
-async def start()
-```
-Initialize the bot and execute it. Similar to calling **execute** method but is asynchronous.
-
-
-### add_coroutine
-```python
-def add_coroutine(coro: Coroutine, **kwargs)
-```
-Add a coroutine to the executor.
-#### Parameters:
-| Name | Type | Description |
-|----------|-------------|--------------------------------------------|
-| `coro` | `Coroutine` | A coroutine to run in the executor |
-| `kwargs` | `Any` | Keyword arguments to pass to the coroutine |
-
-
-### add_function
-```python
-def add_function(func: Callable, **kwargs)
-```
-Add a function to the executor.
-#### Parameters:
-| Name | Type | Description |
-|----------|------------|-------------------------------------------|
-| `func` | `Callable` | A function to run in the executor |
-| `kwargs` | `Any` | Keyword arguments to pass to the function |
-
-
-### add_strategy
-```python
-def add_strategy(strategy: Strategy)
-```
-Add a strategy to the executor. An added strategy will only run if it's symbol was successfully initialized.
-#### Parameters:
-| Name | Type | Description |
-|------------|------------|-----------------------------------|
-| `strategy` | `Strategy` | A Strategy instance to run on bot |
-
-
-### add_strategies
-```python
-def add_strategies(strategies: Iterable[Strategy])
-```
-Add multiple strategies at the same time
-#### Parameters:
-| Name | Type | Description |
-|--------------|----------------------|-----------------------------------|
-| `strategies` | `Iterable[Strategy]` | An iterable of Strategy instances |
-
-
-### add_strategy_all
-```python
-def add_strategy_all(*, strategy: Type[Strategy], params: dict | None = None)
-```
-Use this to run a single strategy on all available instruments in the market using the default parameters
-i.e. one set of parameters for all trading symbols
-#### Parameters:
-| Name | Type | Description |
-|------------|------------------|---------------------------------------------|
-| `strategy` | `Type[Strategy]` | A Strategy class |
-| `params` | `dict` or `None` | A dictionary of parameters for the strategy |
-
-
-### init_symbols
-```python
-async def init_symbols()
-```
-Initialize the symbols for the current trading session. This method is called internally by the bot.
-
-
-### init_symbol
-```python
-async def init_symbol(symbol: Symbol) -> Symbol
-```
-Initialize a symbol before the beginning of a trading session.
-Removes it from the list of symbols if it was not successfully initialized or not available for the account.
-#### Parameters:
-| Name | Type | Description |
-|----------|----------|-------------------|
-| `symbol` | `Symbol` | A Symbol instance |
-#### Returns:
-| Type | Description |
-|----------|-------------------|
-| `Symbol` | A Symbol instance |
-
-
-```python
-@classmethod
-def run_bots(cls, funcs: dict[Callable: dict] = None, num_workers: int = None):
-```
-Run multiple functions (scripts, bots) at the same time in parallel with different accounts.
-Running multiple functions is useful when you want to run different strategies on different accounts.
-The callable can for example be a bot instance that defines its own Config instance within the function scope.
-The dictionary should contain the callable as the key and the dictionary of keyword arguments to pass to the callable as
-the value. Use the path attribute of the config instance to specify the terminal path of each account.
-The num_workers parameter specifies the number of workers to use. If not specified, the number of workers will be the
-number of bots.
-#### Parameters
-| Name | Type | Description |
-|---------------|------------------------|---------------------------------------------------------------------------------|
-| `funcs` | `dict[Callable: dict]` | A dictionary of callables and their keyword arguments to run as bots |
-| `num_workers` | `int` | The number of workers to use. If not specified, the number of bots will be used |
diff --git a/src/aiomql/core/backtesting/backtest_engine.py b/src/aiomql/core/backtesting/backtest_engine.py
index 410b504..9b14ee1 100644
--- a/src/aiomql/core/backtesting/backtest_engine.py
+++ b/src/aiomql/core/backtesting/backtest_engine.py
@@ -3,7 +3,6 @@ import asyncio
import json
from datetime import datetime, UTC
from typing import Literal
-from itertools import zip_longest
import random
from functools import cached_property
from logging import getLogger
@@ -60,7 +59,7 @@ class BackTestEngine:
range: range
speed: int
cursor: Cursor
- iter: zip_longest
+ iter: zip
rates: dict[str, dict[int, DataFrame]]
ticks: dict[str, DataFrame]
prices: dict[str, DataFrame]
@@ -76,7 +75,6 @@ class BackTestEngine:
preloaded_ticks: dict[str, DataFrame]
preload: bool
account_lock: RLock
- use_termial_in_tracker: bool
def __init__(
self,
@@ -89,10 +87,9 @@ class BackTestEngine:
use_terminal: bool = None,
name: str = "",
stop_time: float | datetime = None,
- close_open_positions_on_exit: bool = False,
+ close_open_positions_on_exit: bool = True,
preload=True,
assign_to_config: bool = False,
- use_termial_in_tracker: bool = False,
):
self._data = data or BackTestData()
self.mt5 = MetaTrader()
@@ -171,11 +168,14 @@ class BackTestEngine:
self.speed = speed
self.span = range(span_start, span_end, speed)
self.range = range(0, span_end - span_start, speed)
- self.iter = zip_longest(self.range, self.span)
+ self.iter = zip(self.range, self.span)
if restart is False and self._data.cursor is not None:
self.cursor = self._data.cursor
- self.go_to(time=self.cursor.time)
+ new_range = range(self.range[self.cursor.index], self.range.stop)
+ new_span = range(self.span[self.cursor.index], self.span.stop)
+ self.iter = zip(new_range, new_span)
+ # self.go_to(time=self.cursor.time)
else:
self.cursor = Cursor(index=self.range.start, time=self.span.start)
@@ -220,7 +220,7 @@ class BackTestEngine:
return self._data
def reset(self, clear_data: bool = False):
- self.iter = zip_longest(self.range, self.span)
+ self.iter = zip(self.range, self.span)
self.cursor = Cursor(index=self.range.start, time=self.span.start)
if clear_data:
self.setup_data(restart=True)
@@ -234,6 +234,7 @@ class BackTestEngine:
time = int(time.timestamp())
steps = time - self.cursor.time
steps = steps // self.speed
+ steps = max(steps, 1)
if 0 <= steps < (len(self.range) - 1):
self.fast_forward(steps=steps)
return
diff --git a/src/aiomql/core/meta_trader.py b/src/aiomql/core/meta_trader.py
index 40f8210..d99b5d2 100644
--- a/src/aiomql/core/meta_trader.py
+++ b/src/aiomql/core/meta_trader.py
@@ -17,6 +17,7 @@ from MetaTrader5 import (
OrderSendResult,
OrderCheckResult,
)
+import MetaTrader5 as mt5
from .constants import OrderType, CopyTicks
@@ -102,6 +103,33 @@ class MetaTrader(MetaCore):
self._login, login, password=password, server=server, timeout=timeout
)
+ def login_sync(
+ self,
+ *,
+ login: int = 0,
+ password: str = "",
+ server: str = "",
+ timeout: int = 60000,
+ ) -> bool:
+ """
+ Connects to the MetaTrader terminal using the specified login, password and server.
+
+ Args:
+ login (int): The trading account number.
+ password (str): The trading account password.
+ server (str): The trading server name.
+ timeout (int): The timeout for the connection in seconds.
+
+ Returns:
+ bool: True if successful, False otherwise.
+ """
+ acc_details = self.config.account_info()
+ login = login or acc_details.get("login", 0)
+ password = password or acc_details.get("password", "")
+ server = server or acc_details.get("server", "")
+ res = mt5.login(login, password=password, server=server, timeout=timeout)
+ return res
+
async def initialize(
self,
path: str = None,
@@ -150,6 +178,52 @@ class MetaTrader(MetaCore):
self.error = Error(*err)
return res
+ def initialize_sync(
+ self,
+ path: str = None,
+ login: int = 0,
+ password: str = "",
+ server: str = "",
+ timeout: int | None = None,
+ portable=False,
+ ) -> bool:
+ """
+ Initializes the connection to the MetaTrader terminal. All parameters are optional.
+
+ Keyword Args:
+ path (str): The path to the MetaTrader terminal executable.
+ login (int): The trading account number.
+ password (str): The trading account password.
+ server (str): The trading server name.
+ timeout (int): The timeout for the connection in milliseconds.
+ portable (bool): If True, the terminal will be launched in portable mode.
+
+ Returns:
+ bool: True if successful, False otherwise.
+ """
+ path = self.config.path if path is None else path
+ path = "" if Path(path).exists() is False else path
+ args = (str(path),) if path else ()
+ acc = self.config.account_info()
+ kwargs = {
+ key: value
+ for key, value in (
+ ("login", login or acc.get("login")),
+ ("password", password or acc.get("password")),
+ ("server", server or acc.get("server")),
+ ("timeout", timeout or 60000),
+ ("portable", portable),
+ )
+ if key is not None
+ }
+ res = mt5.initialize(*args, **kwargs)
+ if res is False:
+ self._shutdown()
+ if not res:
+ err = self._last_error()
+ self.error = Error(*err)
+ return res
+
async def shutdown(self) -> None:
"""Closes the connection to the MetaTrader terminal."""
self._shutdown()
diff --git a/src/aiomql/lib/account.py b/src/aiomql/lib/account.py
index a5da45d..41897ce 100644
--- a/src/aiomql/lib/account.py
+++ b/src/aiomql/lib/account.py
@@ -9,16 +9,12 @@ logger = getLogger(__name__)
class Account(_Base, AccountInfo):
- """A class for managing a trading account. A singleton class.
- A subclass of AccountInfo. All AccountInfo attributes are available in this class.
+ """A singleton class for managing a trading account. A subclass of _Base and AccountInfo. It supports
+ Asynchronous context management protocol.
Attributes:
connected (bool): Status of connection to MetaTrader 5 Terminal
-
- Notes:
- Other Account properties are defined in the AccountInfo class.
"""
-
_instance: Self
connected: bool
diff --git a/src/aiomql/lib/backtester.py b/src/aiomql/lib/backtester.py
index c893618..616dffc 100644
--- a/src/aiomql/lib/backtester.py
+++ b/src/aiomql/lib/backtester.py
@@ -100,7 +100,7 @@ class BackTester:
async def start(self):
"""Initialize the bot and execute it. Similar to calling `execute` method but is a coroutine."""
await self.initialize()
- await self.executor.execute()
+ self.executor.execute()
def add_strategy(self, *, strategy: Strategy):
"""Add a strategy to the list of strategies.
diff --git a/src/aiomql/lib/bot.py b/src/aiomql/lib/bot.py
index d101bc7..c10e678 100644
--- a/src/aiomql/lib/bot.py
+++ b/src/aiomql/lib/bot.py
@@ -7,21 +7,20 @@ from .executor import Executor
from ..core.config import Config
from ..core.meta_trader import MetaTrader
from .symbol import Symbol as Symbol
+from .ticks import Tick
from .strategy import Strategy as Strategy
logger = logging.getLogger(__name__)
class Bot:
- """The bot class. Create a bot instance to run your strategies.
+ """The bot class. Creates a bot instance to run strategies.
Attributes:
- executor: The default thread executor.
+ executor: A thread executor.
config (Config): Config instance
mt (MetaTrader): MetaTrader instance
-
"""
-
config: Config
executor: Executor
mt: MetaTrader
@@ -30,32 +29,33 @@ class Bot:
def __init__(self):
self.config = Config(bot=self)
self.executor = Executor()
- self.mt = MetaTrader()
+ self.mt5 = MetaTrader()
self.strategies = []
@classmethod
- def run_bots(cls, funcs: dict[Callable:dict] = None, num_workers: int = None):
- """Run multiple scripts or bots in parallel with different accounts.
+ def process_pool(cls, bots: dict[Callable:dict] = None, num_workers: int = None):
+ """Run multiple bots in parallel using a ProcessPoolExecutor. Each bot should be a callable that accepts
+ keyword arguments only.
Args:
- funcs (dict): A dictionary of functions to run with their respective keyword arguments as a dictionary
- num_workers (int): Number of workers to run the functions
+ bots (dict): A dictionary of bots to run with their respective keyword arguments as a dictionary
+ num_workers (int): Number of workers to run the bots
"""
- num_workers = num_workers or len(funcs)
+ num_workers = num_workers or len(bots) + 1
with ProcessPoolExecutor(max_workers=num_workers) as executor:
- for bot, kwargs in funcs.items():
+ for bot, kwargs in bots.items():
executor.submit(bot, **kwargs)
async def initialize(self):
- """Prepares the bot by signing in to the trading account and initializing the symbols for the trading session.
- Starts the global task queue.
+ """Prepares the bot by signing in to the trading account and initializing the symbols for each strategy.
+ Only strategies with successfully initialized symbols will be added to the executor. Starts the global task queue.
Raises:
SystemExit if sign in was not successful
"""
try:
- await self.mt.initialize()
- login = await self.mt.login()
+ await self.mt5.initialize()
+ login = await self.mt5.login()
if not login:
logger.critical("Unable to sign in to MetaTrder 5 Terminal")
raise Exception("Unable to sign in to MetaTrader 5 Terminal")
@@ -72,12 +72,38 @@ class Bot:
logger.error("%s: Bot initialization failed", err)
raise SystemExit
- def add_function(self, *, function: Callable[..., ...], **kwargs: dict):
+ def initialize_sync(self):
+ """Prepares the bot by signing in to the trading account and initializing the symbols for each strategy.
+ Only strategies with successfully initialized symbols will be added to the executor. Starts the global task queue.
+
+ Raises:
+ SystemExit if sign in was not successful
+ """
+ try:
+ self.mt5.initialize_sync()
+ login = self.mt5.login_sync()
+ if not login:
+ logger.critical("Unable to sign in to MetaTrder 5 Terminal")
+ raise Exception("Unable to sign in to MetaTrader 5 Terminal")
+ logger.info("Login Successful")
+ self.init_strategies_sync()
+ self.add_coroutine(
+ coroutine=self.config.task_queue.run, on_separate_thread=True
+ )
+ self.add_coroutine(coroutine=self.executor.exit)
+
+ if len(self.executor.strategy_runners) == 0:
+ logger.warning("No strategies were added to the bot")
+ except Exception as err:
+ logger.error("%s: Bot initialization failed", err)
+ raise SystemExit
+
+ def add_function(self, *, function: Callable[..., ...], **kwargs):
"""Add a function to the executor.
Args:
function (Callable): A function to be executed
- **kwargs (dict): Keyword arguments for the function
+ **kwargs: Keyword arguments for the function
"""
self.executor.add_function(function=function, kwargs=kwargs)
@@ -103,17 +129,17 @@ class Bot:
)
def execute(self):
- """Execute the bot."""
- asyncio.run(self.start())
+ """Execute the bot using asyncio.run"""
+ self.initialize_sync()
+ self.executor.execute()
async def start(self):
- """Initialize the bot and execute it. Similar to calling `execute` method but is a coroutine."""
+ """Initialize the bot and call the executor it."""
await self.initialize()
- await self.executor.execute()
+ self.executor.execute()
def add_strategy(self, *, strategy: Strategy):
"""Add a strategy to the list of strategies.
- An added strategy will only run if it's symbol was successfully initialized and it is added to the executor.
Args:
strategy (Strategy): A Strategy instance to run on bot
@@ -163,3 +189,28 @@ class Bot:
"""Initialize the symbols for the current trading session. This method is called internally by the bot."""
tasks = [self.init_strategy(strategy=strategy) for strategy in self.strategies]
await asyncio.gather(*tasks)
+
+ def init_strategy_sync(self, *, strategy: Strategy) -> bool:
+ """Initialize a single strategy. This method is called internally by the bot."""
+ try:
+ select = self.mt5._symbol_select(strategy.symbol.name, True)
+ info = self.mt5._symbol_info(strategy.symbol.name)
+ tick = self.mt5._symbol_info_tick(strategy.symbol.name)
+ self.mt5._market_book_add(strategy.symbol.name)
+ if info is not None and tick is not None:
+ info = info._asdict()
+ info["swap_rollover3days"] = info.get("swap_rollover3days", 0) % 7
+ info['select'] = select
+ tick = Tick(**tick._asdict())
+ strategy.symbol.tick = tick
+ strategy.symbol.set_attributes(**info)
+ self.executor.add_strategy(strategy=strategy)
+ return True
+ return False
+ except Exception as err:
+ logger.warning("%s: Unable to initialize strategy", err)
+ return False
+
+ def init_strategies_sync(self):
+ """Initialize the symbols for the current trading session. This method is called internally by the bot."""
+ [self.init_strategy_sync(strategy=strategy) for strategy in self.strategies]
diff --git a/src/aiomql/lib/executor.py b/src/aiomql/lib/executor.py
index 9d6e6f6..3391abc 100644
--- a/src/aiomql/lib/executor.py
+++ b/src/aiomql/lib/executor.py
@@ -132,7 +132,7 @@ class Executor:
except Exception as err:
logger.error(f"Error: {err}. Unable to shutdown executor")
- async def execute(self, *, workers: int = 5):
+ def execute(self, *, workers: int = 5):
"""Run the strategies with a threadpool executor.
Args:
diff --git a/src/aiomql/lib/strategy.py b/src/aiomql/lib/strategy.py
index b6f2ec0..2af312b 100644
--- a/src/aiomql/lib/strategy.py
+++ b/src/aiomql/lib/strategy.py
@@ -123,6 +123,7 @@ class Strategy(ABC):
secs = secs - mod if mod != 0 else mod
if self.backtest_controller.parties == 2:
steps = int(secs) // self.config.backtest_engine.speed
+ steps = max(steps, 1)
self.config.backtest_engine.fast_forward(steps=steps)
self.backtest_controller.wait()
diff --git a/src/aiomql/lib/symbol.py b/src/aiomql/lib/symbol.py
index f7dbbaf..2f3a8b9 100644
--- a/src/aiomql/lib/symbol.py
+++ b/src/aiomql/lib/symbol.py
@@ -41,7 +41,6 @@ class Symbol(_Base, SymbolInfo):
super().__init__(**kwargs)
self.account = Account()
- # @backoff_decorator
async def info_tick(self, *, name: str = "") -> Tick | None:
"""Get the current price tick of a financial instrument.
@@ -77,7 +76,6 @@ class Symbol(_Base, SymbolInfo):
self.select = await self.mt5.symbol_select(self.name, enable)
return self.select
- # @backoff_decorator
async def info(self) -> SymbolInfo | None:
"""Get data on the specified financial instrument and update the symbol object properties
@@ -104,7 +102,7 @@ class Symbol(_Base, SymbolInfo):
info = await self.info()
info_tick = await self.info_tick()
await self.book_add()
- if all((info is not None, info_tick is not None)):
+ if info is not None and info_tick is not None:
return True
logger.warning("Unable to initialize %s", self.name)
return False
diff --git a/tests/live/integration/test_bot.py b/tests/live/integration/test_bot.py
index 1e72d08..c03203b 100644
--- a/tests/live/integration/test_bot.py
+++ b/tests/live/integration/test_bot.py
@@ -18,7 +18,7 @@ async def test_bot():
bot.executor.timeout = 10
bot.add_strategies(strategies=strategies)
await bot.initialize()
- await bot.executor.execute()
+ bot.executor.execute()
assert len(bot.executor.coroutines) == 1
assert len(bot.executor.coroutine_threads) == 1
assert bot.config.shutdown is True
diff --git a/tests/live/integration/test_bot_sync.py b/tests/live/integration/test_bot_sync.py
new file mode 100644
index 0000000..34b6ea3
--- /dev/null
+++ b/tests/live/integration/test_bot_sync.py
@@ -0,0 +1,25 @@
+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") for symbol in symbols]
+ bot = Bot()
+ assert bot.config.shutdown is False
+ 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
+ assert bot.config.shutdown is True
diff --git a/tests/live/unit/test_bot_and_executor.py b/tests/live/unit/test_bot_and_executor.py
index 925fb4a..7167077 100644
--- a/tests/live/unit/test_bot_and_executor.py
+++ b/tests/live/unit/test_bot_and_executor.py
@@ -9,6 +9,7 @@ class TestBotFactoryAndExecutor:
@classmethod
def setup_class(cls):
cls.bot = Bot()
+ cls.sync_bot = Bot()
@pytest.fixture(scope="class", autouse=True)
async def initialize(self):
@@ -18,6 +19,14 @@ class TestBotFactoryAndExecutor:
self.bot.add_coroutine(coroutine=self.coro_thread, on_separate_thread=True)
await self.bot.initialize()
+ @pytest.fixture(scope="class", autouse=True)
+ def initialize_sync(self):
+ self.sync_bot.add_coroutine(coroutine=self.coro_one)
+ self.sync_bot.add_coroutine(coroutine=self.coro_two)
+ self.sync_bot.add_function(function=self.fun_one)
+ self.sync_bot.add_coroutine(coroutine=self.coro_thread, on_separate_thread=True)
+ self.sync_bot.initialize_sync()
+
@staticmethod
def fun_one():
print("function one")
@@ -46,4 +55,8 @@ class TestBotFactoryAndExecutor:
# task_queue already added coroutine_thread
assert len(self.bot.executor.coroutine_threads) == 2
- # def
+ def test_sync_add_workers(self):
+ assert len(self.sync_bot.executor.coroutines) == 3
+ assert len(self.sync_bot.executor.functions) == 1
+ # task_queue already added coroutine_thread
+ assert len(self.sync_bot.executor.coroutine_threads) == 2