This commit is contained in:
Ichinga Samuel
2024-12-01 11:50:05 +01:00
24 changed files with 268 additions and 77 deletions
+13
View File
@@ -2,6 +2,7 @@
Utils is a collection of utility functions that are used throughout the codebase. It is a collection of functions. Utils is a collection of utility functions that are used throughout the codebase. It is a collection of functions.
## Table of Contents ## Table of Contents
- [backtest_sleep](#_utils.backtest_sleep)
- [round_off](#_utiils.round_off) - [round_off](#_utiils.round_off)
- [dict_to_string](#_utils.dict_to_string) - [dict_to_string](#_utils.dict_to_string)
- [round_down](#_utils.round_down) - [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: 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. A decorator to handle exceptions in a sync function.
<a id="_utils.backtest_sleep"></a>
### 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. |
+9
View File
@@ -12,6 +12,7 @@
- [calc_profit](#order.calc_profit) - [calc_profit](#order.calc_profit)
- [calc_loss](#order.calc_loss) - [calc_loss](#order.calc_loss)
- [request](#order.request) - [request](#order.request)
- [modify](#order.modify)
<a id="order.order"></a> <a id="order.order"></a>
### Order ### Order
@@ -160,3 +161,11 @@ Return the trade request object as a dict
| Type | Description | | Type | Description |
|--------|----------------------------------| |--------|----------------------------------|
| `dict` | Returns the trade request object | | `dict` | Returns the trade request object |
<a id="order.modify"></a>
### modify
```python
def modify(**kwargs)
```
Modify the order object with keyword arguments.
+30 -7
View File
@@ -10,6 +10,7 @@
- [close_position_by_ticket](#positions.close_position_by_ticket) - [close_position_by_ticket](#positions.close_position_by_ticket)
- [close_position](#positions.close_position) - [close_position](#positions.close_position)
- [close_all](#positions.close_all) - [close_all](#positions.close_all)
- [get_total_positions](#positions.get_total_positions)
<a id="positions.positions"></a> <a id="positions.positions"></a>
### Positions ### Positions
@@ -19,10 +20,11 @@ class Positions
Get and handle Open positions. Get and handle Open positions.
#### Attributes #### Attributes
| Name | Type | Description | | Name | Type | Description |
|-------------|-----------------------------|----------------------------| |-------------|-----------------------------|-------------------------------------------------------------------------------------|
| `positions` | `tuple[TradePosition, ...]` | Financial instrument name. | | `positions` | `tuple[TradePosition, ...]` | Financial instrument name. |
| `mt5` | `MetaTrader` | MetaTrader instance. | | `mt5` | `MetaTrader` | MetaTrader instance. |
|`total_positions`| `int` | Total number of open positions. Can be set in `get_positions` or `get_total_positions`. |
<a id="positions.__init__"></a> <a id="positions.__init__"></a>
### \_\_init\_\_ ### \_\_init\_\_
@@ -35,11 +37,19 @@ Initialize a position instance
<a id="positions.get_position"></a> <a id="positions.get_position"></a>
### get_positions ### get_positions
```python ```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 | | Type | Description |
|-----------------------------|--------------------------------| |-----------------------------|--------------------------------|
| `tuple[TradePosition, ...]` | A list of open trade positions | | `tuple[TradePosition, ...]` | A list of open trade positions |
@@ -150,3 +160,16 @@ Close all open positions for the trading account.
| Type | Description | | Type | Description |
|-------|--------------------------------------| |-------|--------------------------------------|
| `int` | Return total number of closed trades | | `int` | Return total number of closed trades |
<a id="positions.get_total_positions"></a>
### 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 |
+9
View File
@@ -6,6 +6,7 @@
- [get_amount](#ram.get_amount) - [get_amount](#ram.get_amount)
- [check_losing_positions](#ram.check_losing_positions) - [check_losing_positions](#ram.check_losing_positions)
- [check_open_positions](#ram.check_open_positions) - [check_open_positions](#ram.check_open_positions)
- [modify_ram](#ram.modify_ram)
<a id="ram.ram"></a> <a id="ram.ram"></a>
### RAM ### RAM
@@ -75,3 +76,11 @@ Check if the number of open positions is less than or equal the loss limit.
| Type | Description | | Type | Description |
|--------|---------------------------------------------------------------------------------------| |--------|---------------------------------------------------------------------------------------|
| `bool` | True if the number of open losing trades is more than the loss limit, False otherwise | | `bool` | True if the number of open losing trades is more than the loss limit, False otherwise |
<a id="ram.modify_ram"></a>
### modify_ram
```python
def modify_ram(**kwargs):
```
Modify the RAM attributes. All provided keyword arguments are set as attributes.
@@ -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
}
+2 -2
View File
@@ -3,7 +3,7 @@ from datetime import datetime, UTC
from aiomql.lib.backtester import BackTester from aiomql.lib.backtester import BackTester
from aiomql.core import Config 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.contrib.symbols import ForexSymbol
from aiomql.core.backtesting import BackTestEngine from aiomql.core.backtesting import BackTestEngine
@@ -17,7 +17,7 @@ def back_tester():
start = datetime(2024, 5, 1, tzinfo=UTC) start = datetime(2024, 5, 1, tzinfo=UTC)
stop_time = datetime(2024, 5, 2, tzinfo=UTC) stop_time = datetime(2024, 5, 2, tzinfo=UTC)
end = datetime(2024, 5, 7, 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, close_open_positions_on_exit=True, assign_to_config=True, preload=True,
account_info={"balance": 350}) account_info={"balance": 350})
backtester = BackTester(backtest_engine=back_test_engine) backtester = BackTester(backtest_engine=back_test_engine)
+2 -2
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "aiomql" name = "aiomql"
version = "4.0.4" version = "4.0.5"
readme = "README.md" readme = "README.md"
requires-python = ">=3.11" requires-python = ">=3.11"
classifiers = [ classifiers = [
@@ -14,7 +14,7 @@ classifiers = [
] ]
keywords = ["MetaTrader5", "Asynchronous", "Algorithmic Trading", "Trading Bot", "Backtesting", 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"] dependencies = ["MetaTrader5>=5.0.37", "pandas>=1.5.0", "pandas-ta>=0.3.14b0"]
-1
View File
@@ -1,4 +1,3 @@
-e git+https://github.com/Ichinga-Samuel/aiomql.git@3143ca9936e2f791798b4944bf0df7225c4c6a78#egg=aiomql
anyio==4.3.0 anyio==4.3.0
argon2-cffi==23.1.0 argon2-cffi==23.1.0
argon2-cffi-bindings==21.2.0 argon2-cffi-bindings==21.2.0
+74 -5
View File
@@ -10,7 +10,35 @@ from .core.config import Config
logger = getLogger(__name__) 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: 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: 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: if func is None:
return partial(backoff_decorator, max_retries=max_retries, retries=retries, error=error) 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 return res
except Exception as err: except Exception as err:
logger.error("Error in %s: %s", func.__name__, 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 asyncio.sleep(2**retries + random.uniform(0, max_retries))
await wrapper(*args, **kwargs) 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): 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: if func is None:
return partial(error_handler, msg=msg, exe=exe, response=response, log_error_msg=log_error_msg) 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): 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: if func is None:
return partial(error_handler, msg=msg, exe=exe, response=response, log_error_msg=log_error_msg) 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 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)) 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 # noinspection PyShadowingNames
def round_off(value: float, step: float, round_down: bool = False) -> float: 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: with decimal.localcontext() as ctx:
ctx.rounding = decimal.ROUND_DOWN if round_down else decimal.ROUND_UP ctx.rounding = decimal.ROUND_DOWN if round_down else decimal.ROUND_UP
return float(decimal.Decimal(str(value)).quantize(decimal.Decimal(str(step)))) return float(decimal.Decimal(str(value)).quantize(decimal.Decimal(str(step))))
def async_cache(fun): def async_cache(fun):
"""A decorator to cache the result of an async function."""
@wraps(fun) @wraps(fun)
async def wrapper(*args, **kwargs): async def wrapper(*args, **kwargs):
key = (args, frozenset(kwargs.items())) key = (args, frozenset(kwargs.items()))
with wrapper.lock: with wrapper.lock:
if key not in wrapper.cache: if key not in wrapper.cache:
# print(key)
wrapper.cache[key] = await fun(*args, **kwargs) wrapper.cache[key] = await fun(*args, **kwargs)
return wrapper.cache[key] return wrapper.cache[key]
@@ -13,7 +13,7 @@ logger = getLogger(__name__)
class BackTestController: class BackTestController:
"""The controller for the backtesting engine. """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 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. 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"): if not hasattr(cls, "_instance"):
cls._instance = super().__new__(cls) cls._instance = super().__new__(cls)
cls._instance.config = Config() cls._instance.config = Config()
cls._instance.config.backtest_controller = cls._instance
cls._instance.barrier = Barrier(1) cls._instance.barrier = Barrier(1)
cls._instance.tasks = [] cls._instance.tasks = []
return cls._instance return cls._instance
@@ -79,8 +80,7 @@ class BackTestController:
if pending == 0: if pending == 0:
await self.backtest_engine.tracker() await self.backtest_engine.tracker()
self.backtest_engine.next() self.backtest_engine.next()
# gives an output every 6 hours if self.backtest_engine.cursor.time % (3600 * 12) == 0:
if self.backtest_engine.cursor.time % (3600 * 6) == 0:
logger.info( logger.info(
datetime.strftime( datetime.strftime(
datetime.fromtimestamp(self.backtest_engine.cursor.time), "%Y-%m-%d %H:%M:%S" datetime.fromtimestamp(self.backtest_engine.cursor.time), "%Y-%m-%d %H:%M:%S"
+5 -1
View File
@@ -9,6 +9,7 @@ from .task_queue import TaskQueue
logger = getLogger(__name__) logger = getLogger(__name__)
Bot = TypeVar("Bot") Bot = TypeVar("Bot")
BackTestEngine = TypeVar("BackTestEngine") BackTestEngine = TypeVar("BackTestEngine")
BackTestController = TypeVar("BackTestController")
class Config: class Config:
@@ -28,6 +29,7 @@ class Config:
task_queue: TaskQueue task_queue: TaskQueue
_backtest_engine: BackTestEngine _backtest_engine: BackTestEngine
bot: Bot bot: Bot
backtest_controller: BackTestController
_instance: Self _instance: Self
mode: Literal["backtest", "live"] mode: Literal["backtest", "live"]
use_terminal_for_backtesting: bool use_terminal_for_backtesting: bool
@@ -54,9 +56,11 @@ class Config:
if not hasattr(cls, "_instance"): if not hasattr(cls, "_instance"):
cls._instance = super().__new__(cls) cls._instance = super().__new__(cls)
cls._instance.state = {} 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.set_attributes(**cls._defaults)
cls._instance._backtest_engine = None cls._instance._backtest_engine = None
cls._instance.bot = None
cls._instance.backtest_controller = None
# cls._instance.load_config(**kwargs) # cls._instance.load_config(**kwargs)
return cls._instance return cls._instance
+2 -1
View File
@@ -123,7 +123,8 @@ class OrderType(Repr, IntEnum):
Returns: Returns:
int: integer value of opposite order type 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): class BookType(Repr, IntEnum):
+21 -17
View File
@@ -87,6 +87,10 @@ class TaskQueue:
"""Worker function to run tasks in the queue.""" """Worker function to run tasks in the queue."""
while True: while True:
try: 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): if isinstance(self.queue, asyncio.PriorityQueue):
_, item = self.queue.get_nowait() _, item = self.queue.get_nowait()
else: else:
@@ -96,6 +100,7 @@ class TaskQueue:
await item.run() await item.run()
self.queue.task_done() self.queue.task_done()
self.priority_tasks.discard(item) self.priority_tasks.discard(item)
if self.stop and (self.on_exit == 'cancel' or len(self.priority_tasks) == 0): if self.stop and (self.on_exit == 'cancel' or len(self.priority_tasks) == 0):
@@ -107,16 +112,15 @@ class TaskQueue:
break break
if self.mode == 'finite': if self.mode == 'finite':
self.stop = True
break 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: except Exception as err:
logger.error("%s: Error occurred in worker", err) logger.error("%s: Error occurred in worker", err)
break break
async def dummy_task(self):
await asyncio.sleep(self.worker_timeout)
async def run(self, timeout: int = 0): async def run(self, timeout: int = 0):
"""Run the queue until all tasks are completed or the timeout is reached. """Run the queue until all tasks are completed or the timeout is reached.
@@ -128,13 +132,13 @@ class TaskQueue:
""" """
start = time.perf_counter() start = time.perf_counter()
try: 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 timeout = timeout or self.timeout
self.queue_task = asyncio.create_task(self.queue.join()) self.queue_task = asyncio.create_task(self.queue.join())
if timeout: if timeout:
await asyncio.wait_for(self.queue_task, timeout=timeout) await asyncio.wait_for(self.queue_task, timeout=timeout)
self.stop = True
else: else:
await self.queue_task await self.queue_task
@@ -142,22 +146,23 @@ class TaskQueue:
logger.warning("Timed out after %d seconds, %d tasks remaining", logger.warning("Timed out after %d seconds, %d tasks remaining",
time.perf_counter() - start, self.queue.qsize()) time.perf_counter() - start, self.queue.qsize())
self.stop = True self.stop = True
await self.clean_up()
except asyncio.CancelledError: except asyncio.CancelledError:
self.stop = True self.stop = True
await self.clean_up()
except Exception as err: except Exception as err:
logger.warning("%s: An error occurred in %s.run", err, self.__class__.__name__) logger.warning("%s: An error occurred in %s.run", err, self.__class__.__name__)
self.stop = True self.stop = True
finally:
await self.clean_up() await self.clean_up()
async def clean_up(self): async def clean_up(self):
"""Clean up tasks in the queue, completing priority tasks if `on_exit` is `complete_priority`""" """Clean up tasks in the queue, completing priority tasks if `on_exit` is `complete_priority`"""
self.stop = True
try: try:
logger.info('cleaning up tasks...') logger.info('cleaning up tasks...')
if self.on_exit == 'complete_priority' and (pt := len(self.priority_tasks)) > 0: if self.on_exit == 'complete_priority' and (pt := len(self.priority_tasks)) > 0:
logger.info('Completing %d priority tasks...', pt) logger.info('Completing %d priority tasks...', pt)
self.queue_task = asyncio.create_task(self.queue.join()) self.queue_task = asyncio.create_task(self.queue.join())
@@ -166,20 +171,16 @@ class TaskQueue:
self.cancel() self.cancel()
except asyncio.CancelledError: except asyncio.CancelledError:
self.stop = True self.cancel()
except Exception as err: except Exception as err:
logger.error(f"%s: Error occurred in %s", err, self.__class__.__name__) logger.error(f"%s: Error occurred in %s", err, self.__class__.__name__)
finally:
self.cancel() self.cancel()
def cancel(self): def cancel(self):
"""Cancel all tasks in the queue""" """Cancel all tasks in the queue"""
try: try:
self.queue_task.cancel() self.queue_task.cancel()
except asyncio.CancelledError: except asyncio.CancelledError:
... ...
@@ -188,8 +189,11 @@ class TaskQueue:
def sigint_handle(self, sig, frame): def sigint_handle(self, sig, frame):
logger.info('SIGINT received, cleaning up...') logger.info('SIGINT received, cleaning up...')
self.stop = True if self.stop is False:
self.cancel() 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. TaskQueue.__doc__ = """TaskQueue is a class that allows you to queue tasks and run them concurrently with a specified number of workers.
+1 -1
View File
@@ -38,7 +38,7 @@ class Account(_Base, AccountInfo):
await self.mt5.initialize() await self.mt5.initialize()
self.connected = await self.mt5.login() self.connected = await self.mt5.login()
if not self.connected: if not self.connected:
raise LoginError("Login failed") raise LoginError(f"Login failed: {self.mt5.error}")
await self.refresh() await self.refresh()
return self return self
+1
View File
@@ -183,6 +183,7 @@ class BackTester:
self.executor.add_strategy(strategy=strategy) self.executor.add_strategy(strategy=strategy)
return True return True
else: else:
self.mt._symbol_select(strategy.symbol.name, True)
self.mt._market_book_add(strategy.symbol.name) self.mt._market_book_add(strategy.symbol.name)
info = self.mt._symbol_info(strategy.symbol.name) info = self.mt._symbol_info(strategy.symbol.name)
time = datetime.fromtimestamp(self.backtest_engine.cursor.time, tz=UTC) time = datetime.fromtimestamp(self.backtest_engine.cursor.time, tz=UTC)
+2 -2
View File
@@ -68,7 +68,7 @@ class Bot:
if len(self.executor.strategy_runners) == 0: if len(self.executor.strategy_runners) == 0:
logger.warning("No strategies were added to the bot. Exiting in five seconds") 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 self.config.shutdown = True
except Exception as err: except Exception as err:
logger.error("%s: Bot initialization failed", err) logger.error("%s: Bot initialization failed", err)
@@ -94,7 +94,7 @@ class Bot:
if len(self.executor.strategy_runners) == 0: if len(self.executor.strategy_runners) == 0:
logger.warning("No strategies were added to the bot. Exiting in 5 seconds") logger.warning("No strategies were added to the bot. Exiting in 5 seconds")
time.sleep(5) time.sleep(1)
self.config.shutdown = True self.config.shutdown = True
except Exception as err: except Exception as err:
logger.error("%s: Bot initialization failed", err) logger.error("%s: Bot initialization failed", err)
+21 -19
View File
@@ -1,7 +1,8 @@
import asyncio import asyncio
from concurrent.futures import ThreadPoolExecutor
from typing import Coroutine, Callable
import os import os
from concurrent.futures import ThreadPoolExecutor
from signal import signal, SIGINT
from typing import Coroutine, Callable
from logging import getLogger from logging import getLogger
from ..core.config import Config from ..core.config import Config
@@ -21,7 +22,7 @@ class Executor:
""" """
executor: ThreadPoolExecutor executor: ThreadPoolExecutor
tasks: list[asyncio.Task] tasks: list[asyncio.Task | asyncio.Future]
config: Config config: Config
def __init__(self): def __init__(self):
@@ -32,6 +33,7 @@ class Executor:
self.tasks = [] self.tasks = []
self.config = Config() self.config = Config()
self.timeout = None # Timeout for the executor. For testing purposes only 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): def add_function(self, *, function: Callable, kwargs: dict = None):
kwargs = kwargs or {} kwargs = kwargs or {}
@@ -78,16 +80,11 @@ class Executor:
async def create_coroutines_task(self): async def create_coroutines_task(self):
"""""" """"""
coros = [asyncio.create_task(coroutine) for coroutine in self.coroutines] tasks = [asyncio.create_task(coroutine) for coroutine in self.coroutines]
# task = asyncio.create_task(asyncio.gather(*coros, return_exceptions=False)) self.tasks.extend(tasks)
self.tasks.extend(coros) task = asyncio.gather(*tasks, return_exceptions=True)
# loop = asyncio.get_running_loop()
# loop.run_in_executor()
task = asyncio.gather(*coros, return_exceptions=True)
self.tasks.append(task) self.tasks.append(task)
await task await task
# await task
# return task
def run_coroutine_tasks(self): def run_coroutine_tasks(self):
"""Run all coroutines in the executor""" """Run all coroutines in the executor"""
@@ -109,6 +106,9 @@ class Executor:
except Exception as err: except Exception as err:
logger.error(f"Error: {err}. Unable to run function: {function.__name__}") 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): async def exit(self):
"""Shutdown the executor""" """Shutdown the executor"""
start = asyncio.get_event_loop().time() 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: 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): if self.timeout is not None and self.timeout < (asyncio.get_event_loop().time() - start):
self.config.shutdown = True self.config.shutdown = True
timeout = self.timeout or 120 break
timeout = self.timeout or 30
await asyncio.sleep(timeout) await asyncio.sleep(timeout)
print("Shutting down executor")
for strategy in self.strategy_runners: for strategy in self.strategy_runners:
strategy.running = False 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: for task in self.tasks:
task.cancel() task.cancel()
# self.executor.shutdown(wait=False, cancel_futures=True)
if self.config.force_shutdown: if self.config.force_shutdown:
os._exit(1) os._exit(1)
except Exception as err: 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): def execute(self, *, workers: int = 5):
"""Run the strategies with a threadpool executor. """Run the strategies with a threadpool executor.
@@ -143,7 +145,7 @@ class Executor:
Notes: Notes:
No matter the number specified, the executor will always use a minimum of 5 workers. 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_) workers = max(workers, workers_)
with ThreadPoolExecutor(max_workers=workers) as executor: with ThreadPoolExecutor(max_workers=workers) as executor:
self.executor = executor self.executor = executor
+9
View File
@@ -28,6 +28,15 @@ class Order(_Base, TradeRequest):
kwargs = {"action": TradeAction.DEAL, "type_time": OrderTime.DAY, "type_filling": OrderFilling.FOK, **kwargs} kwargs = {"action": TradeAction.DEAL, "type_time": OrderTime.DAY, "type_filling": OrderFilling.FOK, **kwargs}
super().__init__(**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): async def orders_total(self):
"""Get the number of active pending orders. """Get the number of active pending orders.
+24 -3
View File
@@ -24,23 +24,38 @@ class Positions:
mt5: MetaTrader | MetaBackTester mt5: MetaTrader | MetaBackTester
positions: tuple[TradePosition, ...] positions: tuple[TradePosition, ...]
total_positions: int
def __init__(self): def __init__(self):
"""Get Open Positions""" """Get Open Positions"""
self.config = Config() self.config = Config()
self.mt5 = MetaTrader() if self.config.mode != "backtest" else MetaBackTester() self.mt5 = MetaTrader() if self.config.mode != "backtest" else MetaBackTester()
self.positions = () self.positions = ()
self.total_positions = 0
@backoff_decorator @backoff_decorator
async def get_positions(self) -> tuple[TradePosition, ...]: 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 or ticket. """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: Returns:
tuple[TradePosition, ...]: A tuple of open trade positions 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: if positions is not None:
self.positions = tuple(TradePosition(**pos._asdict()) for pos in positions) self.positions = tuple(TradePosition(**pos._asdict()) for pos in positions)
self.total_positions = len(self.positions)
return self.positions return self.positions
logger.warning("Failed to get open positions") logger.warning("Failed to get open positions")
return () return ()
@@ -130,3 +145,9 @@ class Positions:
*(self.close_position(position=position) for position in positions), return_exceptions=True *(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)]) 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
+5
View File
@@ -36,6 +36,11 @@ class RAM:
self.open_limit = kwargs.get("open_limit", 3) self.open_limit = kwargs.get("open_limit", 3)
self.fixed_amount = kwargs.get("fixed_amount", None) 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: async def get_amount(self) -> float:
"""Calculate the amount to risk per trade as a percentage of margin_free. """Calculate the amount to risk per trade as a percentage of margin_free.
+2 -1
View File
@@ -78,7 +78,8 @@ class Result:
"""Serialize the trade records and strategy parameters""" """Serialize the trade records and strategy parameters"""
try: try:
return str(value) return str(value)
except (ValueError, TypeError) as _: except Exception as err:
logger.error("%s: Unable to serialize value", err)
return "" return ""
async def to_json(self): async def to_json(self):
+8 -8
View File
@@ -5,8 +5,8 @@ from logging import getLogger
from ..core.models import OrderSendResult, TradePosition from ..core.models import OrderSendResult, TradePosition
from ..core.config import Config from ..core.config import Config
from ..core.backtesting.backtest_controller import BackTestController
from .positions import Positions from .positions import Positions
from .._utils import backtest_sleep
logger = getLogger(__name__) 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) return timedelta(hours=obj.hour, minutes=obj.minute, seconds=obj.second, microseconds=obj.microsecond)
async def backtest_sleep(secs): # async def backtest_sleep(secs):
"""An async sleep function for use during backtesting.""" # """An async sleep function for use during backtesting."""
btc = BackTestController() # btc = BackTestController()
config = Config() # config = Config()
sleep = config.backtest_engine.cursor.time + secs # sleep = config.backtest_engine.cursor.time + secs
while sleep > config.backtest_engine.cursor.time: # while sleep > config.backtest_engine.cursor.time:
btc.wait() # btc.wait()
class Session: class Session:
+7 -2
View File
@@ -53,14 +53,19 @@ class Symbol(_Base, SymbolInfo):
None: If request was unsuccessful None: If request was unsuccessful
""" """
try: 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: if tick is not None:
tick = Tick(**tick._asdict()) tick = Tick(**tick._asdict())
setattr(self, "tick", tick) if not name else ... setattr(self, "tick", tick) if not name else ...
return tick return tick
return None return None
except Exception as err: 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 return None
async def symbol_select(self, *, enable: bool = True) -> bool: async def symbol_select(self, *, enable: bool = True) -> bool:
+1 -2
View File
@@ -11,8 +11,7 @@ async def test_bot():
symbols = [ForexSymbol(name=sym) for sym in syms] symbols = [ForexSymbol(name=sym) for sym in syms]
strategies = [Chaos(symbol=symbol, name="test_chaos") for symbol in symbols] strategies = [Chaos(symbol=symbol, name="test_chaos") for symbol in symbols]
bot = Bot() bot = Bot()
bot.config.task_queue.worker_timeout = 2 bot.executor.timeout = 5
bot.executor.timeout = 10
bot.add_strategies(strategies=strategies) bot.add_strategies(strategies=strategies)
await bot.initialize() await bot.initialize()
bot.executor.execute() bot.executor.execute()