This commit is contained in:
Ichinga Samuel
2025-01-23 18:18:18 +01:00
parent 12f61f3b6e
commit 28811df0ce
20 changed files with 284 additions and 157 deletions
+2
View File
@@ -79,4 +79,6 @@ aiomql.json
# development # development
terminals/ terminals/
*.pkl *.pkl
backtesting/
trade_records/
+16 -1
View File
@@ -1,6 +1,21 @@
# Changelog # Changelog
## [4.0.7](https://github.com/Ichinga-Samuel/aiomql/releases/tag/v4.0.8) - 2025-01-21 ## [4.0.9](https://github.com/Ichinga-Samuel/aiomql/releases/tag/v4.0.8) - 2025-01-23
### Fixed
- Fixed `__add__` to return a new Candles object
### Changed
- Removed tasks attribute from executor class
### Added
- Added `initialize_sync` method for synchronous initialization of a symbol
## [4.0.8](https://github.com/Ichinga-Samuel/aiomql/releases/tag/v4.0.8) - 2025-01-21
### Fixed ### Fixed
+36
View File
@@ -0,0 +1,36 @@
import asyncio
async def rt():
await asyncio.sleep(1)
print("Hello")
async def rrt():
while True:
await asyncio.sleep(15)
print("rtt")
async def stop(task, tm=10):
await asyncio.sleep(tm)
print("Stopping", task)
res = task.cancel()
print(res, task)
def main():
task1 = rt()
task2 = rrt()
# task3 = stop(task2)
tasks = [task1, task2, task3]
# task = asyncio.gather(*tasks, return_exceptions=True)
asyncio.run(asyncio.gather(*tasks, return_exceptions=True))
# await asyncio.gather(task, stop(task, tm=3), return_exceptions=True)
main = main()
# asyncio.run(main)
# def main(a, b=6, **kwargs):
# print(a, b, kwargs)
# main(4, c=9, g=4, b=99)
+8
View File
@@ -13,6 +13,7 @@ The base class for creating strategies.
- [backtest_strategy](#strategy.backtest_strategy) - [backtest_strategy](#strategy.backtest_strategy)
- [trade](#strategy.trade) - [trade](#strategy.trade)
- [test](#strategy.test) - [test](#strategy.test)
- [initialize](#strategy.initialize)
<a id="strategy.strategy"></a> <a id="strategy.strategy"></a>
@@ -134,3 +135,10 @@ Runs the strategy in live mode.
async def live_strategy() async def live_strategy()
``` ```
Runs the strategy in backtest mode. Runs the strategy in backtest mode.
<a id="strategy.initialize"></a>
### initialize
```python
async def initialize()
```
Initialize a strategy
+13 -1
View File
@@ -7,6 +7,7 @@ Symbol class for handling a financial instrument.
- [symbol_select](#symbol.symbol_select) - [symbol_select](#symbol.symbol_select)
- [info](#symbol.info) - [info](#symbol.info)
- [initialize](#symbol.initialize) - [initialize](#symbol.initialize)
- [initialize_sync](#symbol.initialize_sync)
- [book_add](#symbol.book_add) - [book_add](#symbol.book_add)
- [book_get](#symbol.book_get) - [book_get](#symbol.book_get)
- [book_release](#symbol.book_release) - [book_release](#symbol.book_release)
@@ -94,7 +95,7 @@ Get data on the specified financial instrument and update the symbol object prop
<a id="symbol.initialize"></a> <a id="symbol.initialize"></a>
### init ### initialize
```python ```python
async def initialize() -> bool async def initialize() -> bool
``` ```
@@ -105,6 +106,17 @@ Initialized the symbol by pulling properties from the terminal
|--------|--------------------------------------------------------| |--------|--------------------------------------------------------|
| `bool` | Returns True if symbol info was successful initialized | | `bool` | Returns True if symbol info was successful initialized |
<a id="symbol.initialize_sync"></a>
### initialize_sync
```python
def initialize_sync() -> bool
```
Initialized the symbol by pulling properties from the terminal
#### Returns:
| Type | Description |
|--------|--------------------------------------------------------|
| `bool` | Returns True if symbol info was successful initialized |
<a id="symbol.book_add"></a> <a id="symbol.book_add"></a>
### book_add ### book_add
@@ -1,17 +1,17 @@
{ {
"balance": 297007.45, "balance": 367056.24,
"profit": 0, "profit": 0,
"equity": 297007.45, "equity": 367056.24,
"margin": 0.0, "margin": 0.0,
"margin_free": 297007.45, "margin_free": 367056.24,
"margin_level": 0, "margin_level": 0,
"wins": 307, "wins": 374,
"losses": 336, "losses": 394,
"total": 643, "total": 768,
"win_percentage": 47.74, "win_percentage": 48.7,
"win": 619421.92, "win": 946684.28,
"loss": -323164.47, "loss": -580378.04,
"net_profit": 296257.45, "net_profit": 366306.24,
"profit_factor": 1.92, "profit_factor": 1.63,
"profitability": 39500.99 "profitability": 48840.83
} }
+7 -5
View File
@@ -20,11 +20,13 @@ class Chaos(Strategy):
fast_ema: int fast_ema: int
slow_ema: int slow_ema: int
tracker: Tracker tracker: Tracker
parameters = {"fast_ema": 8, "slow_ema": 20, "ltf": TimeFrame.M1, "htf": TimeFrame.M2, "lcc": 100, "hcc": 100} interval: int
parameters = {"fast_ema": 8, "slow_ema": 20, "ltf": TimeFrame.M1, "htf": TimeFrame.M2, "lcc": 100, "hcc": 100,
"interval": 0}
def __init__(self, *, symbol: ForexSymbol, params: dict = None, sessions=None, name="Chaos"): def __init__(self, *, symbol: ForexSymbol, params: dict = None, sessions=None, name="Chaos"):
super().__init__(symbol=symbol, params=params, sessions=sessions, name=name) super().__init__(symbol=symbol, params=params, sessions=sessions, name=name)
self.tracker = Tracker(snooze=self.ltf.seconds) self.tracker = Tracker(snooze=self.interval or self.ltf.seconds)
self.trader = ScalpTrader(symbol=self.symbol) self.trader = ScalpTrader(symbol=self.symbol)
async def check_trend(self): async def check_trend(self):
@@ -43,12 +45,12 @@ class Chaos(Strategy):
candles.rename(inplace=True, **{f"EMA_{self.fast_ema}": "fast", f"EMA_{self.slow_ema}": "slow"}) candles.rename(inplace=True, **{f"EMA_{self.fast_ema}": "fast", f"EMA_{self.slow_ema}": "slow"})
order_type = random.choice([OrderType.BUY, OrderType.SELL]) order_type = random.choice([OrderType.BUY, OrderType.SELL])
if order_type == OrderType.BUY: if order_type == OrderType.BUY:
self.tracker.update(trend="bullish", snooze=self.htf.seconds, order_type=OrderType.BUY) self.tracker.update(trend="bullish", snooze=self.interval or self.htf.seconds, order_type=OrderType.BUY)
else: else:
self.tracker.update(trend="bearish", snooze=self.htf.seconds, order_type=OrderType.SELL) self.tracker.update(trend="bearish", snooze=self.interval or self.htf.seconds, order_type=OrderType.SELL)
except Exception as err: except Exception as err:
logger.error(f"{err}. Failed to check trend") logger.error(f"{err}. Failed to check trend")
self.tracker.update(trend="ranging", snooze=self.ltf.seconds, order_type=None) self.tracker.update(trend="ranging", snooze=self.interval or self.ltf.seconds, order_type=None)
async def trade(self): async def trade(self):
try: try:
@@ -1067,6 +1067,54 @@ class BackTestEngine:
tick = await self.get_price_tick(symbol=symbol, time=self.cursor.time) tick = await self.get_price_tick(symbol=symbol, time=self.cursor.time)
return tick return tick
async def symbol_select(self, *, symbol: str, enable: bool) -> bool:
if self.use_terminal:
info = await self.mt5.symbol_select(symbol, enable)
return info
else:
return symbol in self._data.symbols.keys()
def symbol_select_sync(self, *, symbol: str, enable: bool = True) -> bool:
if self.use_terminal:
info = self.mt5._symbol_select(symbol, enable)
return info
else:
return symbol in self._data.symbols.keys()
def symbol_info_tick_sync(self, *, symbol) -> Tick | None:
if self.use_terminal:
time = datetime.fromtimestamp(self.cursor.time, tz=UTC)
tick = self.mt5._copy_ticks_from(symbol, time, 1, CopyTicks.ALL)
tick = Tick(tick[-1]) if tick is not None else None
else:
tick = self.prices[symbol].loc[self.cursor.time]
tick = Tick(tick) if tick is not None else None
return tick
def symbol_info_sync(self, *, symbol) -> SymbolInfo | None:
if self.use_terminal:
info = self.mt5._symbol_info(symbol)
time = datetime.fromtimestamp(self.cursor.time, tz=UTC)
tick = self.mt5._copy_ticks_from(symbol, time, 1, CopyTicks.ALL)
tick = Tick(tick[-1]) if tick is not None else None
else:
info = self.symbols[symbol]
tick = self.prices[symbol].loc[self.cursor.time]
tick = Tick(tick) if tick is not None else None
if info and tick:
info = info._asdict() | {
"bid": tick.bid,
"bidhigh": tick.bid,
"bidlow": tick.bid,
"ask": tick.ask,
"askhigh": tick.ask,
"asklow": tick.bid,
"last": tick.last,
"volume_real": tick.volume_real,
}
return SymbolInfo((info.get(key) for key in SymbolInfo.__match_args__))
@async_cache @async_cache
async def _symbol_info(self, *, symbol: str) -> SymbolInfo: async def _symbol_info(self, *, symbol: str) -> SymbolInfo:
if self.use_terminal: if self.use_terminal:
+15
View File
@@ -87,6 +87,15 @@ class MetaBackTester(MetaTrader):
return super().login_sync(login=login, password=password, server=server, timeout=timeout) return super().login_sync(login=login, password=password, server=server, timeout=timeout)
return True return True
def _symbol_select(self, symbol: str, enable: bool) -> bool:
return self.backtest_engine.symbol_select_sync(symbol=symbol, enable=enable)
def _market_book_add(self, symbol: str) -> bool:
return True
async def market_book_add(self, symbol: str) -> bool:
return True
async def login(self, *, login: int = 0, password: str = "", server: str = "", timeout: int = 60000) -> bool: async def login(self, *, login: int = 0, password: str = "", server: str = "", timeout: int = 60000) -> bool:
if self.config.use_terminal_for_backtesting: if self.config.use_terminal_for_backtesting:
return await super().login(login=login, password=password, server=server, timeout=timeout) return await super().login(login=login, password=password, server=server, timeout=timeout)
@@ -121,6 +130,12 @@ class MetaBackTester(MetaTrader):
sym = await self.backtest_engine.get_symbol_info(symbol=symbol) sym = await self.backtest_engine.get_symbol_info(symbol=symbol)
return sym return sym
def _symbol_info(self, symbol) -> SymbolInfo | None:
return self.backtest_engine.symbol_info_sync(symbol=symbol)
def _symbol_info_tick(self, symbol) -> Tick | None:
return self.backtest_engine.symbol_info_tick_sync(symbol=symbol)
@error_handler(msg="test data not available") @error_handler(msg="test data not available")
async def symbol_info_tick(self, symbol: str) -> Tick | None: async def symbol_info_tick(self, symbol: str) -> Tick | None:
tick = await self.backtest_engine.get_symbol_info_tick(symbol=symbol) tick = await self.backtest_engine.get_symbol_info_tick(symbol=symbol)
+2 -3
View File
@@ -17,7 +17,6 @@ from MetaTrader5 import (
OrderSendResult, OrderSendResult,
OrderCheckResult, OrderCheckResult,
) )
import MetaTrader5 as mt5
from .constants import OrderType, CopyTicks from .constants import OrderType, CopyTicks
@@ -111,7 +110,7 @@ class MetaTrader(MetaCore):
login = login or acc_details.get("login", 0) login = login or acc_details.get("login", 0)
password = password or acc_details.get("password", "") password = password or acc_details.get("password", "")
server = server or acc_details.get("server", "") server = server or acc_details.get("server", "")
res = mt5.login(login, password=password, server=server, timeout=timeout) res = self._login(login, password=password, server=server, timeout=timeout)
return res return res
async def initialize( async def initialize(
@@ -200,7 +199,7 @@ class MetaTrader(MetaCore):
) )
if key is not None if key is not None
} }
res = mt5.initialize(*args, **kwargs) res = self._initialize(*args, **kwargs)
if res is False: if res is False:
self._shutdown() self._shutdown()
if not res: if not res:
+12 -1
View File
@@ -64,7 +64,7 @@ class TaskQueue:
mode: Literal['finite', 'infinite'] = 'finite', worker_timeout: int = 1): mode: Literal['finite', 'infinite'] = 'finite', worker_timeout: int = 1):
self.queue = queue or asyncio.PriorityQueue(maxsize=size) self.queue = queue or asyncio.PriorityQueue(maxsize=size)
self.workers = workers self.workers = workers
self.worker_tasks = {} self.worker_tasks: dict[int|float, asyncio.Task] = {}
self.queue_timeout = queue_timeout self.queue_timeout = queue_timeout
self.absolute_timeout = absolute_timeout self.absolute_timeout = absolute_timeout
self.stop = False self.stop = False
@@ -72,6 +72,7 @@ class TaskQueue:
self.mode = mode self.mode = mode
self.worker_timeout = worker_timeout self.worker_timeout = worker_timeout
self.queue_task_cancelled = False self.queue_task_cancelled = False
self.start_time = time.perf_counter()
signal(SIGINT, self.sigint_handle) signal(SIGINT, self.sigint_handle)
def add(self, *, item: QueueItem, priority=3, must_complete=False): def add(self, *, item: QueueItem, priority=3, must_complete=False):
@@ -187,6 +188,14 @@ class TaskQueue:
wr = range(no_of_workers) wr = range(no_of_workers)
[self.worker_tasks.setdefault(wi:=ri(), ct(wi)) for _ in wr] [self.worker_tasks.setdefault(wi:=ri(), ct(wi)) for _ in wr]
async def watch(self):
while True:
await asyncio.sleep(1)
if (time.perf_counter() - self.start_time) > self.absolute_timeout:
self.stop = True
self.cancel()
break
async def run(self, queue_timeout: int = None, absolute_timeout: int = None): async def run(self, queue_timeout: int = None, absolute_timeout: int = None):
"""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.
@@ -202,6 +211,8 @@ class TaskQueue:
self.start_timer(queue_timeout=queue_timeout, absolute_timeout=absolute_timeout, start=True) self.start_timer(queue_timeout=queue_timeout, absolute_timeout=absolute_timeout, start=True)
await self.add_workers(no_of_workers=self.workers) await self.add_workers(no_of_workers=self.workers)
self.queue_task = asyncio.create_task(self.queue.join()) self.queue_task = asyncio.create_task(self.queue.join())
if self.absolute_timeout:
asyncio.create_task(self.watch())
await self.queue_task await self.queue_task
except asyncio.TimeoutError: except asyncio.TimeoutError:
+17 -47
View File
@@ -1,16 +1,13 @@
import asyncio import asyncio
import logging import logging
import time
from typing import Type, Iterable, Callable, Coroutine from typing import Type, Iterable, Callable, Coroutine
from datetime import datetime, UTC
from MetaTrader5 import Tick
from .executor import Executor from .executor import Executor
from ..core.config import Config from ..core.config import Config
from ..core.backtesting.backtest_controller import BackTestController from ..core.backtesting.backtest_controller import BackTestController
from ..core.meta_backtester import MetaBackTester from ..core.meta_backtester import MetaBackTester
from ..core.backtesting.backtest_engine import BackTestEngine from ..core.backtesting.backtest_engine import BackTestEngine
from ..core.constants import CopyTicks
from .symbol import Symbol as Symbol from .symbol import Symbol as Symbol
from .strategy import Strategy as Strategy from .strategy import Strategy as Strategy
@@ -58,8 +55,10 @@ class BackTester:
self.backtest_engine.setup_account_sync() self.backtest_engine.setup_account_sync()
self.init_strategies_sync() self.init_strategies_sync()
if (strategies := len(self.executor.strategy_runners)) == 0: if (strategies := len(self.executor.strategy_runners)) == 0:
logger.warning("No strategies were added to the backtester. Exiting ...") self.config.shutdown = True
raise Exception("No strategies added to the backtester") logger.warning("No strategies were added to the backtester. Exiting in one second")
time.sleep(1)
return
self.config.task_queue.worker_timeout = 5 self.config.task_queue.worker_timeout = 5
self.add_coroutine(coroutine=self.config.task_queue.run, on_separate_thread=True) self.add_coroutine(coroutine=self.config.task_queue.run, on_separate_thread=True)
self.add_coroutine(coroutine=self.executor.exit) self.add_coroutine(coroutine=self.executor.exit)
@@ -87,8 +86,10 @@ class BackTester:
await self.backtest_engine.setup_account() await self.backtest_engine.setup_account()
await self.init_strategies() await self.init_strategies()
if (strategies := len(self.executor.strategy_runners)) == 0: if (strategies := len(self.executor.strategy_runners)) == 0:
logger.warning("No strategies were added to the backtester. Exiting ...") self.config.shutdown = True
raise Exception("No strategies added to the backtester") logger.warning("No strategies were added to the backtester. Exiting in one second")
await asyncio.sleep(1)
return
self.config.task_queue.worker_timeout = 5 self.config.task_queue.worker_timeout = 5
self.add_coroutine(coroutine=self.config.task_queue.run, on_separate_thread=True) self.add_coroutine(coroutine=self.config.task_queue.run, on_separate_thread=True)
self.add_coroutine(coroutine=self.executor.exit) self.add_coroutine(coroutine=self.executor.exit)
@@ -113,16 +114,19 @@ class BackTester:
def execute(self): def execute(self):
"""Execute the bot.""" """Execute the bot."""
self.initialize_sync() self.initialize_sync()
self.executor.execute() if self.config.shutdown is False:
self.executor.execute()
async def start(self): async def start(self):
"""Initialize the bot and execute it. Similar to calling `execute` method but is a coroutine.""" """Initialize the bot and execute it. Similar to calling `execute` method but is a coroutine."""
await self.initialize() await self.initialize()
if self.config.shutdown is False:
self.executor.execute()
self.executor.execute() self.executor.execute()
def add_strategy(self, *, strategy: Strategy): def add_strategy(self, *, strategy: Strategy):
"""Add a strategy to the list of strategies. """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. An added strategy will only run if it's symbol was successfully initialized, and it is added to the executor.
Args: Args:
strategy (Strategy): A Strategy instance to run on bot strategy (Strategy): A Strategy instance to run on bot
@@ -162,44 +166,10 @@ class BackTester:
def init_strategy_sync(self, *, strategy: Strategy) -> bool: def init_strategy_sync(self, *, strategy: Strategy) -> bool:
"""Initialize a single strategy. This method is called internally by the bot.""" """Initialize a single strategy. This method is called internally by the bot."""
try: try:
if self.backtest_engine.use_terminal is False: res = strategy.symbol.initialize_sync()
info = self.backtest_engine.symbols[strategy.symbol.name] if res:
tick = self.backtest_engine.prices[strategy.symbol.name].loc[self.backtest_engine.cursor.time]
tick = Tick(tick)
info = info._asdict() | {
"bid": tick.bid,
"bidhigh": tick.bid,
"bidlow": tick.bid,
"ask": tick.ask,
"askhigh": tick.ask,
"asklow": tick.bid,
"last": tick.last,
"volume_real": tick.volume_real,
}
strategy.symbol.set_attributes(**info)
self.executor.add_strategy(strategy=strategy) self.executor.add_strategy(strategy=strategy)
return True return res
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)
tick = self.mt._copy_ticks_from(strategy.symbol.name, time, 1, CopyTicks.ALL)
tick = Tick(tick[-1]) if tick is not None else None
info = info._asdict() | {
"bid": tick.bid,
"bidhigh": tick.bid,
"bidlow": tick.bid,
"ask": tick.ask,
"askhigh": tick.ask,
"asklow": tick.bid,
"last": tick.last,
"volume_real": tick.volume_real,
}
strategy.symbol.set_attributes(**info)
strategy.symbol.initialized = True
self.executor.add_strategy(strategy=strategy)
return True
except Exception as err: except Exception as err:
logger.error("%s: Unable to initialize strategy", err) logger.error("%s: Unable to initialize strategy", err)
return False return False
+13 -26
View File
@@ -8,7 +8,6 @@ from .executor import Executor
from ..core.config import Config from ..core.config import Config
from ..core.meta_trader import MetaTrader from ..core.meta_trader import MetaTrader
from .symbol import Symbol as Symbol from .symbol import Symbol as Symbol
from .ticks import Tick
from .strategy import Strategy as Strategy from .strategy import Strategy as Strategy
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -67,7 +66,7 @@ class Bot:
self.add_coroutine(coroutine=self.executor.exit) self.add_coroutine(coroutine=self.executor.exit)
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 one second")
await asyncio.sleep(1) await asyncio.sleep(1)
self.config.shutdown = True self.config.shutdown = True
except Exception as err: except Exception as err:
@@ -76,7 +75,8 @@ class Bot:
def initialize_sync(self): def initialize_sync(self):
"""Prepares the bot by signing in to the trading account and initializing the symbols for each strategy. """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. Only strategies with successfully initialized symbols will be added to the executor.
Starts the global task queue.
Raises: Raises:
SystemExit if sign in was not successful SystemExit if sign in was not successful
@@ -93,7 +93,7 @@ class Bot:
self.add_coroutine(coroutine=self.executor.exit) self.add_coroutine(coroutine=self.executor.exit)
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 one second")
time.sleep(1) time.sleep(1)
self.config.shutdown = True self.config.shutdown = True
except Exception as err: except Exception as err:
@@ -120,14 +120,16 @@ class Bot:
self.executor.add_coroutine(coroutine=coroutine, kwargs=kwargs, on_separate_thread=on_separate_thread) self.executor.add_coroutine(coroutine=coroutine, kwargs=kwargs, on_separate_thread=on_separate_thread)
def execute(self): def execute(self):
"""Execute the bot using asyncio.run""" """Start the bot in sync mode"""
self.initialize_sync() self.initialize_sync()
self.executor.execute() if self.config.shutdown is False:
self.executor.execute()
async def start(self): async def start(self):
"""Initialize the bot and call the executor it.""" """Initialize the bot and call the executor it."""
await self.initialize() await self.initialize()
self.executor.execute() if self.config.shutdown is False:
self.executor.execute()
def add_strategy(self, *, strategy: Strategy): def add_strategy(self, *, strategy: Strategy):
"""Add a strategy to the list of strategies. """Add a strategy to the list of strategies.
@@ -174,25 +176,10 @@ class Bot:
def init_strategy_sync(self, *, strategy: Strategy) -> bool: def init_strategy_sync(self, *, strategy: Strategy) -> bool:
"""Initialize a single strategy. This method is called internally by the bot.""" """Initialize a single strategy. This method is called internally by the bot."""
try: res = strategy.symbol.initialize_sync()
select = self.mt5._symbol_select(strategy.symbol.name, True) if res:
info = self.mt5._symbol_info(strategy.symbol.name) self.executor.add_strategy(strategy=strategy)
tick = self.mt5._symbol_info_tick(strategy.symbol.name) return res
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.initialized = True
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): def init_strategies_sync(self):
"""Initialize the symbols for the current trading session. This method is called internally by the bot.""" """Initialize the symbols for the current trading session. This method is called internally by the bot."""
+2 -2
View File
@@ -298,11 +298,11 @@ class Candles:
"""Add a new row to the candles class.""" """Add a new row to the candles class."""
if isinstance(row, Series): if isinstance(row, Series):
data = self. pd.concat([self._data, pd.DataFrame(row).T]) data = self. pd.concat([self._data, pd.DataFrame(row).T])
return self.Candle(data=data) return self.__class__(data=data)
elif isinstance(row, DataFrame): elif isinstance(row, DataFrame):
data = pd.concat([self._data, row]) data = pd.concat([self._data, row])
return self.Candle(data=data) return self.__class__(data=data)
def add(self, row: DataFrame | Series) -> bool: def add(self, row: DataFrame | Series) -> bool:
"""Add a new row to the candles class.""" """Add a new row to the candles class."""
+25 -42
View File
@@ -17,20 +17,18 @@ class Executor:
Attributes: Attributes:
executor (ThreadPoolExecutor): The executor object. executor (ThreadPoolExecutor): The executor object.
strategy_runners (list): List of strategies. strategy_runners (list): List of strategies.
coroutines (list[Coroutine]): A list of coroutines to run in the executor coroutines (dict[Coroutine, dict]): A list of coroutines to run in the executor
coroutine_threads (dict[Coroutine, dict]): A list of coroutines to run in the executor
functions (dict[Callable, dict]): A dictionary of functions to run in the executor functions (dict[Callable, dict]): A dictionary of functions to run in the executor
""" """
executor: ThreadPoolExecutor executor: ThreadPoolExecutor
tasks: list[asyncio.Task | asyncio.Future]
config: Config config: Config
def __init__(self): def __init__(self):
self.strategy_runners: list[Strategy] = [] self.strategy_runners: list[Strategy] = []
self.coroutines: list[Coroutine] = [] self.coroutines: dict[Coroutine: dict] = {}
self.coroutine_threads: list[Coroutine] = [] self.coroutine_threads: dict[Coroutine: dict] = {}
self.functions: dict[Callable:dict] = {} self.functions: dict[Callable:dict] = {}
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) signal(SIGINT, self.sigint_handle)
@@ -41,8 +39,10 @@ class Executor:
def add_coroutine(self, *, coroutine: Callable | Coroutine, kwargs: dict = None, on_separate_thread=False): def add_coroutine(self, *, coroutine: Callable | Coroutine, kwargs: dict = None, on_separate_thread=False):
kwargs = kwargs or {} kwargs = kwargs or {}
coroutine = coroutine(**kwargs) if on_separate_thread:
self.coroutines.append(coroutine) if on_separate_thread is False else self.coroutine_threads.append(coroutine) self.coroutine_threads[coroutine] = kwargs
else:
self.coroutines[coroutine] = kwargs
def add_strategies(self, *, strategies: tuple[Strategy]): def add_strategies(self, *, strategies: tuple[Strategy]):
"""Add multiple strategies at once """Add multiple strategies at once
@@ -60,12 +60,8 @@ class Executor:
""" """
self.strategy_runners.append(strategy) self.strategy_runners.append(strategy)
# async def create_strategy_task(self, strategy: Strategy): @staticmethod
# task = asyncio.create_task(strategy.run_strategy()) def run_strategy(strategy: Strategy):
# self.tasks.append(task)
# await task
def run_strategy(self, strategy: Strategy):
"""Wraps the coroutine trade method of each strategy with 'asyncio.run'. """Wraps the coroutine trade method of each strategy with 'asyncio.run'.
Args: Args:
@@ -73,38 +69,23 @@ class Executor:
""" """
asyncio.run(strategy.run_strategy()) asyncio.run(strategy.run_strategy())
async def create_coroutine_task(self, coroutine: Coroutine): async def run_coroutine_tasks(self):
task = asyncio.create_task(coroutine)
self.tasks.append(task)
await task
async def create_coroutines_task(self):
""""""
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
def run_coroutine_tasks(self):
"""Run all coroutines in the executor""" """Run all coroutines in the executor"""
asyncio.run(self.create_coroutines_task()) await asyncio.gather(*[coroutine(**kwargs) for coroutine, kwargs in self.coroutines.items()],
return_exceptions=True)
def run_coroutine_task(self, coroutine): @staticmethod
asyncio.run(self.create_coroutine_task(coroutine)) def run_coroutine_task(coroutine, kwargs):
asyncio.run(coroutine(**kwargs))
@staticmethod @staticmethod
def run_function(function: Callable, kwargs: dict): def run_function(function: Callable, kwargs: dict):
"""Run a function """Run a function
Args: Args:
function: The function to run function: The function to run
kwargs: A dictionary of keyword arguments for the function kwargs: A dictionary of keyword arguments for the function
""" """
try: function(**kwargs)
function(**kwargs)
except Exception as err:
logger.error(f"Error: {err}. Unable to run function: {function.__name__}")
def sigint_handle(self, signum, frame): def sigint_handle(self, signum, frame):
self.config.shutdown = True self.config.shutdown = True
@@ -117,19 +98,20 @@ class Executor:
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
break break
timeout = self.timeout or 30
timeout = 1 if self.timeout else 30
await asyncio.sleep(timeout) await asyncio.sleep(timeout)
for strategy in self.strategy_runners: for strategy in self.strategy_runners:
strategy.running = False strategy.running = False
self.config.task_queue.cancel()
if self.config.backtest_engine is not None: if self.config.backtest_engine is not None:
self.config.backtest_engine.stop_testing = True self.config.backtest_engine.stop_testing = True
self.executor.shutdown(wait=False, cancel_futures=False) self.executor.shutdown(wait=False, cancel_futures=False)
for task in self.tasks:
task.cancel()
if self.config.force_shutdown: if self.config.force_shutdown:
os._exit(1) os._exit(1)
except Exception as err: except Exception as err:
@@ -151,5 +133,6 @@ class Executor:
self.executor = executor self.executor = executor
[self.executor.submit(self.run_strategy, strategy) for strategy in self.strategy_runners] [self.executor.submit(self.run_strategy, strategy) for strategy in self.strategy_runners]
[self.executor.submit(function, **kwargs) for function, kwargs in self.functions.items()] [self.executor.submit(function, **kwargs) for function, kwargs in self.functions.items()]
[self.executor.submit(self.run_coroutine_task, coroutine) for coroutine in self.coroutine_threads] [self.executor.submit(self.run_coroutine_task, coroutine, kwargs) for coroutine, kwargs in
self.executor.submit(self.run_coroutine_tasks) self.coroutine_threads.items()]
self.executor.submit(asyncio.run, self.run_coroutine_tasks())
+9 -1
View File
@@ -177,17 +177,25 @@ class Strategy(ABC):
try: try:
await self.sessions.check() await self.sessions.check()
await self.trade() await self.trade()
except StopTrading: except StopTrading:
self.running = False self.running = False
break break
except asyncio.CancelledError:
self.running = False
break
except Exception as err: except Exception as err:
logger.error("Error: %s in live_strategy", err) logger.error("Error: %s in live_strategy", err)
return self.running = False
break
async def backtest_strategy(self): async def backtest_strategy(self):
"""Backtest the strategy.""" """Backtest the strategy."""
async with self as _: async with self as _:
logger.info("Testing %s strategy on %s with Backtester", self.name, self.symbol.name) logger.info("Testing %s strategy on %s with Backtester", self.name, self.symbol.name)
await self.initialize()
while self.running: while self.running:
try: try:
await self.sessions.check() await self.sessions.check()
+40 -7
View File
@@ -40,6 +40,7 @@ class Symbol(_Base, SymbolInfo):
assert "name" in kwargs, "Symbol Object Must be initialized with a name" assert "name" in kwargs, "Symbol Object Must be initialized with a name"
super().__init__(**kwargs) super().__init__(**kwargs)
self.account = Account() self.account = Account()
self.initialized = False
@backoff_decorator @backoff_decorator
async def info_tick(self, *, name: str = "") -> Tick | None: async def info_tick(self, *, name: str = "") -> Tick | None:
@@ -63,7 +64,6 @@ class Symbol(_Base, SymbolInfo):
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
except Exception as err: except Exception as err:
logger.warning("%s: Unable to get tick for %s", err, self.name) logger.warning("%s: Unable to get tick for %s", err, self.name)
return None return None
@@ -92,8 +92,7 @@ class Symbol(_Base, SymbolInfo):
info = await self.mt5.symbol_info(self.name) info = await self.mt5.symbol_info(self.name)
if info is not None: if info is not None:
info = info._asdict() info = info._asdict()
info["swap_rollover3days"] = info.get("swap_rollover3days", 0) % 7 # self.set_attributes(**info)
self.set_attributes(**info)
return SymbolInfo(**info) return SymbolInfo(**info)
return None return None
@@ -104,10 +103,44 @@ class Symbol(_Base, SymbolInfo):
bool: Returns True if symbol info was successful initialized bool: Returns True if symbol info was successful initialized
""" """
try: try:
await self.symbol_select() select = await self.mt5.symbol_select(self.name, True)
info = await self.info() self.select = select
info_tick = await self.info_tick()
await self.book_add() await self.mt5.market_book_add(self.name)
info = await self.mt5.symbol_info(self.name)
if info is not None:
self.set_attributes(**info._asdict())
info_tick = await self.mt5.symbol_info_tick(self.name)
if info_tick:
self.tick = Tick(**info_tick._asdict())
if info is not None and info_tick is not None:
self.initialized = True
return True
logger.warning("Unable to initialize %s", self.name)
return False
except Exception as err:
logger.warning("%s: Unable to initialize %s", err, self.name)
return False
def initialize_sync(self) -> bool:
"""Synchronous version of the initialize method"""
try:
select = self.mt5._symbol_select(self.name, True)
self.select = select
self.mt5._market_book_add(self.name)
info = self.mt5._symbol_info(self.name)
if info is not None:
self.set_attributes(**info._asdict())
info_tick = self.mt5._symbol_info_tick(self.name)
if info_tick:
self.tick = Tick(**info_tick._asdict())
if info is not None and info_tick is not None: if info is not None and info_tick is not None:
self.initialized = True self.initialized = True
return True return True
+5 -2
View File
@@ -4,17 +4,20 @@ from aiomql.lib.bot import Bot
from aiomql.contrib.strategies import Chaos from aiomql.contrib.strategies import Chaos
from aiomql.contrib.symbols import ForexSymbol from aiomql.contrib.symbols import ForexSymbol
logger = logging.getLogger(__name__)
async def test_bot(): async def test_bot():
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s") logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s")
syms = ["BTCUSD", "SOLUSD", "ETHUSD"] syms = ["BTCUSD", "SOLUSD", "ETHUSD"]
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", params={"interval": 3}) for symbol in symbols]
bot = Bot() bot = Bot()
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()
assert len(bot.executor.coroutines) == 1 assert len(bot.executor.coroutines) == 1
assert len(bot.executor.coroutine_threads) == 1 assert len(bot.executor.coroutine_threads) == 1
assert len(bot.executor.strategy_runners) == 3
assert bot.config.shutdown is True assert bot.config.shutdown is True
+1 -2
View File
@@ -9,9 +9,8 @@ def test_bot_sync():
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s") logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s")
syms = ["BTCUSD", "SOLUSD", "ETHUSD"] syms = ["BTCUSD", "SOLUSD", "ETHUSD"]
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", params={"interval": 3}) for symbol in symbols]
bot = Bot() bot = Bot()
bot.config.task_queue.worker_timeout = 2
bot.executor.timeout = 10 bot.executor.timeout = 10
bot.add_strategies(strategies=strategies) bot.add_strategies(strategies=strategies)
bot.initialize_sync() bot.initialize_sync()
+1 -5
View File
@@ -6,7 +6,7 @@ from aiomql.core.task_queue import TaskQueue, QueueItem
class TestTaskQueue: class TestTaskQueue:
@classmethod @classmethod
def setup_class(cls): def setup_class(cls):
cls.task_queue = TaskQueue(timeout=5, worker_timeout=1) cls.task_queue = TaskQueue(absolute_timeout=6, worker_timeout=1)
cls.data = {} cls.data = {}
async def task_one(self): async def task_one(self):
@@ -27,14 +27,10 @@ class TestTaskQueue:
async def test_queue(self): async def test_queue(self):
item_one = QueueItem(self.task_one) item_one = QueueItem(self.task_one)
self.task_queue.add(item=item_one, must_complete=False) self.task_queue.add(item=item_one, must_complete=False)
assert len(self.task_queue.priority_tasks) == 0
assert self.task_queue.queue.qsize() == 1 assert self.task_queue.queue.qsize() == 1
self.task_queue.add(item=QueueItem(self.task_two), must_complete=True) self.task_queue.add(item=QueueItem(self.task_two), must_complete=True)
assert len(self.task_queue.priority_tasks) == 1
assert self.task_queue.queue.qsize() == 2 assert self.task_queue.queue.qsize() == 2
self.task_queue.add(item=QueueItem(self.task_three), must_complete=False) self.task_queue.add(item=QueueItem(self.task_three), must_complete=False)
await self.task_queue.run() await self.task_queue.run()
assert len(self.data["task_one"]) >= 2 assert len(self.data["task_one"]) >= 2
assert len(self.data["task_two"]) == 10 assert len(self.data["task_two"]) == 10
assert len(self.data["task_three"]) == 1
assert len(self.task_queue.priority_tasks) == 0