diff --git a/docs/_utils.md b/docs/_utils.md
index 126e984..566e4ed 100644
--- a/docs/_utils.md
+++ b/docs/_utils.md
@@ -2,6 +2,7 @@
Utils is a collection of utility functions that are used throughout the codebase. It is a collection of functions.
## Table of Contents
+- [backtest_sleep](#_utils.backtest_sleep)
- [round_off](#_utiils.round_off)
- [dict_to_string](#_utils.dict_to_string)
- [round_down](#_utils.round_down)
@@ -126,3 +127,15 @@ A decorator to handle exceptions in an async function.
def error_handler_sync(func=None, *, msg="", exe=Exception, response=None, log_error_msg=True) -> Callable:
```
A decorator to handle exceptions in a sync function.
+
+
+### backtest_sleep
+```python
+def backtest_sleep(seconds: float) -> None:
+```
+Sleeps for a given number of seconds in backtest mode.
+
+#### Parameters:
+| Name | Type | Description |
+|---------|-------|----------------------------------|
+| seconds | float | The number of seconds to sleep. |
diff --git a/docs/lib/order.md b/docs/lib/order.md
index 3630bee..72f8af1 100644
--- a/docs/lib/order.md
+++ b/docs/lib/order.md
@@ -12,6 +12,7 @@
- [calc_profit](#order.calc_profit)
- [calc_loss](#order.calc_loss)
- [request](#order.request)
+- [modify](#order.modify)
### Order
@@ -160,3 +161,11 @@ Return the trade request object as a dict
| Type | Description |
|--------|----------------------------------|
| `dict` | Returns the trade request object |
+
+
+
+### modify
+```python
+def modify(**kwargs)
+```
+Modify the order object with keyword arguments.
diff --git a/docs/lib/positions.md b/docs/lib/positions.md
index 2fb307b..d3da353 100644
--- a/docs/lib/positions.md
+++ b/docs/lib/positions.md
@@ -10,6 +10,7 @@
- [close_position_by_ticket](#positions.close_position_by_ticket)
- [close_position](#positions.close_position)
- [close_all](#positions.close_all)
+- [get_total_positions](#positions.get_total_positions)
### Positions
@@ -19,10 +20,11 @@ class Positions
Get and handle Open positions.
#### Attributes
-| Name | Type | Description |
-|-------------|-----------------------------|----------------------------|
-| `positions` | `tuple[TradePosition, ...]` | Financial instrument name. |
-| `mt5` | `MetaTrader` | MetaTrader instance. |
+| Name | Type | Description |
+|-------------|-----------------------------|-------------------------------------------------------------------------------------|
+| `positions` | `tuple[TradePosition, ...]` | Financial instrument name. |
+| `mt5` | `MetaTrader` | MetaTrader instance. |
+|`total_positions`| `int` | Total number of open positions. Can be set in `get_positions` or `get_total_positions`. |
### \_\_init\_\_
@@ -35,11 +37,19 @@ Initialize a position instance
### get_positions
```python
-async def get_positions(self) -> tuple[TradePosition, ...]:
+async def get_positions(*, symbol: str = None, ticket: int = None, group: str = None) -> tuple[TradePosition, ...]:
```
-Get open positions
+Get open positions, with the ability to filter by symbol, ticket, or group.
-#### Returns
+#### Parameters:
+| Name | Type | Description |
+|----------|-------|-----------------|
+| `symbol` | `str` | Symbol |
+| `ticket` | `int` | Position ticket |
+| `group` | `str` | Group name |
+
+
+#### Returns:
| Type | Description |
|-----------------------------|--------------------------------|
| `tuple[TradePosition, ...]` | A list of open trade positions |
@@ -150,3 +160,16 @@ Close all open positions for the trading account.
| Type | Description |
|-------|--------------------------------------|
| `int` | Return total number of closed trades |
+
+
+
+### get_total_positions
+```python
+async def get_total_positions() -> int
+```
+Get the total number of open positions and set the `total_positions` attribute.
+
+#### Returns:
+| Type | Description |
+|-------|--------------------------------------|
+| `int` | Return total number of open trades |
diff --git a/docs/lib/ram.md b/docs/lib/ram.md
index 2ab19eb..34fe951 100644
--- a/docs/lib/ram.md
+++ b/docs/lib/ram.md
@@ -6,6 +6,7 @@
- [get_amount](#ram.get_amount)
- [check_losing_positions](#ram.check_losing_positions)
- [check_open_positions](#ram.check_open_positions)
+- [modify_ram](#ram.modify_ram)
### RAM
@@ -75,3 +76,11 @@ Check if the number of open positions is less than or equal the loss limit.
| Type | Description |
|--------|---------------------------------------------------------------------------------------|
| `bool` | True if the number of open losing trades is more than the loss limit, False otherwise |
+
+
+
+### modify_ram
+```python
+def modify_ram(**kwargs):
+```
+Modify the RAM attributes. All provided keyword arguments are set as attributes.
diff --git a/examples/backtesting/backtest_data_01_05_24_06_05_24.json b/examples/backtesting/backtest_data_01_05_24_06_05_24.json
new file mode 100644
index 0000000..d399abf
--- /dev/null
+++ b/examples/backtesting/backtest_data_01_05_24_06_05_24.json
@@ -0,0 +1,17 @@
+{
+ "balance": 504.26,
+ "profit": 0,
+ "equity": 504.26,
+ "margin": 0.0,
+ "margin_free": 504.26,
+ "margin_level": 0,
+ "wins": 12,
+ "losses": 15,
+ "total": 27,
+ "win_percentage": 44.44,
+ "win": 326.55,
+ "loss": -172.29,
+ "net_profit": 154.26,
+ "profit_factor": 1.9,
+ "profitability": 44.07
+}
\ No newline at end of file
diff --git a/examples/sample_backtester.py b/examples/sample_backtester.py
index 6dc3232..b204fee 100644
--- a/examples/sample_backtester.py
+++ b/examples/sample_backtester.py
@@ -3,7 +3,7 @@ from datetime import datetime, UTC
from aiomql.lib.backtester import BackTester
from aiomql.core import Config
-from aiomql.contrib.strategies import FingerTrap
+from aiomql.contrib.strategies import FingerTrap, Chaos
from aiomql.contrib.symbols import ForexSymbol
from aiomql.core.backtesting import BackTestEngine
@@ -17,7 +17,7 @@ def back_tester():
start = datetime(2024, 5, 1, tzinfo=UTC)
stop_time = datetime(2024, 5, 2, tzinfo=UTC)
end = datetime(2024, 5, 7, tzinfo=UTC)
- back_test_engine = BackTestEngine(start=start, end=end, speed=3600,
+ back_test_engine = BackTestEngine(start=start, end=end, speed=7200,
close_open_positions_on_exit=True, assign_to_config=True, preload=True,
account_info={"balance": 350})
backtester = BackTester(backtest_engine=back_test_engine)
diff --git a/pyproject.toml b/pyproject.toml
index 4dc2ff2..26c5b9e 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "aiomql"
-version = "4.0.4"
+version = "4.0.5"
readme = "README.md"
requires-python = ">=3.11"
classifiers = [
@@ -14,7 +14,7 @@ classifiers = [
]
keywords = ["MetaTrader5", "Asynchronous", "Algorithmic Trading", "Trading Bot", "Backtesting",
- "Technical Analysis", "Forex", "Stocks", "Cryptocurrency", "Futures", "Options"]
+ "Technical Analysis", "Forex", "Stocks", "Cryptocurrency", "Futures", "Options", "Crypto", "Algo Trading"]
dependencies = ["MetaTrader5>=5.0.37", "pandas>=1.5.0", "pandas-ta>=0.3.14b0"]
diff --git a/requirements.txt b/requirements.txt
index 01caf1d..ac3ed1a 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,4 +1,3 @@
--e git+https://github.com/Ichinga-Samuel/aiomql.git@3143ca9936e2f791798b4944bf0df7225c4c6a78#egg=aiomql
anyio==4.3.0
argon2-cffi==23.1.0
argon2-cffi-bindings==21.2.0
diff --git a/src/aiomql/_utils.py b/src/aiomql/_utils.py
index 23f1763..16e61c2 100644
--- a/src/aiomql/_utils.py
+++ b/src/aiomql/_utils.py
@@ -10,7 +10,35 @@ from .core.config import Config
logger = getLogger(__name__)
-config = Config()
+
+async def backtest_sleep(secs: float):
+ config = Config()
+ btc = config.backtest_controller
+ try:
+ if btc.parties == 2:
+ steps = int(secs) // config.backtest_engine.speed
+ steps = max(steps, 1)
+ config.backtest_engine.fast_forward(steps=steps)
+ btc.wait()
+
+ elif btc.parties > 2:
+ _time = config.backtest_engine.cursor.time + secs
+ while _time > config.backtest_engine.cursor.time:
+ btc.wait()
+ else:
+ btc.wait()
+ except Exception as err:
+ btc.wait()
+ logger.error("Error: %s in backtest_sleep", err)
+
+
+# async def backtest_sleep(secs):
+# """An async sleep function for use during backtesting."""
+# btc = BackTestController()
+# config = Config()
+# sleep = config.backtest_engine.cursor.time + secs
+# while sleep > config.backtest_engine.cursor.time:
+# btc.wait()
def dict_to_string(data: dict, multi=False) -> str:
@@ -28,6 +56,13 @@ def dict_to_string(data: dict, multi=False) -> str:
def backoff_decorator(func=None, *, max_retries: int = 2, retries: int = 0, error="") -> callable:
+ """A decorator to retry a function a number of times before giving up.
+ Args:
+ func (callable, optional): The function to decorate. Defaults to None.
+ max_retries (int, optional): The maximum number of retries. Defaults to 2.
+ retries (int, optional): The number of retries. Defaults to 0.
+ error (Any, optional): The error to raise when the maximum number of retries is reached. Defaults to "".
+ """
if func is None:
return partial(backoff_decorator, max_retries=max_retries, retries=retries, error=error)
@@ -47,7 +82,7 @@ def backoff_decorator(func=None, *, max_retries: int = 2, retries: int = 0, erro
return res
except Exception as err:
logger.error("Error in %s: %s", func.__name__, err)
- if config.mode != "backtest":
+ if Config().mode != "backtest":
await asyncio.sleep(2**retries + random.uniform(0, max_retries))
await wrapper(*args, **kwargs)
@@ -55,6 +90,14 @@ def backoff_decorator(func=None, *, max_retries: int = 2, retries: int = 0, erro
def error_handler(func=None, *, msg="", exe=Exception, response=None, log_error_msg=True):
+ """A decorator to handle errors in an async function.
+ Args:
+ func (callable, optional): The function to decorate. Defaults to None.
+ msg (str, optional): The error message to log. Defaults to "".
+ exe (Exception, optional): The exception to catch. Defaults to Exception.
+ response (Any, optional): The response to return when an error occurs. Defaults to None.
+ log_error_msg (bool, optional): If True, log the error message. Defaults to True.
+ """
if func is None:
return partial(error_handler, msg=msg, exe=exe, response=response, log_error_msg=log_error_msg)
@@ -72,6 +115,15 @@ def error_handler(func=None, *, msg="", exe=Exception, response=None, log_error_
def error_handler_sync(func=None, *, msg="", exe=Exception, response=None, log_error_msg=True):
+ """A decorator to handle errors in a synchronous function.
+
+ Args:
+ func (callable, optional): The function to decorate. Defaults to None.
+ msg (str, optional): The error message to log. Defaults to "".
+ exe (Exception, optional): The exception to catch. Defaults to Exception.
+ response (Any, optional): The response to return when an error occurs. Defaults to None.
+ log_error_msg (bool, optional): If True, log the error message. Defaults to True.
+ """
if func is None:
return partial(error_handler, msg=msg, exe=exe, response=response, log_error_msg=log_error_msg)
@@ -88,7 +140,15 @@ def error_handler_sync(func=None, *, msg="", exe=Exception, response=None, log_e
return wrapper
-def round_down(value: int | float, base: int) -> int:
+def round_down(value: int | float, base: int) -> int | float:
+ """Round down a number to the nearest base.
+ Args:
+ value (int | float): The number to round down.
+ base (int): The base to round down to.
+
+ Returns:
+ (int | float): The rounded down number.
+ """
return int(value) if value % base == 0 else int(value - (value % base))
@@ -98,19 +158,28 @@ def round_up(value: int | float, base: int) -> int:
# noinspection PyShadowingNames
def round_off(value: float, step: float, round_down: bool = False) -> float:
- """Round off a number to the nearest step."""
+ """Round off a number to the nearest step.
+
+ Args:
+ value (float): The number to round off.
+ step (float): The step to round off to.
+ round_down (bool, optional): If True, the number is rounded down otherwise it is rounded up. Defaults to False.
+
+ Returns:
+ float: The rounded off number.
+ """
with decimal.localcontext() as ctx:
ctx.rounding = decimal.ROUND_DOWN if round_down else decimal.ROUND_UP
return float(decimal.Decimal(str(value)).quantize(decimal.Decimal(str(step))))
def async_cache(fun):
+ """A decorator to cache the result of an async function."""
@wraps(fun)
async def wrapper(*args, **kwargs):
key = (args, frozenset(kwargs.items()))
with wrapper.lock:
if key not in wrapper.cache:
- # print(key)
wrapper.cache[key] = await fun(*args, **kwargs)
return wrapper.cache[key]
diff --git a/src/aiomql/core/backtesting/backtest_controller.py b/src/aiomql/core/backtesting/backtest_controller.py
index 34eadf6..637d32a 100644
--- a/src/aiomql/core/backtesting/backtest_controller.py
+++ b/src/aiomql/core/backtesting/backtest_controller.py
@@ -13,7 +13,7 @@ logger = getLogger(__name__)
class BackTestController:
"""The controller for the backtesting engine.
- It also act's as a synchronizier for running multiple strategies (tasks) using a threading.Barrier primitive.
+ It also acts as a synchronizer for running multiple strategies (tasks) using a threading.Barrier primitive.
It handles the updating of open positions and close them when necessary.
It handles the iterator for the backtesting engine and handles it movement in time by moving it to the next time step.
@@ -33,6 +33,7 @@ class BackTestController:
if not hasattr(cls, "_instance"):
cls._instance = super().__new__(cls)
cls._instance.config = Config()
+ cls._instance.config.backtest_controller = cls._instance
cls._instance.barrier = Barrier(1)
cls._instance.tasks = []
return cls._instance
@@ -79,8 +80,7 @@ class BackTestController:
if pending == 0:
await self.backtest_engine.tracker()
self.backtest_engine.next()
- # gives an output every 6 hours
- if self.backtest_engine.cursor.time % (3600 * 6) == 0:
+ if self.backtest_engine.cursor.time % (3600 * 12) == 0:
logger.info(
datetime.strftime(
datetime.fromtimestamp(self.backtest_engine.cursor.time), "%Y-%m-%d %H:%M:%S"
diff --git a/src/aiomql/core/config.py b/src/aiomql/core/config.py
index 49300a0..211265f 100644
--- a/src/aiomql/core/config.py
+++ b/src/aiomql/core/config.py
@@ -9,6 +9,7 @@ from .task_queue import TaskQueue
logger = getLogger(__name__)
Bot = TypeVar("Bot")
BackTestEngine = TypeVar("BackTestEngine")
+BackTestController = TypeVar("BackTestController")
class Config:
@@ -28,6 +29,7 @@ class Config:
task_queue: TaskQueue
_backtest_engine: BackTestEngine
bot: Bot
+ backtest_controller: BackTestController
_instance: Self
mode: Literal["backtest", "live"]
use_terminal_for_backtesting: bool
@@ -54,9 +56,11 @@ class Config:
if not hasattr(cls, "_instance"):
cls._instance = super().__new__(cls)
cls._instance.state = {}
- cls._instance.task_queue = TaskQueue()
+ cls._instance.task_queue = TaskQueue(mode='infinite', workers=10)
cls._instance.set_attributes(**cls._defaults)
cls._instance._backtest_engine = None
+ cls._instance.bot = None
+ cls._instance.backtest_controller = None
# cls._instance.load_config(**kwargs)
return cls._instance
diff --git a/src/aiomql/core/constants.py b/src/aiomql/core/constants.py
index 12696e7..47afb58 100644
--- a/src/aiomql/core/constants.py
+++ b/src/aiomql/core/constants.py
@@ -123,7 +123,8 @@ class OrderType(Repr, IntEnum):
Returns:
int: integer value of opposite order type
"""
- return {0: 1, 1: 0, 2: 3, 3: 2, 4: 5, 5: 4, 6: 7, 7: 6, 8: 8}[self]
+ _type = {0: 1, 1: 0, 2: 3, 3: 2, 4: 5, 5: 4, 6: 7, 7: 6, 8: 8}[self]
+ return OrderType(_type)
class BookType(Repr, IntEnum):
diff --git a/src/aiomql/core/task_queue.py b/src/aiomql/core/task_queue.py
index 9b70398..d94d137 100644
--- a/src/aiomql/core/task_queue.py
+++ b/src/aiomql/core/task_queue.py
@@ -87,6 +87,10 @@ class TaskQueue:
"""Worker function to run tasks in the queue."""
while True:
try:
+ if self.mode == 'infinite' and self.queue.qsize() <= 1:
+ dummy = QueueItem(self.dummy_task)
+ self.add(item=dummy)
+
if isinstance(self.queue, asyncio.PriorityQueue):
_, item = self.queue.get_nowait()
else:
@@ -96,6 +100,7 @@ class TaskQueue:
await item.run()
self.queue.task_done()
+
self.priority_tasks.discard(item)
if self.stop and (self.on_exit == 'cancel' or len(self.priority_tasks) == 0):
@@ -107,16 +112,15 @@ class TaskQueue:
break
if self.mode == 'finite':
+ self.stop = True
break
-
- # add dummy task to prevent worker from exiting
- sleep = QueueItem(asyncio.sleep, 1)
- self.add(item=sleep)
- await asyncio.sleep(self.worker_timeout)
except Exception as err:
logger.error("%s: Error occurred in worker", err)
break
+ async def dummy_task(self):
+ await asyncio.sleep(self.worker_timeout)
+
async def run(self, timeout: int = 0):
"""Run the queue until all tasks are completed or the timeout is reached.
@@ -128,13 +132,13 @@ class TaskQueue:
"""
start = time.perf_counter()
try:
- self.worker_tasks.extend([asyncio.create_task(self.worker()) for _ in range(self.workers)])
+ self.worker_tasks.extend(asyncio.create_task(self.worker()) for _ in range(self.workers))
timeout = timeout or self.timeout
self.queue_task = asyncio.create_task(self.queue.join())
if timeout:
await asyncio.wait_for(self.queue_task, timeout=timeout)
- self.stop = True
+
else:
await self.queue_task
@@ -142,22 +146,23 @@ class TaskQueue:
logger.warning("Timed out after %d seconds, %d tasks remaining",
time.perf_counter() - start, self.queue.qsize())
self.stop = True
+ await self.clean_up()
except asyncio.CancelledError:
self.stop = True
+ await self.clean_up()
except Exception as err:
logger.warning("%s: An error occurred in %s.run", err, self.__class__.__name__)
self.stop = True
-
- finally:
await self.clean_up()
+
async def clean_up(self):
"""Clean up tasks in the queue, completing priority tasks if `on_exit` is `complete_priority`"""
+ self.stop = True
try:
logger.info('cleaning up tasks...')
-
if self.on_exit == 'complete_priority' and (pt := len(self.priority_tasks)) > 0:
logger.info('Completing %d priority tasks...', pt)
self.queue_task = asyncio.create_task(self.queue.join())
@@ -166,20 +171,16 @@ class TaskQueue:
self.cancel()
except asyncio.CancelledError:
- self.stop = True
+ self.cancel()
except Exception as err:
logger.error(f"%s: Error occurred in %s", err, self.__class__.__name__)
-
- finally:
self.cancel()
def cancel(self):
"""Cancel all tasks in the queue"""
try:
-
self.queue_task.cancel()
-
except asyncio.CancelledError:
...
@@ -188,8 +189,11 @@ class TaskQueue:
def sigint_handle(self, sig, frame):
logger.info('SIGINT received, cleaning up...')
- self.stop = True
- self.cancel()
+ if self.stop is False:
+ self.stop = True
+ else:
+ self.stop = True
+ self.cancel()
TaskQueue.__doc__ = """TaskQueue is a class that allows you to queue tasks and run them concurrently with a specified number of workers.
diff --git a/src/aiomql/lib/account.py b/src/aiomql/lib/account.py
index cdfe8f1..6af1f7c 100644
--- a/src/aiomql/lib/account.py
+++ b/src/aiomql/lib/account.py
@@ -38,7 +38,7 @@ class Account(_Base, AccountInfo):
await self.mt5.initialize()
self.connected = await self.mt5.login()
if not self.connected:
- raise LoginError("Login failed")
+ raise LoginError(f"Login failed: {self.mt5.error}")
await self.refresh()
return self
diff --git a/src/aiomql/lib/backtester.py b/src/aiomql/lib/backtester.py
index 636dfd8..2acfdb5 100644
--- a/src/aiomql/lib/backtester.py
+++ b/src/aiomql/lib/backtester.py
@@ -183,6 +183,7 @@ class BackTester:
self.executor.add_strategy(strategy=strategy)
return True
else:
+ self.mt._symbol_select(strategy.symbol.name, True)
self.mt._market_book_add(strategy.symbol.name)
info = self.mt._symbol_info(strategy.symbol.name)
time = datetime.fromtimestamp(self.backtest_engine.cursor.time, tz=UTC)
diff --git a/src/aiomql/lib/bot.py b/src/aiomql/lib/bot.py
index 99c42b4..8552b0e 100644
--- a/src/aiomql/lib/bot.py
+++ b/src/aiomql/lib/bot.py
@@ -68,7 +68,7 @@ class Bot:
if len(self.executor.strategy_runners) == 0:
logger.warning("No strategies were added to the bot. Exiting in five seconds")
- await asyncio.sleep(5)
+ await asyncio.sleep(1)
self.config.shutdown = True
except Exception as err:
logger.error("%s: Bot initialization failed", err)
@@ -94,7 +94,7 @@ class Bot:
if len(self.executor.strategy_runners) == 0:
logger.warning("No strategies were added to the bot. Exiting in 5 seconds")
- time.sleep(5)
+ time.sleep(1)
self.config.shutdown = True
except Exception as err:
logger.error("%s: Bot initialization failed", err)
diff --git a/src/aiomql/lib/executor.py b/src/aiomql/lib/executor.py
index 253df38..7e12830 100644
--- a/src/aiomql/lib/executor.py
+++ b/src/aiomql/lib/executor.py
@@ -1,7 +1,8 @@
import asyncio
-from concurrent.futures import ThreadPoolExecutor
-from typing import Coroutine, Callable
import os
+from concurrent.futures import ThreadPoolExecutor
+from signal import signal, SIGINT
+from typing import Coroutine, Callable
from logging import getLogger
from ..core.config import Config
@@ -21,7 +22,7 @@ class Executor:
"""
executor: ThreadPoolExecutor
- tasks: list[asyncio.Task]
+ tasks: list[asyncio.Task | asyncio.Future]
config: Config
def __init__(self):
@@ -32,6 +33,7 @@ class Executor:
self.tasks = []
self.config = Config()
self.timeout = None # Timeout for the executor. For testing purposes only
+ signal(SIGINT, self.sigint_handle)
def add_function(self, *, function: Callable, kwargs: dict = None):
kwargs = kwargs or {}
@@ -78,16 +80,11 @@ class Executor:
async def create_coroutines_task(self):
""""""
- coros = [asyncio.create_task(coroutine) for coroutine in self.coroutines]
- # task = asyncio.create_task(asyncio.gather(*coros, return_exceptions=False))
- self.tasks.extend(coros)
- # loop = asyncio.get_running_loop()
- # loop.run_in_executor()
- task = asyncio.gather(*coros, return_exceptions=True)
+ tasks = [asyncio.create_task(coroutine) for coroutine in self.coroutines]
+ self.tasks.extend(tasks)
+ task = asyncio.gather(*tasks, return_exceptions=True)
self.tasks.append(task)
await task
- # await task
- # return task
def run_coroutine_tasks(self):
"""Run all coroutines in the executor"""
@@ -109,6 +106,9 @@ class Executor:
except Exception as err:
logger.error(f"Error: {err}. Unable to run function: {function.__name__}")
+ def sigint_handle(self, signum, frame):
+ self.config.shutdown = True
+
async def exit(self):
"""Shutdown the executor"""
start = asyncio.get_event_loop().time()
@@ -116,23 +116,25 @@ class Executor:
while self.config.shutdown is False and self.config.force_shutdown is False:
if self.timeout is not None and self.timeout < (asyncio.get_event_loop().time() - start):
self.config.shutdown = True
- timeout = self.timeout or 120
+ break
+ timeout = self.timeout or 30
await asyncio.sleep(timeout)
-
- print("Shutting down executor")
for strategy in self.strategy_runners:
strategy.running = False
- self.executor.shutdown(wait=False, cancel_futures=True)
+
+ if self.config.backtest_engine is not None:
+ self.config.backtest_engine.stop_testing = True
+
+ self.executor.shutdown(wait=False, cancel_futures=False)
for task in self.tasks:
task.cancel()
-
- # self.executor.shutdown(wait=False, cancel_futures=True)
if self.config.force_shutdown:
os._exit(1)
except Exception as err:
- logger.error(f"Error: {err}. Unable to shutdown executor")
+ logger.error("%s: Unable to shutdown executor", err)
+ os._exit(1)
def execute(self, *, workers: int = 5):
"""Run the strategies with a threadpool executor.
@@ -143,7 +145,7 @@ class Executor:
Notes:
No matter the number specified, the executor will always use a minimum of 5 workers.
"""
- workers_ = len(self.strategy_runners) + len(self.functions) + len(self.coroutine_threads) + 2
+ workers_ = len(self.strategy_runners) + len(self.functions) + len(self.coroutine_threads) + 3
workers = max(workers, workers_)
with ThreadPoolExecutor(max_workers=workers) as executor:
self.executor = executor
diff --git a/src/aiomql/lib/order.py b/src/aiomql/lib/order.py
index 56c06f2..224c102 100644
--- a/src/aiomql/lib/order.py
+++ b/src/aiomql/lib/order.py
@@ -28,6 +28,15 @@ class Order(_Base, TradeRequest):
kwargs = {"action": TradeAction.DEAL, "type_time": OrderTime.DAY, "type_filling": OrderFilling.FOK, **kwargs}
super().__init__(**kwargs)
+ def modify(self, **kwargs):
+ """Modify the order object with keyword arguments.
+
+ Args:
+ **kwargs: Keyword arguments must match the attributes of TradeRequest as well as the attributes of
+ Order class as specified in the annotations in the class definition.
+ """
+ self.set_attributes(**kwargs)
+
async def orders_total(self):
"""Get the number of active pending orders.
diff --git a/src/aiomql/lib/positions.py b/src/aiomql/lib/positions.py
index 4c9e934..798fd78 100644
--- a/src/aiomql/lib/positions.py
+++ b/src/aiomql/lib/positions.py
@@ -24,23 +24,38 @@ class Positions:
mt5: MetaTrader | MetaBackTester
positions: tuple[TradePosition, ...]
+ total_positions: int
def __init__(self):
"""Get Open Positions"""
self.config = Config()
self.mt5 = MetaTrader() if self.config.mode != "backtest" else MetaBackTester()
self.positions = ()
+ self.total_positions = 0
@backoff_decorator
- async def get_positions(self) -> tuple[TradePosition, ...]:
- """Get open positions with the ability to filter by symbol or ticket.
+ async def get_positions(self, *, symbol: str = None, ticket: int = None, group: str = None) -> tuple[TradePosition, ...]:
+ """Get open positions with the ability to filter by symbol, ticket or group of symbols.
+ Args:
+ symbol (Optional[str]): Financial instrument name. If symbol is provided, ticket is ignored.
+ ticket (Optional[int]): Position ticket.
+ group (Optional[str]): Group of symbols.
Returns:
tuple[TradePosition, ...]: A tuple of open trade positions
"""
- positions = await self.mt5.positions_get()
+ kwargs = {}
+ if symbol is not None:
+ kwargs["symbol"] = symbol
+ ticket = None
+ if ticket is not None:
+ kwargs["ticket"] = ticket
+ if group is not None:
+ kwargs["group"] = group
+ positions = await self.mt5.positions_get(**kwargs)
if positions is not None:
self.positions = tuple(TradePosition(**pos._asdict()) for pos in positions)
+ self.total_positions = len(self.positions)
return self.positions
logger.warning("Failed to get open positions")
return ()
@@ -130,3 +145,9 @@ class Positions:
*(self.close_position(position=position) for position in positions), return_exceptions=True
)
return len([res for res in results if (isinstance(res, OrderSendResult) and res.retcode == 10009)])
+
+ async def get_total_positions(self) -> int:
+ """Get total number of open positions."""
+ total = await self.mt5.positions_total()
+ self.total_positions = total or self.total_positions
+ return self.total_positions
diff --git a/src/aiomql/lib/ram.py b/src/aiomql/lib/ram.py
index 9656de6..9fabd3f 100644
--- a/src/aiomql/lib/ram.py
+++ b/src/aiomql/lib/ram.py
@@ -36,6 +36,11 @@ class RAM:
self.open_limit = kwargs.get("open_limit", 3)
self.fixed_amount = kwargs.get("fixed_amount", None)
+ def modify_ram(self, **kwargs):
+ """Modify the Risk Assessment and Management with the provided keyword arguments.
+ """
+ [setattr(self, key, value) for key, value in kwargs.items()]
+
async def get_amount(self) -> float:
"""Calculate the amount to risk per trade as a percentage of margin_free.
diff --git a/src/aiomql/lib/result.py b/src/aiomql/lib/result.py
index c8a8d10..5e20969 100644
--- a/src/aiomql/lib/result.py
+++ b/src/aiomql/lib/result.py
@@ -78,7 +78,8 @@ class Result:
"""Serialize the trade records and strategy parameters"""
try:
return str(value)
- except (ValueError, TypeError) as _:
+ except Exception as err:
+ logger.error("%s: Unable to serialize value", err)
return ""
async def to_json(self):
diff --git a/src/aiomql/lib/sessions.py b/src/aiomql/lib/sessions.py
index 259918f..103f346 100644
--- a/src/aiomql/lib/sessions.py
+++ b/src/aiomql/lib/sessions.py
@@ -5,8 +5,8 @@ from logging import getLogger
from ..core.models import OrderSendResult, TradePosition
from ..core.config import Config
-from ..core.backtesting.backtest_controller import BackTestController
from .positions import Positions
+from .._utils import backtest_sleep
logger = getLogger(__name__)
@@ -26,13 +26,13 @@ def delta(obj: time) -> timedelta:
return timedelta(hours=obj.hour, minutes=obj.minute, seconds=obj.second, microseconds=obj.microsecond)
-async def backtest_sleep(secs):
- """An async sleep function for use during backtesting."""
- btc = BackTestController()
- config = Config()
- sleep = config.backtest_engine.cursor.time + secs
- while sleep > config.backtest_engine.cursor.time:
- btc.wait()
+# async def backtest_sleep(secs):
+# """An async sleep function for use during backtesting."""
+# btc = BackTestController()
+# config = Config()
+# sleep = config.backtest_engine.cursor.time + secs
+# while sleep > config.backtest_engine.cursor.time:
+# btc.wait()
class Session:
diff --git a/src/aiomql/lib/symbol.py b/src/aiomql/lib/symbol.py
index f2769b8..d036763 100644
--- a/src/aiomql/lib/symbol.py
+++ b/src/aiomql/lib/symbol.py
@@ -53,14 +53,19 @@ class Symbol(_Base, SymbolInfo):
None: If request was unsuccessful
"""
try:
- tick = await self.mt5.symbol_info_tick(name or self.name)
+ if not name:
+ tick = await self.mt5.symbol_info_tick(self.name)
+ else:
+ await self.mt5.symbol_select(name, True)
+ await self.mt5.market_book_add(name)
+ tick = await self.mt5.symbol_info_tick(name)
if tick is not None:
tick = Tick(**tick._asdict())
setattr(self, "tick", tick) if not name else ...
return tick
return None
except Exception as err:
- logger.warning(f"{err}: Unable to get tick for {self.name}")
+ logger.warning("%s: Unable to get tick for %s", err, self.name)
return None
async def symbol_select(self, *, enable: bool = True) -> bool:
diff --git a/tests/live/integration/test_bot.py b/tests/live/integration/test_bot.py
index af471d4..50435a6 100644
--- a/tests/live/integration/test_bot.py
+++ b/tests/live/integration/test_bot.py
@@ -11,8 +11,7 @@ async def test_bot():
symbols = [ForexSymbol(name=sym) for sym in syms]
strategies = [Chaos(symbol=symbol, name="test_chaos") for symbol in symbols]
bot = Bot()
- bot.config.task_queue.worker_timeout = 2
- bot.executor.timeout = 10
+ bot.executor.timeout = 5
bot.add_strategies(strategies=strategies)
await bot.initialize()
bot.executor.execute()