From d9bd84bc2ed82f169f18f6861349b6b365eabf5d Mon Sep 17 00:00:00 2001 From: Ichinga Samuel Date: Fri, 8 Nov 2024 05:41:23 +0100 Subject: [PATCH] v4 --- docs/lib/account.md | 83 +-------- docs/lib/bot.md | 153 +++++++++++++++++ docs/lib/bot_builder.md | 162 ------------------ .../core/backtesting/backtest_engine.py | 17 +- src/aiomql/core/meta_trader.py | 74 ++++++++ src/aiomql/lib/account.py | 8 +- src/aiomql/lib/backtester.py | 2 +- src/aiomql/lib/bot.py | 95 +++++++--- src/aiomql/lib/executor.py | 2 +- src/aiomql/lib/strategy.py | 1 + src/aiomql/lib/symbol.py | 4 +- tests/live/integration/test_bot.py | 2 +- tests/live/integration/test_bot_sync.py | 25 +++ tests/live/unit/test_bot_and_executor.py | 15 +- 14 files changed, 361 insertions(+), 282 deletions(-) create mode 100644 docs/lib/bot.md delete mode 100644 docs/lib/bot_builder.md create mode 100644 tests/live/integration/test_bot_sync.py 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