mirror of
https://github.com/Ichinga-Samuel/aiomql.git
synced 2026-08-24 01:08:07 +00:00
v4.0.1
This commit is contained in:
@@ -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
@@ -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. |
|
||||||
|
|||||||
@@ -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
@@ -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]
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|||||||
+91
-95
@@ -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
|
||||||
for dirname in self.walk_to_root(self.root):
|
if config_file.exists():
|
||||||
check_path = os.path.join(dirname, self.filename)
|
return config_file
|
||||||
if os.path.isfile(check_path):
|
if current == self.root:
|
||||||
return check_path
|
return
|
||||||
return None
|
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:
|
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.
|
||||||
|
"""
|
||||||
|
|||||||
@@ -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,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
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -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()
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
@@ -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
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -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
|
||||||
|
@@ -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
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user