From f8c4c6e9a72acd2ca2a83f383a2c76d6e1099b80 Mon Sep 17 00:00:00 2001 From: Ichinga Samuel Date: Sat, 23 Nov 2024 10:23:01 +0100 Subject: [PATCH] v4.0.1 --- README.md | 2 +- .../backtest_data_01_05_24_06_05_24.json | 24 +-- docs/core/config.md | 20 +- examples/sample_backtester.py | 2 +- pyproject.toml | 6 +- .../core/backtesting/backtest_engine.py | 142 ++++++------- src/aiomql/core/config.py | 186 +++++++++--------- src/aiomql/lib/symbol.py | 2 +- src/aiomql/lib/trader.py | 1 - tests/backtest/conftest.py | 8 +- .../backtest/integration/test_backtesting.py | 3 +- tests/backtest/unit/test_config.py | 5 + tests/live/conftest.py | 2 +- tests/live/records_dir/test_result.csv | 3 + tests/live/records_dir/test_result.json | 24 +++ tests/live/records_dir/test_trades.csv | 3 + tests/live/records_dir/test_trades.json | 30 +++ tests/live/unit/test_config.py | 2 +- tests/live/unit/test_trader.py | 23 ++- 19 files changed, 279 insertions(+), 209 deletions(-) create mode 100644 tests/backtest/unit/test_config.py create mode 100644 tests/live/records_dir/test_result.csv create mode 100644 tests/live/records_dir/test_result.json create mode 100644 tests/live/records_dir/test_trades.csv create mode 100644 tests/live/records_dir/test_trades.json diff --git a/README.md b/README.md index a343ae5..5b5731c 100644 --- a/README.md +++ b/README.md @@ -201,7 +201,7 @@ class EMAXOver(Strategy): Run the tests with pytest ```bash -pytest test +pytest tests ``` ### API Documentation diff --git a/backtesting/backtest_data_01_05_24_06_05_24.json b/backtesting/backtest_data_01_05_24_06_05_24.json index 17d4152..d387040 100644 --- a/backtesting/backtest_data_01_05_24_06_05_24.json +++ b/backtesting/backtest_data_01_05_24_06_05_24.json @@ -1,17 +1,17 @@ { - "balance": 221.44, + "balance": 635.28, "profit": 0, - "equity": 221.44, + "equity": 635.28, "margin": 0.0, - "margin_free": 221.44, + "margin_free": 635.28, "margin_level": 0, - "wins": 11, - "losses": 54, - "total": 65, - "win_percentage": 16.92, - "win": 85.57, - "loss": -214.13, - "net_profit": -128.56, - "profit_factor": 0.4, - "profitability": -36.73 + "wins": 29, + "losses": 40, + "total": 69, + "win_percentage": 42.03, + "win": 847.84, + "loss": -562.56, + "net_profit": 285.28, + "profit_factor": 1.51, + "profitability": 81.51 } \ No newline at end of file diff --git a/docs/core/config.md b/docs/core/config.md index 3b85f7c..a0d3636 100644 --- a/docs/core/config.md +++ b/docs/core/config.md @@ -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 | | `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 | @@ -71,8 +69,8 @@ def backtest_engine(self) Returns the backtest engine object. #### Returns: -| Type | Description | -|-----------------|------------------------| +| Type | Description | +|------------------|----------------------------| | `BackTestEngine` | The backtest engine object | @@ -98,14 +96,14 @@ Set attributes on the config object. The root folder attribute can't be set here ### load_config ```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. #### Parameters: -| Name | Type | Description | -|------------|---------------|----------------------------------------------------------------------------------------------------| -| `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. | -| `root` | `str` | The root directory of the project. | -| `**kwargs` | `dict` | Additional keyword arguments to be set on the config object. | +| Name | Type | Description | +|---------------|---------------|----------------------------------------------------------------------------------------------------| +| `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. | +| `root` | `str` | The root directory of the project. | +| `**kwargs` | `dict` | Additional keyword arguments to be set on the config object. | diff --git a/examples/sample_backtester.py b/examples/sample_backtester.py index 2aab074..6dc3232 100644 --- a/examples/sample_backtester.py +++ b/examples/sample_backtester.py @@ -17,7 +17,7 @@ def back_tester(): start = datetime(2024, 5, 1, tzinfo=UTC) stop_time = datetime(2024, 5, 2, tzinfo=UTC) end = datetime(2024, 5, 7, tzinfo=UTC) - back_test_engine = BackTestEngine(start=start, end=end, speed=3600, 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, account_info={"balance": 350}) backtester = BackTester(backtest_engine=back_test_engine) diff --git a/pyproject.toml b/pyproject.toml index 7f2611c..5d507a7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "aiomql" -version = "4.1.0" +version = "4.0.1" readme = "README.md" requires-python = ">=3.11" classifiers = [ @@ -12,10 +12,14 @@ classifiers = [ "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", ] + keywords = ["MetaTrader5", "Asynchronous", "Algorithmic Trading", "Trading Bot", "Backtesting", "Technical Analysis", "Forex", "Stocks", "Cryptocurrency", "Futures", "Options"] + dependencies = ["MetaTrader5>=5.0.37", "pandas>=1.5.0", "pandas-ta>=0.3.14b0"] + authors = [{name = "Ichinga Samuel", email = "ichingasamuel@gmail.com"}] + description = "Asynchronous MetaTrader5 library and Algorithmic Trading Framework" [project.urls] diff --git a/src/aiomql/core/backtesting/backtest_engine.py b/src/aiomql/core/backtesting/backtest_engine.py index 8a6f566..3a1e2c1 100644 --- a/src/aiomql/core/backtesting/backtest_engine.py +++ b/src/aiomql/core/backtesting/backtest_engine.py @@ -88,76 +88,6 @@ class BackTestEngine: assign_to_config: bool = True, 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.mt5 = MetaTrader() self.config = self.mt5.config @@ -1523,3 +1453,75 @@ class BackTestEngine: return self.deals.history_deals_get( 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. + + """ diff --git a/src/aiomql/core/config.py b/src/aiomql/core/config.py index 3e7fb43..49300a0 100644 --- a/src/aiomql/core/config.py +++ b/src/aiomql/core/config.py @@ -1,6 +1,6 @@ import os from pathlib import Path -from typing import Iterator, Literal, TypeVar, Self +from typing import Literal, TypeVar, Self import json from logging import getLogger @@ -12,41 +12,6 @@ BackTestEngine = TypeVar("BackTestEngine") 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 trade_record_mode: Literal["csv", "json"] password: str @@ -59,9 +24,7 @@ class Config: root: Path record_trades: bool records_dir: Path - records_dir_name: str backtest_dir: Path - backtest_dir_name: str task_queue: TaskQueue _backtest_engine: BackTestEngine bot: Bot @@ -77,17 +40,14 @@ class Config: "trade_record_mode": "csv", "mode": "live", "filename": "aiomql.json", - "records_dir_name": "trade_records", - "backtest_dir_name": "backtesting", "use_terminal_for_backtesting": True, "path": "", "login": 0, "password": "", "server": "", - "records_dir": None, "shutdown": False, "force_shutdown": False, - "root": '.', + "root": None, } def __new__(cls, *args, **kwargs): @@ -97,15 +57,15 @@ class Config: cls._instance.task_queue = TaskQueue() cls._instance.set_attributes(**cls._defaults) cls._instance._backtest_engine = None - cls._instance.load_config(**kwargs) + # cls._instance.load_config(**kwargs) return cls._instance def __init__(self, **kwargs): """Initialize the Config object. The root directory can be set here or in the load_config method.""" root = kwargs.pop("root", None) config_file = kwargs.pop("config_file", None) - if root is not None or config_file is not None: - self.load_config(root=root, file=config_file, **kwargs) + if self.root is None or root is not None or config_file is not None: + self.load_config(root=root, config_file=config_file, **kwargs) else: self.set_attributes(**kwargs) @@ -125,45 +85,40 @@ class Config: Args: **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") + 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()] - @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): try: current = Path.cwd() - parents = current.parents - for dir in parents: - - for dirname in self.walk_to_root(self.root): - check_path = os.path.join(dirname, self.filename) - if os.path.isfile(check_path): - return check_path - return None + current = os.path.commonpath([current, self.root]) + 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 + + if self.root == dirname: + break except Exception as 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. 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 root (str): The root directory of the project. **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 ... self.root = root else: - self.root = self.root if hasattr(self, "root") else Path.cwd() - if file is not None: - file = Path(file).resolve() - if not file.exists(): + self.root = self.root if isinstance(self.root, Path) else Path.cwd() + + if config_file is not None: + config_file = Path(config_file).resolve() + if not config_file.exists(): self.filename = filename or self.filename - file = self.find_config_file() + self.config_file = self.find_config_file() else: - self.filename = file.name - self.config_file = file + self.filename = config_file.name + self.config_file = config_file else: self.filename = filename or self.filename - file = self.find_config_file() + self.config_file = self.find_config_file() - if file is None: - logger.warning("No Config File Found") + if self.config_file is None: + logger.debug("No Config File Found") file_config = {} else: - fh = open(file, mode="r") + fh = open(self.config_file, mode="r") file_config = json.load(fh) fh.close() data = file_config | kwargs self.set_attributes(**data) - if self.path: - self.path = self.root / self.path if not Path(self.path).resolve().exists() else self.path - - if self.record_trades and ( - hasattr(self, "records_dir") is False or self.records_dir is None or root is not None - ): - 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) + try: + if self.path: + self.path = self.root / self.path if not Path(self.path).resolve().exists() else self.path + except Exception as err: + logger.debug(f"Error setting path: {err}") + self.path = "" 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]: """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 """ 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. + """ diff --git a/src/aiomql/lib/symbol.py b/src/aiomql/lib/symbol.py index 8599477..f2769b8 100644 --- a/src/aiomql/lib/symbol.py +++ b/src/aiomql/lib/symbol.py @@ -158,7 +158,7 @@ class Symbol(_Base, SymbolInfo): if check := self.volume_min <= volume <= self.volume_max: return check, volume 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: """Round off the volume to the nearest volume step. diff --git a/src/aiomql/lib/trader.py b/src/aiomql/lib/trader.py index a923be4..3eff64a 100644 --- a/src/aiomql/lib/trader.py +++ b/src/aiomql/lib/trader.py @@ -1,6 +1,5 @@ from abc import ABC, abstractmethod from datetime import datetime, UTC -from string import digits from typing import TypeVar from logging import getLogger diff --git a/tests/backtest/conftest.py b/tests/backtest/conftest.py index 47218d7..155830a 100644 --- a/tests/backtest/conftest.py +++ b/tests/backtest/conftest.py @@ -1,11 +1,12 @@ -from datetime import datetime, UTC import asyncio import json import shutil +from datetime import datetime, UTC from logging import getLogger from pathlib import Path import pytest + from aiomql.core import Config from aiomql.core.meta_backtester import MetaBackTester from aiomql.core.backtesting.backtest_engine import BackTestEngine @@ -57,7 +58,7 @@ async def config(request): data["mode"] = "backtest" json.dump(data, fh1, 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 await cleanup() @@ -73,7 +74,8 @@ async def mt(): @pytest.fixture(scope="package") 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") diff --git a/tests/backtest/integration/test_backtesting.py b/tests/backtest/integration/test_backtesting.py index d850391..f23ba16 100644 --- a/tests/backtest/integration/test_backtesting.py +++ b/tests/backtest/integration/test_backtesting.py @@ -27,7 +27,8 @@ async def make_buy_sell_orders(): 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 isinstance(backtest_engine, BackTestEngine) assert isinstance(history.mt5, MetaBackTester) diff --git a/tests/backtest/unit/test_config.py b/tests/backtest/unit/test_config.py new file mode 100644 index 0000000..70cbeee --- /dev/null +++ b/tests/backtest/unit/test_config.py @@ -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" diff --git a/tests/live/conftest.py b/tests/live/conftest.py index c0f51d8..ec257fd 100644 --- a/tests/live/conftest.py +++ b/tests/live/conftest.py @@ -53,7 +53,7 @@ async def config(request): data = json.load(fh) json.dump(data, fh1, 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 await cleanup() diff --git a/tests/live/records_dir/test_result.csv b/tests/live/records_dir/test_result.csv new file mode 100644 index 0000000..6188fa1 --- /dev/null +++ b/tests/live/records_dir/test_result.csv @@ -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 diff --git a/tests/live/records_dir/test_result.json b/tests/live/records_dir/test_result.json new file mode 100644 index 0000000..7d06809 --- /dev/null +++ b/tests/live/records_dir/test_result.json @@ -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 + } +] \ No newline at end of file diff --git a/tests/live/records_dir/test_trades.csv b/tests/live/records_dir/test_trades.csv new file mode 100644 index 0000000..c786920 --- /dev/null +++ b/tests/live/records_dir/test_trades.csv @@ -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 diff --git a/tests/live/records_dir/test_trades.json b/tests/live/records_dir/test_trades.json new file mode 100644 index 0000000..46191a5 --- /dev/null +++ b/tests/live/records_dir/test_trades.json @@ -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 + } +] \ No newline at end of file diff --git a/tests/live/unit/test_config.py b/tests/live/unit/test_config.py index 3b58b93..4c9d781 100644 --- a/tests/live/unit/test_config.py +++ b/tests/live/unit/test_config.py @@ -25,5 +25,5 @@ class TestConfig: assert "server" in account_info 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" diff --git a/tests/live/unit/test_trader.py b/tests/live/unit/test_trader.py index 08f54f7..229636d 100644 --- a/tests/live/unit/test_trader.py +++ b/tests/live/unit/test_trader.py @@ -5,7 +5,8 @@ from aiomql.lib.ram import RAM from aiomql.contrib.traders import SimpleTrader from aiomql.contrib.symbols import ForexSymbol from aiomql.core.constants import OrderType - +from aiomql.lib.account import Account +from aiomql._utils import round_down class TestTrader: @classmethod @@ -13,11 +14,13 @@ class TestTrader: ram = RAM(fixed_amount=10) cls.trader = SimpleTrader(symbol=ForexSymbol(name="BTCUSD"), ram=ram) cls.simple_trader2 = SimpleTrader(symbol=ForexSymbol(name="EURJPY"), ram=ram) + cls.account = Account() @pytest.fixture(scope="class", autouse=True) async def initialize(self): await self.trader.symbol.initialize() await self.simple_trader2.symbol.initialize() + await self.account.refresh() async def test_create_order_no_stops(self): 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()) 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 abs(profit - self.trader.ram.fixed_amount * self.trader.ram.risk_to_reward) <= 2.5 + assert abs(abs(loss) - abs(-self.trader.ram.fixed_amount)) <= 2 assert res is not None assert res.retcode == 10009 @@ -47,8 +50,8 @@ class TestTrader: 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 abs(profit - self.trader.ram.fixed_amount * self.trader.ram.risk_to_reward) <= 2.5 + assert abs(abs(loss) - abs(-self.trader.ram.fixed_amount)) <= 2 assert res is not None assert res.retcode == 10009 @@ -60,10 +63,10 @@ class TestTrader: tp = tick.ask + tp await self.trader.create_order_with_stops(order_type=OrderType.BUY, sl=sl, tp=tp) 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.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