This commit is contained in:
Ichinga Samuel
2024-11-23 10:23:01 +01:00
parent 9d9c02fcfd
commit f8c4c6e9a7
19 changed files with 279 additions and 209 deletions
+1 -1
View File
@@ -201,7 +201,7 @@ class EMAXOver(Strategy):
Run the tests with pytest Run the tests with pytest
```bash ```bash
pytest test pytest tests
``` ```
### API Documentation ### API Documentation
@@ -1,17 +1,17 @@
{ {
"balance": 221.44, "balance": 635.28,
"profit": 0, "profit": 0,
"equity": 221.44, "equity": 635.28,
"margin": 0.0, "margin": 0.0,
"margin_free": 221.44, "margin_free": 635.28,
"margin_level": 0, "margin_level": 0,
"wins": 11, "wins": 29,
"losses": 54, "losses": 40,
"total": 65, "total": 69,
"win_percentage": 16.92, "win_percentage": 42.03,
"win": 85.57, "win": 847.84,
"loss": -214.13, "loss": -562.56,
"net_profit": -128.56, "net_profit": 285.28,
"profit_factor": 0.4, "profit_factor": 1.51,
"profitability": -36.73 "profitability": 81.51
} }
+9 -11
View File
@@ -29,9 +29,7 @@ A single instance of this class is created and used per bot instance.
| `root` | `Path` | The root directory of the project | | `root` | `Path` | The root directory of the project |
| `record_trades` | `bool` | To record trades or not. Default is True | | `record_trades` | `bool` | To record trades or not. Default is True |
| `records_dir` | `Path` | The directory to store trade records, relative to the root directory | | `records_dir` | `Path` | The directory to store trade records, relative to the root directory |
| `records_dir_name` | `str` | The name of the trade records directory |
| `backtest_dir` | `Path` | The directory to store backtest results, relative to the root directory | | `backtest_dir` | `Path` | The directory to store backtest results, relative to the root directory |
| `backtest_dir_name` | `str` | The name of the backtest directory |
| `task_queue` | `TaskQueue` | The TaskQueue object for handling background tasks | | `task_queue` | `TaskQueue` | The TaskQueue object for handling background tasks |
| `_backtest_engine` | `BackTestEngine` | The backtest engine object | | `_backtest_engine` | `BackTestEngine` | The backtest engine object |
| `bot` | `Bot` | The bot object | | `bot` | `Bot` | The bot object |
@@ -71,8 +69,8 @@ def backtest_engine(self)
Returns the backtest engine object. Returns the backtest engine object.
#### Returns: #### Returns:
| Type | Description | | Type | Description |
|-----------------|------------------------| |------------------|----------------------------|
| `BackTestEngine` | The backtest engine object | | `BackTestEngine` | The backtest engine object |
@@ -98,14 +96,14 @@ Set attributes on the config object. The root folder attribute can't be set here
<a id="config.load_config"></a> <a id="config.load_config"></a>
### load_config ### load_config
```python ```python
def load_config(*, file: str | Path = None, filename: str = None, root: str | Path = None, **kwargs) -> Config def load_config(*, config_file: str | Path = None, filename: str = None, root: str | Path = None, **kwargs) -> Config
``` ```
Load configuration settings from a file and reset the config object. Load configuration settings from a file and reset the config object.
#### Parameters: #### Parameters:
| Name | Type | Description | | Name | Type | Description |
|------------|---------------|----------------------------------------------------------------------------------------------------| |---------------|---------------|----------------------------------------------------------------------------------------------------|
| `file` | `str \| Path` | The absolute path to the config file. | | `config_file` | `str \| Path` | The absolute path to the config file. |
| `filename` | `str` | The name of the file to load if file path is not specified. If not provided `aiomql.json` is used. | | `filename` | `str` | The name of the file to load if file path is not specified. If not provided `aiomql.json` is used. |
| `root` | `str` | The root directory of the project. | | `root` | `str` | The root directory of the project. |
| `**kwargs` | `dict` | Additional keyword arguments to be set on the config object. | | `**kwargs` | `dict` | Additional keyword arguments to be set on the config object. |
+1 -1
View File
@@ -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, stop_time=stop_time, back_test_engine = BackTestEngine(start=start, end=end, speed=3600,
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)
+5 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "aiomql" name = "aiomql"
version = "4.1.0" version = "4.0.1"
readme = "README.md" readme = "README.md"
requires-python = ">=3.11" requires-python = ">=3.11"
classifiers = [ classifiers = [
@@ -12,10 +12,14 @@ classifiers = [
"License :: OSI Approved :: MIT License", "License :: OSI Approved :: MIT License",
"Operating System :: OS Independent", "Operating System :: OS Independent",
] ]
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"]
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"]
authors = [{name = "Ichinga Samuel", email = "ichingasamuel@gmail.com"}] authors = [{name = "Ichinga Samuel", email = "ichingasamuel@gmail.com"}]
description = "Asynchronous MetaTrader5 library and Algorithmic Trading Framework" description = "Asynchronous MetaTrader5 library and Algorithmic Trading Framework"
[project.urls] [project.urls]
+72 -70
View File
@@ -88,76 +88,6 @@ class BackTestEngine:
assign_to_config: bool = True, assign_to_config: bool = True,
account_info: dict = None, account_info: dict = None,
): ):
"""The BackTestEngine class is used to simulate trading strategies on historical data.
It can accept already saved data or create new data for backtesting on the fly. Ideally only one instance of
this class should be created per session. By default it is automatically assigned to the global config instance
during instantiation, replacing any existing backtest engine instance. But this is a configurable behaviour.
The start and end time can still be specified even when test data is provided. In that case it will be used
to set the range of the backtest.
Args:
data (BackTestData, optional): The data to use for backtesting. Defaults to None.
speed (int, optional): The speed of the backtest. Defaults to 60 seconds.
start (float | datetime, optional): The start time of the backtest. Defaults to 0. If a float is passed,
it is assumed to be a timestamp.
end (float | datetime, optional): The end time of the backtest. Defaults to 0. If a float is passed,
it is assumed to be a timestamp.
restart (bool, optional): Whether to restart the backtest from the beginning. Defaults to True.
This is useful when resuming a backtest using a saved BackTestData instance.
use_terminal (bool, optional): Whether to use the terminal for backtesting. Defaults to None. If None,
it uses the global config setting. If use terminal is true, the backtest engine will use the terminal to
get price data, compute margins, profit and check order viability. If false, it will use the data
provided in the BackTestData instance and default algorithm for the calculations
name (str, optional): The name of the backtest. Defaults to "". If not provided,
it is generated from the start and end times.
stop_time (float | datetime, optional): The time to stop the backtest. Defaults to None.
If a float is passed, it is assumed to be a timestamp. If not given it is assumed to be the end of the backtest range.
close_open_positions_on_exit (bool, optional): Whether to close all open positions when the backtest
is stopped. Defaults to True.
preload (bool, optional): Whether to preload the ticks for the backtest. Defaults to True.
assign_to_config (bool, optional): Whether to assign the backtest engine to the global config instance.
Defaults to True.
account_info (dict, optional): A dictionary of account information to use for the backtest. Defaults to None. Use this to set
the account information for the backtest.
Attributes:
_data (BackTestData): The data used for backtesting. This is the data that is saved to disk when the
backtest is stopped.
mt5 (MetaTrader): The MetaTrader instance for the backtest engine.
config (Config): The global configuration instance.
name (str): The name of the backtest.
stop_testing (bool): Whether to stop the backtest.
use_terminal (bool): Whether to use the terminal for backtesting.
close_open_positions_on_exit (bool): Whether to close all open positions when the backtest is stopped.
stop_time (int): The time to stop the backtest.
preload (bool): Whether to preload the ticks for the backtest.
preloaded_ticks (dict): A dictionary of preloaded ticks for the backtest.
account_lock (RLock): A reentrant lock for the account data.
account_info (dict): A dictionary of account information for the backtest.
"""
self._data = data or BackTestData() self._data = data or BackTestData()
self.mt5 = MetaTrader() self.mt5 = MetaTrader()
self.config = self.mt5.config self.config = self.mt5.config
@@ -1523,3 +1453,75 @@ class BackTestEngine:
return self.deals.history_deals_get( return self.deals.history_deals_get(
date_from=date_from, date_to=date_to, group=group, position=position, ticket=ticket date_from=date_from, date_to=date_to, group=group, position=position, ticket=ticket
) )
BackTestEngine.__doc__ = """The BackTestEngine class is used to simulate trading strategies on historical data.
It can accept already saved data or create new data for backtesting on the fly. Ideally only one instance of
this class should be created per session. By default it is automatically assigned to the global config instance
during instantiation, replacing any existing backtest engine instance. But this is a configurable behaviour.
The start and end time can still be specified even when test data is provided. In that case it will be used
to set the range of the backtest.
Args:
data (BackTestData, optional): The data to use for backtesting. Defaults to None.
speed (int, optional): The speed of the backtest. Defaults to 60 seconds.
start (float | datetime, optional): The start time of the backtest. Defaults to 0. If a float is passed,
it is assumed to be a timestamp.
end (float | datetime, optional): The end time of the backtest. Defaults to 0. If a float is passed,
it is assumed to be a timestamp.
restart (bool, optional): Whether to restart the backtest from the beginning. Defaults to True.
This is useful when resuming a backtest using a saved BackTestData instance.
use_terminal (bool, optional): Whether to use the terminal for backtesting. Defaults to None. If None,
it uses the global config setting. If use terminal is true, the backtest engine will use the terminal to
get price data, compute margins, profit and check order viability. If false, it will use the data
provided in the BackTestData instance and default algorithm for the calculations
name (str, optional): The name of the backtest. Defaults to "". If not provided,
it is generated from the start and end times.
stop_time (float | datetime, optional): The time to stop the backtest. Defaults to None.
If a float is passed, it is assumed to be a timestamp. If not given it is assumed to be the end of the backtest range.
close_open_positions_on_exit (bool, optional): Whether to close all open positions when the backtest
is stopped. Defaults to True.
preload (bool, optional): Whether to preload the ticks for the backtest. Defaults to True.
assign_to_config (bool, optional): Whether to assign the backtest engine to the global config instance.
Defaults to True.
account_info (dict, optional): A dictionary of account information to use for the backtest. Defaults to None. Use this to set
the account information for the backtest.
Attributes:
_data (BackTestData): The data used for backtesting. This is the data that is saved to disk when the
backtest is stopped.
mt5 (MetaTrader): The MetaTrader instance for the backtest engine.
config (Config): The global configuration instance.
name (str): The name of the backtest.
stop_testing (bool): Whether to stop the backtest.
use_terminal (bool): Whether to use the terminal for backtesting.
close_open_positions_on_exit (bool): Whether to close all open positions when the backtest is stopped.
stop_time (int): The time to stop the backtest.
preload (bool): Whether to preload the ticks for the backtest.
preloaded_ticks (dict): A dictionary of preloaded ticks for the backtest.
account_lock (RLock): A reentrant lock for the account data.
account_info (dict): A dictionary of account information for the backtest.
"""
+90 -94
View File
@@ -1,6 +1,6 @@
import os import os
from pathlib import Path from pathlib import Path
from typing import Iterator, Literal, TypeVar, Self from typing import Literal, TypeVar, Self
import json import json
from logging import getLogger from logging import getLogger
@@ -12,41 +12,6 @@ BackTestEngine = TypeVar("BackTestEngine")
class Config: class Config:
"""A class for handling configuration settings for the aiomql package.
Attributes:
login (int): The account login number
trade_record_mode (Literal["csv", "json"]): The mode for recording trades
password (str): The account password
server (str): The account server
path (str | Path): The path to the terminal
timeout (int): The timeout argument for the terminal
filename (str): The filename of the config file
state (dict): The
root (Path): The root directory of the project
record_trades (bool): To record trades or not. Default is True
records_dir (Path): The directory to store trade records, relative to the root directory
records_dir_name (str): The name of the trade records directory
backtest_dir (Path): The directory to store backtest results, relative to the root directory
backtest_dir_name (str): The name of the backtest directory
task_queue (TaskQueue): The TaskQueue object for handling background tasks
_backtest_engine (BackTestEngine): The backtest engine object
bot (Bot): The bot object
_instance (Self): The instance of the Config class
mode (Literal["backtest", "live"]): The trading mode, either backtest or live, default is live
use_terminal_for_backtesting (bool): Use the terminal for backtesting, default is True
shutdown (bool): A signal to shut down the terminal, default is False
force_shutdown (bool): A signal to force shut down the terminal, default is False
Notes:
By default, the config class looks for a file named aiomql.json. This can be changed by setting the filename
attribute to the desired file name. The root directory of the project can be set by passing the root argument
to the load_config method or during object instantiation. If not provided it is assumed to be the current working
directory. All directories and files are assumed to be relative to the root directory except when an absolute path
is provided, this includes the config file, the records_dir and the backtest_dir attributes.
The root directory is used to locate the config file and to set the records_dir and backtest_dir attributes.
"""
login: int login: int
trade_record_mode: Literal["csv", "json"] trade_record_mode: Literal["csv", "json"]
password: str password: str
@@ -59,9 +24,7 @@ class Config:
root: Path root: Path
record_trades: bool record_trades: bool
records_dir: Path records_dir: Path
records_dir_name: str
backtest_dir: Path backtest_dir: Path
backtest_dir_name: str
task_queue: TaskQueue task_queue: TaskQueue
_backtest_engine: BackTestEngine _backtest_engine: BackTestEngine
bot: Bot bot: Bot
@@ -77,17 +40,14 @@ class Config:
"trade_record_mode": "csv", "trade_record_mode": "csv",
"mode": "live", "mode": "live",
"filename": "aiomql.json", "filename": "aiomql.json",
"records_dir_name": "trade_records",
"backtest_dir_name": "backtesting",
"use_terminal_for_backtesting": True, "use_terminal_for_backtesting": True,
"path": "", "path": "",
"login": 0, "login": 0,
"password": "", "password": "",
"server": "", "server": "",
"records_dir": None,
"shutdown": False, "shutdown": False,
"force_shutdown": False, "force_shutdown": False,
"root": '.', "root": None,
} }
def __new__(cls, *args, **kwargs): def __new__(cls, *args, **kwargs):
@@ -97,15 +57,15 @@ class Config:
cls._instance.task_queue = TaskQueue() cls._instance.task_queue = TaskQueue()
cls._instance.set_attributes(**cls._defaults) cls._instance.set_attributes(**cls._defaults)
cls._instance._backtest_engine = None cls._instance._backtest_engine = None
cls._instance.load_config(**kwargs) # cls._instance.load_config(**kwargs)
return cls._instance return cls._instance
def __init__(self, **kwargs): def __init__(self, **kwargs):
"""Initialize the Config object. The root directory can be set here or in the load_config method.""" """Initialize the Config object. The root directory can be set here or in the load_config method."""
root = kwargs.pop("root", None) root = kwargs.pop("root", None)
config_file = kwargs.pop("config_file", None) config_file = kwargs.pop("config_file", None)
if root is not None or config_file is not None: if self.root is None or root is not None or config_file is not None:
self.load_config(root=root, file=config_file, **kwargs) self.load_config(root=root, config_file=config_file, **kwargs)
else: else:
self.set_attributes(**kwargs) self.set_attributes(**kwargs)
@@ -125,45 +85,40 @@ class Config:
Args: Args:
**kwargs: Object attributes and values as keyword arguments **kwargs: Object attributes and values as keyword arguments
""" """
if kwargs.pop("root", None) is not None: if kwargs.get("root", None) is not None:
kwargs.pop("root", None)
logger.debug("Tried setting root from set_attributes. Use load_config to change project root") logger.debug("Tried setting root from set_attributes. Use load_config to change project root")
if kwargs.get("config_file", None) is not None:
kwargs.pop("config_file", None)
logger.debug("Tried setting config_file from set_attributes. Use load_config to change project root")
[setattr(self, key, value) for key, value in kwargs.items()] [setattr(self, key, value) for key, value in kwargs.items()]
@staticmethod
def walk_to_root(path: str | Path) -> Iterator[str]:
if not os.path.exists(path):
raise IOError("Starting path not found")
if os.path.isfile(path):
path = os.path.dirname(path)
last_dir = None
current_dir = os.path.abspath(path)
while last_dir != current_dir:
yield current_dir
parent_dir = os.path.abspath(os.path.join(current_dir, os.path.pardir))
last_dir, current_dir = current_dir, parent_dir
def find_config_file(self): def find_config_file(self):
try: try:
current = Path.cwd() current = Path.cwd()
parents = current.parents current = os.path.commonpath([current, self.root])
for dir in parents: current = Path(current).resolve()
config_file = current / self.filename
if config_file.exists():
return config_file
if current == self.root:
return
for dirname in current.parents:
config_file = dirname / self.filename
if config_file.exists():
return config_file
for dirname in self.walk_to_root(self.root): if self.root == dirname:
check_path = os.path.join(dirname, self.filename) break
if os.path.isfile(check_path):
return check_path
return None
except Exception as err: except Exception as err:
logger.debug(f"Error finding config file: {err}") logger.debug(f"Error finding config file: {err}")
return
def load_config(self, *, file: str | Path = None, filename: str = None, root: str | Path = None, **kwargs) -> Self:
def load_config(self, *, config_file: str | Path = None, filename: str = None, root: str | Path = None, **kwargs) -> Self:
"""Load configuration settings from a file and reset the config object. """Load configuration settings from a file and reset the config object.
Args: Args:
file (str | Path): The absolute path to the config file. config_file (str | Path): The absolute path to the config file.
filename (str): The name of the file to load if file path is not specified. If not provided aiomql.json is used filename (str): The name of the file to load if file path is not specified. If not provided aiomql.json is used
root (str): The root directory of the project. root (str): The root directory of the project.
**kwargs: Additional keyword arguments to be set on the config object. **kwargs: Additional keyword arguments to be set on the config object.
@@ -173,45 +128,52 @@ class Config:
root.mkdir(parents=True, exist_ok=True) if not root.exists() else ... root.mkdir(parents=True, exist_ok=True) if not root.exists() else ...
self.root = root self.root = root
else: else:
self.root = self.root if hasattr(self, "root") else Path.cwd() self.root = self.root if isinstance(self.root, Path) else Path.cwd()
if file is not None:
file = Path(file).resolve() if config_file is not None:
if not file.exists(): config_file = Path(config_file).resolve()
if not config_file.exists():
self.filename = filename or self.filename self.filename = filename or self.filename
file = self.find_config_file() self.config_file = self.find_config_file()
else: else:
self.filename = file.name self.filename = config_file.name
self.config_file = file self.config_file = config_file
else: else:
self.filename = filename or self.filename self.filename = filename or self.filename
file = self.find_config_file() self.config_file = self.find_config_file()
if file is None: if self.config_file is None:
logger.warning("No Config File Found") logger.debug("No Config File Found")
file_config = {} file_config = {}
else: else:
fh = open(file, mode="r") fh = open(self.config_file, mode="r")
file_config = json.load(fh) file_config = json.load(fh)
fh.close() fh.close()
data = file_config | kwargs data = file_config | kwargs
self.set_attributes(**data) self.set_attributes(**data)
if self.path: try:
self.path = self.root / self.path if not Path(self.path).resolve().exists() else self.path if self.path:
self.path = self.root / self.path if not Path(self.path).resolve().exists() else self.path
if self.record_trades and ( except Exception as err:
hasattr(self, "records_dir") is False or self.records_dir is None or root is not None logger.debug(f"Error setting path: {err}")
): self.path = ""
self.records_dir = self.root / self.records_dir_name
self.records_dir.mkdir(parents=True, exist_ok=True)
if hasattr(self, "backtest_dir") is False or root is not None:
self.backtest_dir = self.root / self.backtest_dir_name
self.backtest_dir.mkdir(parents=True, exist_ok=True)
return self return self
@property
def records_dir(self):
rec_dir = self.root / 'trade_records'
rec_dir.mkdir(parents=True, exist_ok=True) if rec_dir.exists() is False else ...
return rec_dir
@property
def backtest_dir(self) -> Path:
b_dir = self.root / 'backtesting'
b_dir.mkdir(parents=True, exist_ok=True) if b_dir.exists() is False else ...
return b_dir
def account_info(self) -> dict[str, int | str]: def account_info(self) -> dict[str, int | str]:
"""Returns Account login details as found in the config object if available """Returns Account login details as found in the config object if available
@@ -219,3 +181,37 @@ class Config:
dict[str, int | str]: A dictionary of login details dict[str, int | str]: A dictionary of login details
""" """
return {"login": self.login, "password": self.password, "server": self.server} return {"login": self.login, "password": self.password, "server": self.server}
Config.__doc__ = """A class for handling configuration settings for the aiomql package.
Attributes:
login (int): The account login number
trade_record_mode (Literal["csv", "json"]): The mode for recording trades
password (str): The account password
server (str): The account server
path (str | Path): The path to the terminal
timeout (int): The timeout argument for the terminal
filename (str): The filename of the config file
config_file (Path): The config file path
state (dict): The
root (Path): The root directory of the project
record_trades (bool): To record trades or not. Default is True
records_dir (Path): The directory to store trade records, relative to the root directory
backtest_dir (Path): The directory to store backtest results, relative to the root directory
task_queue (TaskQueue): The TaskQueue object for handling background tasks
_backtest_engine (BackTestEngine): The backtest engine object
bot (Bot): The bot object
_instance (Self): The instance of the Config class
mode (Literal["backtest", "live"]): The trading mode, either backtest or live, default is live
use_terminal_for_backtesting (bool): Use the terminal for backtesting, default is True
shutdown (bool): A signal to shut down the terminal, default is False
force_shutdown (bool): A signal to force shut down the terminal, default is False
Notes:
By default, the config class looks for a file named aiomql.json. This can be changed by setting the filename
attribute to the desired file name. The root directory of the project can be set by passing the root argument
to the load_config method or during object instantiation. If not provided it is assumed to be the current working
directory. All directories and files are assumed to be relative to the root directory except when an absolute path
is provided, this includes the config file, the records_dir and the backtest_dir attributes.
The root directory is used to locate the config file and to set the records_dir and backtest_dir attributes.
"""
+1 -1
View File
@@ -158,7 +158,7 @@ class Symbol(_Base, SymbolInfo):
if check := self.volume_min <= volume <= self.volume_max: if check := self.volume_min <= volume <= self.volume_max:
return check, volume return check, volume
else: else:
return (check, self.volume_min if volume <= self.volume_min else self.volume_max) return check, self.volume_min if volume <= self.volume_min else self.volume_max
def round_off_volume(self, *, volume: float, round_down: bool = False) -> float: def round_off_volume(self, *, volume: float, round_down: bool = False) -> float:
"""Round off the volume to the nearest volume step. """Round off the volume to the nearest volume step.
-1
View File
@@ -1,6 +1,5 @@
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from datetime import datetime, UTC from datetime import datetime, UTC
from string import digits
from typing import TypeVar from typing import TypeVar
from logging import getLogger from logging import getLogger
+5 -3
View File
@@ -1,11 +1,12 @@
from datetime import datetime, UTC
import asyncio import asyncio
import json import json
import shutil import shutil
from datetime import datetime, UTC
from logging import getLogger from logging import getLogger
from pathlib import Path from pathlib import Path
import pytest import pytest
from aiomql.core import Config from aiomql.core import Config
from aiomql.core.meta_backtester import MetaBackTester from aiomql.core.meta_backtester import MetaBackTester
from aiomql.core.backtesting.backtest_engine import BackTestEngine from aiomql.core.backtesting.backtest_engine import BackTestEngine
@@ -57,7 +58,7 @@ async def config(request):
data["mode"] = "backtest" data["mode"] = "backtest"
json.dump(data, fh1, indent=2) json.dump(data, fh1, indent=2)
json.dump(data, fh2, indent=2) json.dump(data, fh2, indent=2)
config = Config(filename="test.json", root="tests/backtest") config = Config(config_file="tests/backtest/test.json", root="tests/backtest", filename="test.json")
yield config yield config
await cleanup() await cleanup()
@@ -73,7 +74,8 @@ async def mt():
@pytest.fixture(scope="package") @pytest.fixture(scope="package")
async def period(): async def period():
return {"start": datetime(2024, 2, 1, hour=8, tzinfo=UTC), "end": datetime(2024, 2, 7, hour=16, tzinfo=UTC)} return {"start": datetime(2024, 2, 1, hour=8, tzinfo=UTC),
"end": datetime(2024, 2, 7, hour=16, tzinfo=UTC)}
@pytest.fixture(scope="package") @pytest.fixture(scope="package")
@@ -27,7 +27,8 @@ async def make_buy_sell_orders():
return {"buy": Order(**buy_req), "sell": Order(**sell_req)} return {"buy": Order(**buy_req), "sell": Order(**sell_req)}
def test_trade_mode(config, backtest_engine, history, positions, order_sell, order_buy, btc_usd): def test_trade_mode(config, backtest_engine, history, positions, order_sell, order_buy, btc_usd, capsys):
print(config.filename, config.root)
assert config.mode == "backtest" assert config.mode == "backtest"
assert isinstance(backtest_engine, BackTestEngine) assert isinstance(backtest_engine, BackTestEngine)
assert isinstance(history.mt5, MetaBackTester) assert isinstance(history.mt5, MetaBackTester)
+5
View File
@@ -0,0 +1,5 @@
def test_config(config, capsys):
print(config.filename, config.root, config.mode, config.config_file)
assert 6 == 6
assert config.mode == "backtest"
+1 -1
View File
@@ -53,7 +53,7 @@ async def config(request):
data = json.load(fh) data = json.load(fh)
json.dump(data, fh1, indent=2) json.dump(data, fh1, indent=2)
json.dump(data, fh2, indent=2) json.dump(data, fh2, indent=2)
config = Config(root="tests/live", filename="test.json") config = Config(root="tests/live", config_file="tests/live/test.json")
yield config yield config
await cleanup() await cleanup()
+3
View File
@@ -0,0 +1,3 @@
bid,volume,actual_profit,order,closed,price,ask,deal,win
0.0,0.0,0,0,False,0.0,0.0,0,False
0.0,0.0,0,0,False,0.0,0.0,0,False
1 bid volume actual_profit order closed price ask deal win
2 0.0 0.0 0 0 False 0.0 0.0 0 False
3 0.0 0.0 0 0 False 0.0 0.0 0 False
+24
View File
@@ -0,0 +1,24 @@
[
{
"deal": 0,
"order": 0,
"volume": 0.0,
"price": 0.0,
"bid": 0.0,
"ask": 0.0,
"actual_profit": 0,
"closed": false,
"win": false
},
{
"deal": 0,
"order": 0,
"volume": 0.0,
"price": 0.0,
"bid": 0.0,
"ask": 0.0,
"actual_profit": 0,
"closed": false,
"win": false
}
]
+3
View File
@@ -0,0 +1,3 @@
bid,name,volume,actual_profit,order,closed,ema,price,ask,deal,rsi,win
0.0,test_trades,0.0,0,0,False,20,0.0,0.0,0,14,False
0.0,test_trades,0.0,0,0,False,20,0.0,0.0,0,14,False
1 bid name volume actual_profit order closed ema price ask deal rsi win
2 0.0 test_trades 0.0 0 0 False 20 0.0 0.0 0 14 False
3 0.0 test_trades 0.0 0 0 False 20 0.0 0.0 0 14 False
+30
View File
@@ -0,0 +1,30 @@
[
{
"name": "test_trades",
"ema": 20,
"rsi": 14,
"deal": 0,
"order": 0,
"volume": 0.0,
"price": 0.0,
"bid": 0.0,
"ask": 0.0,
"actual_profit": 0,
"closed": false,
"win": false
},
{
"name": "test_trades",
"ema": 20,
"rsi": 14,
"deal": 0,
"order": 0,
"volume": 0.0,
"price": 0.0,
"bid": 0.0,
"ask": 0.0,
"actual_profit": 0,
"closed": false,
"win": false
}
]
+1 -1
View File
@@ -25,5 +25,5 @@ class TestConfig:
assert "server" in account_info assert "server" in account_info
def test_load_config(self, config): def test_load_config(self, config):
config.load_config(file="tests/live/configs/test2.json") config.load_config(config_file="tests/live/configs/test2.json")
assert config.filename == "test2.json" assert config.filename == "test2.json"
+13 -10
View File
@@ -5,7 +5,8 @@ from aiomql.lib.ram import RAM
from aiomql.contrib.traders import SimpleTrader from aiomql.contrib.traders import SimpleTrader
from aiomql.contrib.symbols import ForexSymbol from aiomql.contrib.symbols import ForexSymbol
from aiomql.core.constants import OrderType from aiomql.core.constants import OrderType
from aiomql.lib.account import Account
from aiomql._utils import round_down
class TestTrader: class TestTrader:
@classmethod @classmethod
@@ -13,11 +14,13 @@ class TestTrader:
ram = RAM(fixed_amount=10) ram = RAM(fixed_amount=10)
cls.trader = SimpleTrader(symbol=ForexSymbol(name="BTCUSD"), ram=ram) cls.trader = SimpleTrader(symbol=ForexSymbol(name="BTCUSD"), ram=ram)
cls.simple_trader2 = SimpleTrader(symbol=ForexSymbol(name="EURJPY"), ram=ram) cls.simple_trader2 = SimpleTrader(symbol=ForexSymbol(name="EURJPY"), ram=ram)
cls.account = Account()
@pytest.fixture(scope="class", autouse=True) @pytest.fixture(scope="class", autouse=True)
async def initialize(self): async def initialize(self):
await self.trader.symbol.initialize() await self.trader.symbol.initialize()
await self.simple_trader2.symbol.initialize() await self.simple_trader2.symbol.initialize()
await self.account.refresh()
async def test_create_order_no_stops(self): async def test_create_order_no_stops(self):
await self.trader.create_order_no_stops(order_type=OrderType.BUY) await self.trader.create_order_no_stops(order_type=OrderType.BUY)
@@ -35,8 +38,8 @@ class TestTrader:
profit = floor(await self.trader.order.calc_profit()) profit = floor(await self.trader.order.calc_profit())
loss = -floor(abs(await self.trader.order.calc_loss())) loss = -floor(abs(await self.trader.order.calc_loss()))
assert profit == -loss * self.trader.ram.risk_to_reward assert profit == -loss * self.trader.ram.risk_to_reward
assert profit == self.trader.ram.fixed_amount * self.trader.ram.risk_to_reward assert abs(profit - self.trader.ram.fixed_amount * self.trader.ram.risk_to_reward) <= 2.5
assert loss == -self.trader.ram.fixed_amount assert abs(abs(loss) - abs(-self.trader.ram.fixed_amount)) <= 2
assert res is not None assert res is not None
assert res.retcode == 10009 assert res.retcode == 10009
@@ -47,8 +50,8 @@ class TestTrader:
profit = floor(await self.trader.order.calc_profit()) profit = floor(await self.trader.order.calc_profit())
loss = -floor(abs(await self.trader.order.calc_loss())) loss = -floor(abs(await self.trader.order.calc_loss()))
assert profit == -loss * self.trader.ram.risk_to_reward assert profit == -loss * self.trader.ram.risk_to_reward
assert profit == self.trader.ram.fixed_amount * self.trader.ram.risk_to_reward assert abs(profit - self.trader.ram.fixed_amount * self.trader.ram.risk_to_reward) <= 2.5
assert loss == -self.trader.ram.fixed_amount assert abs(abs(loss) - abs(-self.trader.ram.fixed_amount)) <= 2
assert res is not None assert res is not None
assert res.retcode == 10009 assert res.retcode == 10009
@@ -60,10 +63,10 @@ class TestTrader:
tp = tick.ask + tp tp = tick.ask + tp
await self.trader.create_order_with_stops(order_type=OrderType.BUY, sl=sl, tp=tp) await self.trader.create_order_with_stops(order_type=OrderType.BUY, sl=sl, tp=tp)
res = await self.trader.order.send() res = await self.trader.order.send()
profit = floor(await self.trader.order.calc_profit())
loss = -floor(abs(await self.trader.order.calc_loss()))
assert profit == -loss * self.trader.ram.risk_to_reward
assert profit == self.trader.ram.fixed_amount * self.trader.ram.risk_to_reward
assert loss == -self.trader.ram.fixed_amount
assert res is not None assert res is not None
assert res.retcode == 10009 assert res.retcode == 10009
profit = round(await self.trader.order.calc_profit(), self.account.currency_digits)
loss = -round(abs(await self.trader.order.calc_loss()), self.account.currency_digits)
assert profit == -loss * self.trader.ram.risk_to_reward
assert abs(profit - (self.trader.ram.fixed_amount * self.trader.ram.risk_to_reward)) <= 2.5
assert abs(abs(loss) - self.trader.ram.fixed_amount) <= 2.5