This commit is contained in:
Ichinga Samuel
2024-10-29 12:27:22 +01:00
commit 9eb0baa85c
42 changed files with 5555 additions and 0 deletions
+79
View File
@@ -0,0 +1,79 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
env/
venv/
.venv/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib64/
parts/
sdist/
var/
notebooks/
*.egg-info/
.installed.cfg
*.egg
*.pkl
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
.pytest_cache
nosetests.xml
coverage.xml
*,cover
.hypothesis/
# Translations
*.mo
*.pot
# Django stuff:
*.log
# Sphinx documentation
docs/_build/
# PyBuilder
target/
#Ipython Notebook
.ipynb_checkpoints
# Ide environment
*.idea/
.pypirc
.vscode/
# config files
aiomql.json
# development
terminals/
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 Ichinga Samuel
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+100
View File
@@ -0,0 +1,100 @@
# Aiomql - Bot Building Framework and Asynchronous MetaTrader5 Library
![GitHub](https://img.shields.io/github/license/ichinga-samuel/aiomql?style=plastic)
![GitHub issues](https://img.shields.io/github/issues/ichinga-samuel/aiomql?style=plastic)
![PyPI](https://img.shields.io/pypi/v/aiomql)
### Installation
```bash
pip install aiomql
```
### Key Features
- Asynchronous Python Library For MetaTrader5
- Asynchronous Bot Building Framework
- Build bots for trading in different financial markets using a bot factory
- Use threadpool executors to run multiple strategies on multiple instruments concurrently
- Records and keep track of trades and strategies in csv files.
- Helper classes for Bot Building. Easy to use and extend.
- Compatible with pandas-ta.
- Sample Pre-Built strategies
- Visualization of charts using matplotlib and mplfinance
- Manage Trading periods using Sessions
- Risk Management
- Run multiple bots concurrently with different accounts from the same broker or different brokers
### As an asynchronous MetaTrader5 Libray
```python
import asyncio
from aiomql import MetaTrader
async def main():
mt5 = MetaTrader()
await mt5.initialize()
await mt5.login(123456, '*******', 'Broker-Server')
symbols = await mt5.symbols_get()
print(symbols)
asyncio.run(main())
```
### As a Bot Building FrameWork using a Sample Strategy
***The following code is a sample bot that uses the FingerTrap strategy from the library.\
It assumes that you have a config file in the same directory as the script.\
The config file should be named aiomql.json and should contain the login details for your account.\
It demonstrates the use of sessions and risk management.\
Sessions allows you to specify the trading period for a strategy. You can also set an action to be performed at the end of a session.\
Risk Management allows you to manage the risk of a strategy. You can set the risk per trade and the risk to reward ratio.\
The trader class handles the placing of orders and risk management. It is an attribute of the strategy class.***
```python
from datetime import time
import logging
from aiomql import Bot, ForexSymbol, FingerTrap, Session, Sessions, RAM, SimpleTrader, TimeFrame
logging.basicConfig(level=logging.INFO)
def build_bot():
bot = Bot()
# create sessions for the strategies
london = Session(name='London', start=8, end=time(hour=15, minute=30), on_end='close_all')
new_york = Session(name='New York', start=13, end=time(hour=20, minute=30))
tokyo = Session(name='Tokyo', start=23, end=time(hour=6, minute=30))
# configure the parameters and the trader for a strategy
params = {'trend_candles_count': 500, 'fast_period': 8, 'slow_period': 34, 'etf': TimeFrame.M5}
gbpusd = ForexSymbol(name='GBPUSD')
st1 = FingerTrap(symbol=gbpusd, params=params, trader=SimpleTrader(symbol=gbpusd, ram=RAM(risk=0.05, risk_to_reward=2)),
sessions=Sessions(london, new_york))
# use the default for the other strategies
st2 = FingerTrap(symbol=ForexSymbol(name='AUDUSD'), sessions=Sessions(tokyo, new_york))
st3 = FingerTrap(symbol=ForexSymbol(name='USDCAD'), sessions=Sessions(new_york))
st4 = FingerTrap(symbol=ForexSymbol(name='USDJPY'), sessions=Sessions(tokyo))
st5 = FingerTrap(symbol=ForexSymbol(name='EURGBP'), sessions=Sessions(london))
# sessions are not required
st6 = FingerTrap(symbol=ForexSymbol(name='EURUSD'))
# add strategies to the bot
bot.add_strategies([st1, st2, st3, st4, st5, st6])
bot.execute()
# run the bot
build_bot()
```
## API Documentation
see [API Documentation](https://github.com/Ichinga-Samuel/aiomql/tree/master/docs) for more details
## Contributing
Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.
## Support
Feeling generous, like the package or want to see it become a more mature package?
Consider supporting the project by buying me a coffee.\
[!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/ichingasamuel)
+25
View File
@@ -0,0 +1,25 @@
[build-system]
requires = [
"setuptools>=42",
"wheel"
]
build-backend = "setuptools.build_meta"
[project]
name = "aiomql"
version = "3.23"
readme = "README.md"
requires-python = ">=3.11"
classifiers = [
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
]
keywords = ['MetaTrader5', 'Asynchronous', 'Algorithmic Trading', 'Trading Bot']
dependencies = ["MetaTrader5>=5.0.37", "pandas>=1.5.0", "pandas-ta>=0.3.14b0", "matplotlib>=3.8.4", "mplfinance>=0.12.10b0"]
authors = [{name = "Ichinga Samuel", email = "ichingasamuel@gmail.com"}]
description = "Asynchronous MetaTrader5 library and Algorithmic Trading Framework"
[project.urls]
"Homepage" = "https://github.com/Ichinga-Samuel/aiomql"
"Bug Tracker" = "https://github.com/Ichinga-Samuel/aiomql/issues"
+154
View File
@@ -0,0 +1,154 @@
anyio==4.3.0
argon2-cffi==23.1.0
argon2-cffi-bindings==21.2.0
arrow==1.3.0
asttokens==2.4.1
async-lru==2.0.4
attrs==23.2.0
Babel==2.14.0
beautifulsoup4==4.12.3
black==23.9.1
bleach==6.1.0
build==1.0.3
certifi==2023.7.22
cffi==1.16.0
charset-normalizer==3.3.0
click==8.1.7
colorama==0.4.6
comm==0.2.2
contourpy==1.2.1
cycler==0.12.1
databind.core==4.4.1
databind.json==4.4.1
debugpy==1.8.1
decorator==5.1.1
defusedxml==0.7.1
Deprecated==1.2.14
docspec==2.2.1
docspec-python==2.2.1
docstring-parser==0.11
docutils==0.20.1
executing==2.0.1
fastjsonschema==2.19.1
fonttools==4.51.0
fqdn==1.5.1
h11==0.14.0
httpcore==1.0.5
httpx==0.27.0
idna==3.4
importlib-metadata==6.8.0
iniconfig==2.0.0
ipykernel==6.29.4
ipython==8.23.0
ipywidgets==8.1.2
isoduration==20.11.0
jaraco.classes==3.3.0
jedi==0.19.1
Jinja2==3.1.2
json5==0.9.24
jsonpointer==2.4
jsonschema==4.21.1
jsonschema-specifications==2023.12.1
jupyter==1.0.0
jupyter-console==6.6.3
jupyter-events==0.10.0
jupyter-lsp==2.2.5
jupyter_client==8.6.1
jupyter_core==5.7.2
jupyter_server==2.13.0
jupyter_server_terminals==0.5.3
jupyterlab==4.1.6
jupyterlab_pygments==0.3.0
jupyterlab_server==2.26.0
jupyterlab_widgets==3.0.10
keyring==24.2.0
kiwisolver==1.4.5
markdown-it-py==3.0.0
MarkupSafe==2.1.3
matplotlib==3.8.4
matplotlib-inline==0.1.6
mdurl==0.1.2
MetaTrader5==5.0.45
mistune==3.0.2
more-itertools==10.1.0
mplfinance==0.12.10b0
mypy-extensions==1.0.0
nbclient==0.10.0
nbconvert==7.16.3
nbformat==5.10.4
nest-asyncio==1.6.0
nh3==0.2.14
notebook==7.1.2
notebook_shim==0.2.4
nr-date==2.1.0
nr-stream==1.1.5
nr.util==0.8.12
numpy==1.26.0
overrides==7.7.0
packaging==23.2
pandas==2.1.1
pandas-ta==0.3.14b0
pandocfilters==1.5.1
parso==0.8.4
pathspec==0.11.2
pillow==10.3.0
pkginfo==1.9.6
platformdirs==3.11.0
pluggy==1.3.0
prometheus_client==0.20.0
prompt-toolkit==3.0.43
psutil==5.9.8
pure-eval==0.2.2
pycparser==2.22
pydoc-markdown==4.8.2
Pygments==2.16.1
pyparsing==3.1.2
pyproject_hooks==1.0.0
pytest==7.4.4
python-dateutil==2.8.2
python-json-logger==2.0.7
python-telegram-bot==21.0.1
pytz==2023.3.post1
pywin32==306
pywin32-ctypes==0.2.2
pywinpty==2.0.13
PyYAML==6.0.1
pyzmq==25.1.2
qtconsole==5.5.1
QtPy==2.4.1
readme-renderer==42.0
referencing==0.34.0
requests==2.31.0
requests-toolbelt==1.0.0
rfc3339-validator==0.1.4
rfc3986==2.0.0
rfc3986-validator==0.1.1
rich==13.6.0
rpds-py==0.18.0
Send2Trash==1.8.3
six==1.16.0
sniffio==1.3.1
soupsieve==2.5
stack-data==0.6.3
terminado==0.18.1
tinycss2==1.2.1
tomli==2.0.1
tomli_w==1.0.0
tornado==6.4
traitlets==5.14.2
twine==4.0.2
typeapi==2.1.1
types-python-dateutil==2.9.0.20240316
typing_extensions==4.6.3
tzdata==2023.3
uri-template==1.3.0
urllib3==2.0.6
watchdog==3.0.0
wcwidth==0.2.13
webcolors==1.13
webencodings==0.5.1
websocket-client==1.7.0
widgetsnbextension==4.0.10
wrapt==1.15.0
yapf==0.40.2
zipp==3.17.0
+3
View File
@@ -0,0 +1,3 @@
from setuptools import setup
setup()
+20
View File
@@ -0,0 +1,20 @@
from .core import *
from .account import Account
from .ram import RAM
from .symbol import Symbol
from .strategy import Strategy
from .bot_builder import Bot
from .result import Result
from .records import Records
from .trade_records import TradeRecords
from .candle import Candle, Candles
from .positions import Positions
from .executor import Executor
from .order import Order
from .ticks import Tick, Ticks
from .history import History
from .trader import Trader
from .terminal import Terminal
from .sessions import Session, Sessions
from .utils import dict_to_string, round_off, find_bearish_fractal, find_bullish_fractal
from .lib import *
+113
View File
@@ -0,0 +1,113 @@
import asyncio
from logging import getLogger
from .core.models import AccountInfo, SymbolInfo
from .core.exceptions import LoginError
logger = getLogger(__name__)
class Account(AccountInfo):
"""A class for managing a trading account. A singleton class.
A subclass of AccountInfo. All AccountInfo attributes are available in this class.
Attributes:
connected (bool): Status of connection to MetaTrader 5 Terminal
symbols (set[SymbolInfo]): A set of available symbols for the financial market.
Notes:
Other Account properties are defined in the AccountInfo class.
"""
_instance: 'Account'
connected: bool
symbols = set()
def __new__(cls, *args, **kwargs):
if not hasattr(cls, '_instance'):
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self, **kwargs):
super().__init__(**kwargs)
acc = self.config.account_info()
acc_details = {k: v for k, v in self.get_dict(include={'login', 'server', 'password'}).items() if v}
acc |= acc_details
self.config.set_attributes(**acc)
self.set_attributes(**acc)
async def refresh(self):
"""Refreshes the account instance with the latest account details from the MetaTrader 5 terminal"""
account_info = await self.mt5.account_info()
acc = account_info._asdict()
self.set_attributes(**acc)
async def __aenter__(self) -> 'Account':
"""Connect to a trading account and return the account instance.
Async context manager for the Account class.
Returns:
Account: An instance of the Account class
Raises:
LoginError: If login fails
"""
res = await self.sign_in()
if not res:
raise LoginError('Login failed')
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
await self.mt5.shutdown()
self.connected = False
async def sign_in(self) -> bool:
"""Connect to a trading account.
Returns:
bool: True if login was successful else False
"""
acc = self.get_dict(include={'login', 'server', 'password'})
self.connected = await self._login(acc=acc)
if self.connected:
await self.refresh()
self.symbols = await self.symbols_get()
return self.connected
await self.mt5.shutdown()
return False
async def _login(self, *, acc: dict, tries=3):
res = False
if tries == 0:
return False
ini = await self.mt5.initialize(**acc, path=self.config.path)
if ini:
res = await self.mt5.login(**acc)
if ini and res:
return True
else:
await asyncio.sleep(5+tries)
return await self._login(acc=acc, tries=tries-1)
def has_symbol(self, symbol: str | SymbolInfo):
"""Checks to see if a symbol is available for a trading account.
Args:
symbol (str | SymbolInfo):
Returns:
bool: True if symbol is present otherwise False
"""
try:
return str(symbol) in {s.name for s in self.symbols}
except Exception as err:
logger.warning(f'Error: {err}; {symbol} not available in this market')
return False
async def symbols_get(self) -> set[SymbolInfo]:
"""Get all financial instruments from the MetaTrader 5 terminal available for the current account.
Returns:
set[Symbol]: A set of available symbols.
"""
syms = await self.mt5.symbols_get()
return {SymbolInfo(name=sym.name) for sym in syms}
+157
View File
@@ -0,0 +1,157 @@
import asyncio
from concurrent.futures import ProcessPoolExecutor
from typing import Type, Iterable, TypeVar, Callable, Coroutine
import logging
from .executor import Executor
from .account import Account
from .core.config import Config
from .symbol import Symbol as _Symbol
from .strategy import Strategy as _Strategy
logger = logging.getLogger(__name__)
Strategy = TypeVar("Strategy", bound=_Strategy)
Symbol = TypeVar("Symbol", bound=_Symbol)
class Bot:
"""The bot class. Create a bot instance to run your strategies.
Attributes:
account (Account): Account Object.
executor: The default thread executor.
symbols (list[Symbols]): A set of symbols for the trading session
config (Config): Config instance
"""
config: Config
account: Account
symbols: set
executor: Executor
def __init__(self):
self.config = Config()
self.account = Account()
self.symbols = set()
self.executor = Executor()
@classmethod
def run_bots(cls, funcs: dict[Callable: dict] = None, num_workers: int = None):
"""Run multiple scripts or bots in parallel with different accounts.
Args:
funcs (dict): A dictionary of functions to run with their respective keyword arguments as a dictionary
num_workers (int): Number of workers to run the functions
"""
num_workers = num_workers or len(funcs) * 2
with ProcessPoolExecutor(max_workers=num_workers) as executor:
for bot, kwargs in funcs.items():
executor.submit(bot, **kwargs)
async def initialize(self):
"""Prepares the bot by signing in to the trading account and initializing the symbols for the trading session.
Starts the global task queue.
Raises:
SystemExit if sign in was not successful
"""
try:
init = await self.account.sign_in()
if not init:
logger.warning(f"Unable to sign in to MetaTrder 5 Terminal")
raise SystemExit
logger.info("Login Successful")
await self.init_symbols()
self.executor.remove_workers(symbols=self.symbols)
self.add_coroutine(self.config.task_queue.start)
self.config.bot = self
except Exception as err:
logger.error(f"{err}. Bot initialization failed")
raise SystemExit
def add_function(self, func: Callable, **kwargs: dict):
"""Add a function to the executor.
Args:
func (Callable): A function to be executed
**kwargs (dict): Keyword arguments for the function
"""
self.executor.add_function(func, kwargs)
def add_coroutine(self, coro: Coroutine | Callable, **kwargs):
"""Add a coroutine to the executor.
Args:
coro (Coroutine): A coroutine to be executed
**kwargs (dict): keyword arguments for the coroutine
Returns:
"""
self.executor.add_coroutine(coro, kwargs)
def execute(self):
"""Execute the bot."""
asyncio.run(self.start())
async def start(self):
"""Initialize the bot and execute it. Similar to calling `execute` method but is a coroutine."""
await self.initialize()
await self.executor.execute()
def add_strategy(self, strategy: Strategy):
"""Add a strategy to the executor. An added strategy will only run if it's symbol was successfully initialized.
Args:
strategy (Strategy): A Strategy instance to run on bot
Notes:
Make sure the symbol has been added to the market
"""
self.executor.add_worker(strategy)
def add_strategies(self, strategies: Iterable[Strategy]):
"""Add multiple strategies at the same time
Args:
strategies: A list of strategies
"""
[self.add_strategy(strategy) for strategy in strategies]
def add_strategy_all(self, *, strategy: Type[Strategy], params: dict | None = None):
"""Use this to run a single strategy on all available instruments in the market using the default parameters
i.e. one set of parameters for all trading symbols
Keyword Args:
strategy (Strategy): Strategy class
params (dict): A dictionary of parameters for the strategy
"""
[
self.add_strategy(strategy(symbol=symbol, params=params))
for symbol in self.symbols
]
async def init_symbols(self):
"""Initialize the symbols for the current trading session. This method is called internally by the bot."""
syms = [self.init_symbol(strategy.symbol) for strategy in self.executor.workers]
await asyncio.gather(*syms, return_exceptions=True)
async def init_symbol(self, symbol: Symbol) -> Symbol:
"""Initialize a symbol before the beginning of a trading sessions.
Removes it from the list of symbols if it was not successfully initialized or not available
for the account.
Args:
symbol (Symbol): Symbol object to be initialized
Returns:
Symbol: if successfully initialized
"""
if self.account.has_symbol(symbol):
init = await symbol.init()
if init:
self.symbols.add(symbol)
return symbol
logger.warning(f"Unable to initialize symbol {symbol}")
logger.warning(f"{symbol} not a available for this market")
+296
View File
@@ -0,0 +1,296 @@
"""Candle and Candles classes for handling bars from the MetaTrader 5 terminal."""
from typing import Type, TypeVar, Generic, Iterable
from logging import getLogger
from pandas import DataFrame, Series
import pandas as pd
import pandas_ta as ta
import mplfinance as mplt
from .core.constants import TimeFrame
logger = getLogger(__name__)
class Candle:
"""A customized class representing rates from the MetaTrader 5 terminal analogous to Japanese
Candlesticks. You can subclass this class for added customization.
Attributes:
time (int): Period start time.
open (int): Open price
high (float): The highest price of the period
low (float): The lowest price of the period
close (float): Close price
tick_volume (float): Tick volume
real_volume (float): Trade volume
spread (float): Spread
Index (int): Custom attribute representing the position of the candle in a sequence.
"""
time: float
open: float
high: float
low: float
close: float
real_volume: float
spread: float
tick_volume: float
Index: int
def __init__(self, **kwargs):
"""Create a Candle object from keyword arguments. This class must always be instantiated with open, high, low
and close prices.
Keyword Args:
**kwargs: Candle attributes and values as keyword arguments.
"""
if not all(i in kwargs for i in ['open', 'high', 'low', 'close']):
raise ValueError("Candle must be instantiated with open, high, low and close prices")
self.time = kwargs.pop('time', 0)
self.Index = kwargs.pop('Index', 0)
self.set_attributes(**kwargs)
def __repr__(self):
return ("%(class)s(Index=%(Index)s, time=%(time)s, open=%(open)s, high=%(high)s, low=%(low)s, close=%(close)s)"
% {"class": self.__class__.__name__, "open": self.open, "high": self.high,
"low": self.low, "close": self.close, "time": self.time, 'Index': self.Index})
def __str__(self):
return str(self.dict())
def __eq__(self, other: "Candle"):
eq = self.open == other.open and self.high == other.high and self.low == other.low and self.close == other.close
return eq
def __hash__(self):
return hash(self.time)
def __lt__(self, other: "Candle"):
return self.time < other.time
def __gt__(self, other: "Candle"):
return self.time > other.time
def __getitem__(self, item):
return self.__dict__[item]
def set_attributes(self, **kwargs):
"""Set keyword arguments as instance attributes
Keyword Args:
**kwargs: Instance attributes and values as keyword arguments
"""
[setattr(self, i, j) for i, j in kwargs.items()]
def is_bullish(self) -> bool:
"""A simple check to see if the candle is bullish.
Returns:
bool: True or False
"""
return self.close >= self.open
def is_bearish(self) -> bool:
"""A simple check to see if the candle is bearish.
Returns:
bool: True or False
"""
return self.open > self.close
def dict(self, exclude: set = None, include: set = None) -> dict:
"""
Returns a dictionary of the instance attributes.
Args:
exclude: A set of attributes to exclude from the dictionary. Defaults to None.
include: A set of attributes to include in the dictionary. Defaults to None.
Returns: dict
"""
exclude = exclude or set()
include = include or set()
keys = include or set(self.__dict__.keys()).difference(exclude)
return {k: v for k, v in self.__dict__.items() if k in keys}
_Candle = TypeVar("_Candle", bound=Candle)
_Candles = TypeVar("_Candles", bound="Candles")
class Candles(Generic[_Candle]):
"""An iterable container class of Candle objects in chronological order.
Attributes:
Index (Series['int']): A pandas Series of the indexes of all candles in the object.
time (Series['int']): A pandas Series of the time of all candles in the object.
open (Series[float]): A pandas Series of the opening price of all candles in the object.
high (Series[float]): A pandas Series of the high price of all candles in the object.
low (Series[float]): A pandas Series of the low price of all candles in the object.
close (Series[float]): A pandas Series of the closing price of all candles in the object.
tick_volume (Series[float]): A pandas Series of the tick volume of all candles in the object.
real_volume (Series[float]): A pandas Series of the real volume of all candles in the object.
spread (Series[float]): A pandas Series of the spread of all candles in the object.
timeframe (TimeFrame): The timeframe of the candles in the object.
Candle (Type[Candle]): The Candle class for representing the candles in the object.
properties:
data (DataFrame): A pandas DataFrame of all candles in the object.
Notes:
The candle class can be customized by subclassing the Candle class and passing the subclass as the candle
keyword argument, or defining it on the class body as a class attribute.
"""
Index: Series
time: Series
open: Series
high: Series
low: Series
close: Series
tick_volume: Series
real_volume: Series
spread: Series
Candle: Type[Candle]
timeframe: TimeFrame
_data: DataFrame
def __init__(self, *, data: DataFrame | _Candles | Iterable, flip=False, candle_class: Type[_Candle] = None):
"""A container class of Candle objects in chronological order.
Args:
data (DataFrame|Candles|Iterable): A pandas dataframe, a Candles object or any suitable iterable
Keyword Args:
flip (bool): Reverse the chronological order of the candles to the oldest first. Defaults to False.
candle_class: A subclass of Candle to use as the candle class. Defaults to Candle.
"""
if isinstance(data, DataFrame):
data = data
elif isinstance(data, type(self)):
data = DataFrame(data.data)
elif isinstance(data, Iterable):
data = DataFrame(data)
else:
raise ValueError(f"Cannot create DataFrame from object of {type(data)}")
self._data = data.loc[::-1].reset_index(drop=True) if flip else data
self.Candle = candle_class or Candle
def __repr__(self):
return self._data.__repr__()
def __len__(self):
return len(self._data.index)
def __contains__(self, item: _Candle):
return item.time == self[item.Index].time
def __getitem__(self, index) -> _Candle | _Candles | Series:
if isinstance(index, slice):
cls = self.__class__
data = self._data.iloc[index]
data.reset_index(drop=True, inplace=True)
return cls(data=data)
elif isinstance(index, str):
if index == 'Index':
return Series(self._data.index)
return self._data[index]
elif isinstance(index, int):
index = index if index >= 0 else len(self) + index
return self.Candle(**self._data.iloc[index], Index=index)
raise TypeError(f"Expected int, slice or str got {type(index)}")
def __setitem__(self, index, value: Series):
if isinstance(value, Series):
self._data[index] = value
return
raise TypeError(f"Expected Series got {type(value)}")
def __getattr__(self, item):
if item in self._data.columns:
return self._data[item]
if item == 'Index':
return Series(self._data.index)
raise AttributeError(f"Attribute {item} not defined on class {self.__class__.__name__}")
def __iter__(self):
return (self.Candle(**row._asdict()) for row in self._data.itertuples())
@property
def timeframe(self):
tf = self.time[1] - self.time[0]
return TimeFrame.get(abs(tf))
@property
def ta(self):
"""Access to the pandas_ta library for performing technical analysis on the underlying data attribute.
Returns:
pandas_ta: The pandas_ta library
"""
return self._data.ta
@property
def ta_lib(self):
"""Access to the ta library for performing technical analysis. Not dependent on the underlying data attribute.
Returns:
ta: The ta library
"""
return ta
@property
def data(self) -> DataFrame:
"""The original data passed to the class as a pandas DataFrame"""
return self._data
def rename(self, inplace=True, **kwargs) -> _Candles:
"""Rename columns of the candles class.
Keyword Args:
inplace (bool): Rename the columns inplace or return a new instance of the class with the renamed columns
**kwargs: The new names of the columns
Returns:
Candles: A new instance of the class with the renamed columns if inplace is False else the modified instance
"""
res = self._data.rename(columns=kwargs, inplace=inplace)
return self if inplace else self.__class__(data=res)
def make_addplot(self, *, count: int = 50, columns: list = None, **kwargs) -> dict:
"""
Make subplots for adding to the main plot
Args:
count (int): The numbers of candles to make the addplot for. Defaults to 50.
columns (list[str]): The columns to make the plot from. Defaults to None.
**kwargs: Valid arguments for the mplfinance make_addplot function
"""
columns = columns or []
data = self._data[-count:]
data.index = pd.to_datetime(data['time'], unit='s')
return mplt.make_addplot(data[columns], **kwargs)
def visualize(self, *, count: int = 50, type='candle', savefig: str | dict = None, addplot: dict = None,
style: str = 'charles', ylabel: str = 'Price', title: str = 'Chart', **kwargs):
"""Visualize the candles using the mplfinance library.
Args:
count (int): The number of candles to visualize, counting from behind, i.e the most recent candles.
Defaults to 50.
type: Type of chart, defaults to candle
savefig (str|dict): The path to save the figure or a dictionary of parameters to pass to the savefig method.
addplot: Additional plots to add to the chart. Defaults to None. They should match the dimension of the
original data which is specified via the count parameter.
style (str): The style of the chart. Defaults to 'charles'.
ylabel (str): The label of the y-axis. Defaults to 'Price'.
title (str): The title of the chart. Defaults to 'Chart'.
kwargs: valid kwargs for the plot function.
"""
kwargs |= {key: arg for key, arg in (('savefig', savefig), ('addplot', addplot), ('style', style),
('ylabel', ylabel), ('title', title), ('type', type)) if arg}
data = self._data[-count:]
data.index = pd.to_datetime(data['time'], unit='s')
mplt.plot(data, **kwargs)
+8
View File
@@ -0,0 +1,8 @@
from .meta_trader import MetaTrader
from .config import Config
from .models import *
from .constants import *
from .base import Base
from .errors import Error
from .exceptions import *
from .task_queue import TaskQueue
+120
View File
@@ -0,0 +1,120 @@
from functools import cache
import enum
from logging import getLogger
from .config import Config
from .meta_trader import MetaTrader
logger = getLogger(__name__)
class Base:
"""A base class for all data structure classes in the aiomql package. This class provides a set of common methods
and attributes for handling data.
"""
mt5: MetaTrader
config: Config
def __init__(self, **kwargs):
"""
Initialize a new instance of the Base class
Args:
**kwargs: Set instance attributes with keyword arguments. Only if they are annotated on the class body.
"""
self.config = Config()
self.mt5 = MetaTrader()
self.exclude = {'mt5', "config", 'exclude', 'include', 'annotations', 'class_vars', 'dict'}
self.include = set()
self.set_attributes(**kwargs)
def __repr__(self):
kv = [(k, v) for k, v in self.__dict__.items() if not k.startswith('_') and
(type(v) in (int, float, str) or isinstance(v, enum.Enum))]
args = (', '.join('%s=%s' % (i, j) for i, j in kv[:3]))
args = args if len(kv) <= 3 else args + ' ... ' + ', '.join('%s=%s' % (i, j) for i, j in kv[-1:])
return '%(class)s(%(args)s)' % {'class': self.__class__.__name__, 'args': args}
def set_attributes(self, **kwargs):
"""Set keyword arguments as object attributes
Keyword Args:
**kwargs: Object attributes and values as keyword arguments
Raises:
AttributeError: When assigning an attribute that does not belong to the class or any parent class
Notes:
Only sets attributes that have been annotated on the class body.
"""
for i, j in kwargs.items():
try:
setattr(self, i, self.annotations[i](j))
except KeyError:
logger.warning(f"Attribute {i} does not belong to class {self.__class__.__name__}")
continue
except ValueError:
logger.warning(f'Cannot covert object of type {type(j)} to type {self.annotations[i]}')
continue
except Exception as exe:
logger.warning(f'Did not set attribute {i} on class {self.__class__.__name__} due to {exe}')
continue
@property
@cache
def annotations(self) -> dict:
"""Class annotations from all ancestor classes and the current class.
Returns:
dict: A dictionary of class annotations
"""
annots = {}
for base in self.__class__.__mro__[-3::-1]:
annots |= getattr(base, '__annotations__', {})
return annots
def get_dict(self, exclude: set = None, include: set = None) -> dict:
"""Returns class attributes as a dict, with the ability to filter
Keyword Args:
exclude: A set of attributes to be excluded
include: Specific attributes to be returned
Returns:
dict: A dictionary of specified class attributes
Notes:
You can only set either of include or exclude. If you set both, include will take precedence
"""
exclude, include = exclude or set(), include or set()
filter_ = include or set(self.dict.keys()).difference(exclude)
return {key: value for key, value in self.dict.items() if key in filter_}
@property
@cache
def class_vars(self):
"""Annotated class attributes
Returns:
dict: A dictionary of available class attributes in all ancestor classes and the current class.
"""
clss = self.__class__.__mro__[-3::-1]
cls_dict = {}
for cls in clss:
cls_dict |= cls.__dict__
return {key: value for key, value in cls_dict.items() if key in self.annotations}
@property
def dict(self) -> dict:
"""All instance and class attributes as a dictionary, except those excluded in the Meta class.
Returns:
dict: A dictionary of instance and class attributes
"""
try:
_filter = self.exclude.difference(self.include)
return {key: value for key, value in (self.class_vars | self.__dict__).items() if
key not in _filter}
except Exception as err:
logger.warning(err)
+161
View File
@@ -0,0 +1,161 @@
import os
from pathlib import Path
from typing import Iterator, Literal, TypeVar
import json
from logging import getLogger
from .task_queue import TaskQueue
logger = getLogger(__name__)
Bot = TypeVar("Bot")
class Config:
"""A class for handling configuration settings for the aiomql package.
Attributes:
record_trades (bool): Whether to keep record of trades or not.
trade_record_mode: How to save trade, json or csv. Defaults to json
filename (str): Name of the config file
records_dir (str): Path to the directory where trade records are saved
login (int): Trading account number
password (str): Trading account password
server (str): Broker server
path (str): Path to terminal file
timeout (int): Timeout for terminal connection
_initialize (bool): First time initialization flag
state (dict): A global state dictionary for storing data across the framework
root_dir (str): The root directory of the project
Notes:
By default, the config class looks for a file named aiomql.json.
You can change this by passing the filename and/or the config_dir keyword argument(s) to the constructor
or the load_config method.
By passing reload=True to the load_config method, you can reload and search again for the config file.
"""
login: int = 0
trade_record_mode: Literal['csv', 'json'] = 'csv'
password: str = ""
server: str = ""
path: str | Path = ""
timeout: int = 60000
record_trades: bool = True
filename: str = "aiomql.json"
_initialize = True
state: dict = {}
root: Path
root_dir: Path
records_dir: Path
config_dir: str = ''
task_queue: TaskQueue = TaskQueue()
bot: Bot = None
_instance: 'Config'
def __new__(cls, *args, **kwargs):
if not hasattr(cls, "_instance"):
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self, **kwargs):
reload = kwargs.pop('reload', False)
self.load_config(reload=reload, **kwargs)
def set_root(self, *, root: str | Path):
root = Path(root) if str else root
self.root = root.absolute().resolve()
self.root_dir = self.root
def __setattr__(self, key, value):
if key == 'path':
value = str(self.root_dir / Path(value).absolute().resolve())
super().__setattr__(key, value)
def set_attributes(self, **kwargs):
"""Set keyword arguments as object attributes
Keyword Args:
**kwargs: Object attributes and values as keyword arguments
"""
[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(self):
try:
path = self.root_dir / self.config_dir
for dirname in self.walk_to_root(path):
check_path = os.path.join(dirname, self.filename)
if os.path.isfile(check_path):
return check_path
return None
except Exception as _:
return
def create_records_dir(self, *, records_dir: str | Path = 'records'):
"""Create records directory if it does not exist. By default, it is relative to the root directory of the
project unless an absolute path is provided.
Keyword Args:
records_dir (str|Path): The directory to save trade records. Default is 'records'
"""
try:
if isinstance(records_dir, str):
records_dir = self.root_dir / records_dir
elif isinstance(records_dir, Path):
records_dir = records_dir.absolute().resolve()
records_dir.mkdir(parents=True, exist_ok=True)
self.records_dir = records_dir
except Exception as err:
logger.warning(f"{err}: Unable to create records directory")
def load_config(self, *, file: str = None, reload: bool = True, filename: str = None,
config_dir: str = '', **kwargs):
"""Load configuration settings from a file.
Keyword Args:
file (str): The path to the file to load. If not provided, the file is searched for
reload (bool): Whether to reload the config object. Default is True
filename (str): The name of the file to load. If not provided, the default filename is used
config_dir (str): The name of the directory to search for the file. Default is the root directory
root_dir (str): The root directory of the project
kwargs: Additional keyword arguments
"""
if not (self._initialize or reload):
return
data = {}
self.filename = filename or self.filename
self.config_dir = config_dir or self.config_dir
root_dir = kwargs.pop('root_dir', None)
records_dir = kwargs.pop('records_dir', 'records')
if self._initialize or (root_dir is not None):
self.set_root(root=(root_dir or '.'))
self.create_records_dir(records_dir=records_dir)
if (file := (file or self.find_config())) is None:
logger.warning("No Config File Found")
else:
fh = open(file, mode="r")
data = json.load(fh)
fh.close()
data |= kwargs
self.set_attributes(**data)
self._initialize = False
def account_info(self) -> dict[str, int | str]:
"""Returns Account login details as found in the config object if available
Returns:
dict: A dictionary of login details
"""
return {"login": self.login, "password": self.password, "server": self.server}
+791
View File
@@ -0,0 +1,791 @@
from enum import IntEnum, IntFlag
import MetaTrader5 as mt5
"""
MetaTrader5 constants as IntEnum types with Python style class names and nice string representation
Examples:
>>> from aiomql import OrderFilling
>>> fok = OrderFilling.FOK
>>> print(fok)
"ORDER_FILLING_FOK"
"""
class Repr:
__enum_name__ = ""
name: str
def __repr__(self):
return f"{self.__enum_name__}_{self.name}"
class TradeAction(Repr, IntEnum):
"""TRADE_REQUEST_ACTION Enum.
Attributes:
DEAL (int): Delete the pending order placed previously Place a trade order for an immediate execution with the
specified parameters (market order).
PENDING (int): Delete the pending order placed previously
SLTP (int): Modify Stop Loss and Take Profit values of an opened position
MODIFY (int): Modify the parameters of the order placed previously
REMOVE (int): Delete the pending order placed previously
CLOSE_BY (int): Close a position by an opposite one
"""
__enum_name__ = "TRADE_ACTION"
DEAL = mt5.TRADE_ACTION_DEAL
PENDING = mt5.TRADE_ACTION_PENDING
SLTP = mt5.TRADE_ACTION_SLTP
MODIFY = mt5.TRADE_ACTION_MODIFY
REMOVE = mt5.TRADE_ACTION_MODIFY
CLOSE_BY = mt5.TRADE_ACTION_CLOSE_BY
class OrderFilling(Repr, IntEnum):
"""ORDER_TYPE_FILLING Enum.
Attributes:
FOK (int): This execution policy means that an order can be executed only in the specified volume.
If the necessary amount of a financial instrument is currently unavailable in the market, the order will
not be executed. The desired volume can be made up of several available offers.
IOC (int): An agreement to execute a deal at the maximum volume available in the market within the volume
specified in the order. If the request cannot be filled completely, an order with the available volume will
be executed, and the remaining volume will be canceled.
RETURN (int): This policy is used only for market (ORDER_TYPE_BUY and ORDER_TYPE_SELL), limit and stop limit
orders (ORDER_TYPE_BUY_LIMIT, ORDER_TYPE_SELL_LIMIT,ORDER_TYPE_BUY_STOP_LIMIT and
ORDER_TYPE_SELL_STOP_LIMIT) and only for the symbols with Market or Exchange execution modes. If filled
partially, a market or limit order with the remaining volume is not canceled, and is processed further.
During activation of the ORDER_TYPE_BUY_STOP_LIMIT and ORDER_TYPE_SELL_STOP_LIMIT orders, an appropriate
limit order ORDER_TYPE_BUY_LIMIT/ORDER_TYPE_SELL_LIMIT with the ORDER_FILLING_RETURN type is created.
"""
__enum_name__ = "ORDER_FILLING"
FOK = mt5.ORDER_FILLING_FOK
IOC = mt5.ORDER_FILLING_IOC
RETURN = mt5.ORDER_FILLING_RETURN
class OrderTime(Repr, IntEnum):
"""ORDER_TIME Enum.
Attributes:
GTC (int): Good till cancel order
DAY (int): Good till current trade day order
SPECIFIED (int): The order is active until the specified date
SPECIFIED_DAY (int): The order is active until 23:59:59 of the specified day. If this time appears to be out of
a trading session, the expiration is processed at the nearest trading time.
"""
__enum_name__ = "ORDER_TIME"
GTC = mt5.ORDER_TIME_GTC
DAY = mt5.ORDER_TIME_DAY
SPECIFIED = mt5.ORDER_TIME_SPECIFIED
SPECIFIED_DAY = mt5.ORDER_TIME_SPECIFIED_DAY
class OrderType(Repr, IntEnum):
"""ORDER_TYPE Enum.
Attributes:
BUY (int): Market buy order
SELL (int): Market sell order
BUY_LIMIT (int): Buy Limit pending order
SELL_LIMIT (int): Sell Limit pending order
BUY_STOP (int): Buy Stop pending order
SELL_STOP (int): Sell Stop pending order
BUY_STOP_LIMIT (int): Upon reaching the order price, Buy Limit pending order is placed at StopLimit price
SELL_STOP_LIMIT (int): Upon reaching the order price, Sell Limit pending order is placed at StopLimit price
CLOSE_BY (int): Order for closing a position by an opposite one
Properties:
opposite (int): Gets the opposite of an order type
"""
__enum_name__ = "ORDER_TYPE"
BUY = mt5.ORDER_TYPE_BUY
SELL = mt5.ORDER_TYPE_SELL
BUY_LIMIT = mt5.ORDER_TYPE_BUY_LIMIT
SELL_LIMIT = mt5.ORDER_TYPE_SELL_LIMIT
BUY_STOP = mt5.ORDER_TYPE_BUY_STOP
SELL_STOP = mt5.ORDER_TYPE_SELL_STOP
BUY_STOP_LIMIT = mt5.ORDER_TYPE_BUY_STOP_LIMIT
SELL_STOP_LIMIT = mt5.ORDER_TYPE_SELL_STOP_LIMIT
CLOSE_BY = mt5.ORDER_TYPE_CLOSE_BY
@property
def opposite(self):
"""Gets the opposite of an order type for closing an open position
Returns:
int: integer value of opposite order type
"""
return {0: 1, 1: 0, 2: 3, 3: 2, 4: 5, 5: 4, 6: 7, 7: 6, 8: 8}[self]
class BookType(Repr, IntEnum):
"""BOOK_TYPE Enum.
Attributes:
SELL (int): Sell order (Offer)
BUY (int): Buy order (Bid)
SELL_MARKET (int): Sell order by Market
BUY_MARKET (int): Buy order by Market
"""
__enum_name__ = "BOOK_TYPE"
SELL = mt5.BOOK_TYPE_SELL
BUY = mt5.BOOK_TYPE_BUY
SELL_MARKET = mt5.BOOK_TYPE_SELL_MARKET
BUY_MARKET = mt5.BOOK_TYPE_BUY_MARKET
class TimeFrame(Repr, IntEnum):
"""TIMEFRAME Enum.
Attributes:
M1 (int): One Minute
M2 (int): Two Minutes
M3 (int): Three Minutes
M4 (int): Four Minutes
M5 (int): Five Minutes
M6 (int): Six Minutes
M10 (int): Ten Minutes
M15 (int): Fifteen Minutes
M20 (int): Twenty Minutes
M30 (int): Thirty Minutes
H1 (int): One Hour
H2 (int): Two Hours
H3 (int): Three Hours
H4 (int): Four Hours
H6 (int): Six Hours
H8 (int): Eight Hours
D1 (int): One Day
W1 (int): One Week
MN1 (int): One Month
Properties:
time: return the value of the timeframe object in seconds. Used as a property
Methods:
get: get a timeframe object from a time value in seconds
"""
__enum_name__ = "TIMEFRAME"
def __str__(self):
return self.name
M1 = mt5.TIMEFRAME_M1
M2 = mt5.TIMEFRAME_M2
M3 = mt5.TIMEFRAME_M3
M4 = mt5.TIMEFRAME_M4
M5 = mt5.TIMEFRAME_M5
M6 = mt5.TIMEFRAME_M6
M10 = mt5.TIMEFRAME_M10
M15 = mt5.TIMEFRAME_M15
M20 = mt5.TIMEFRAME_M20
M30 = mt5.TIMEFRAME_M30
H1 = mt5.TIMEFRAME_H1
H2 = mt5.TIMEFRAME_H2
H3 = mt5.TIMEFRAME_H3
H4 = mt5.TIMEFRAME_H4
H6 = mt5.TIMEFRAME_H6
H8 = mt5.TIMEFRAME_H8
H12 = mt5.TIMEFRAME_H12
D1 = mt5.TIMEFRAME_D1
W1 = mt5.TIMEFRAME_W1
MN1 = mt5.TIMEFRAME_MN1
@property
def time(self):
"""The number of seconds in a TIMEFRAME
Returns:
int: The number of seconds in a TIMEFRAME
Examples:
>>> t = TimeFrame.H1
>>> print(t.time)
3600
"""
times = {1: 60, 2: 120, 3: 180, 4: 240, 5: 300, 6: 360, 10: 600, 15: 900, 20: 1200, 30: 1800, 16385: 3600,
16386: 7200, 16387: 10800, 16388: 14400, 16390: 21600, 16392: 28800, 16396: 43200, 16408: 86400,
32769: 604800, 49153: 2592000}
return times[self]
@classmethod
def get(cls, time: int) -> 'TimeFrame':
times = {60: 1, 120: 2, 180: 3, 240: 4, 300: 5, 360: 6, 600: 10, 900: 15, 1200: 20, 1800: 30, 3600: 16385,
7200: 16386, 10800: 16387, 14400: 16388, 21600: 16390, 28800: 16392, 43200: 16396, 86400: 16408,
604800: 32769, 2592000: 49153}
return TimeFrame(times[int(time)])
class CopyTicks(Repr, IntEnum):
"""COPY_TICKS Enum. This defines the types of ticks that can be requested using the copy_ticks_from() and
copy_ticks_range() functions.
Attributes:
ALL (int): All ticks
INFO (int): Ticks containing Bid and/or Ask price changes
TRADE (int): Ticks containing Last and/or Volume price changes
"""
__enum_name__ = "COPY_TICKS"
ALL = mt5.COPY_TICKS_ALL
INFO = mt5.COPY_TICKS_INFO
TRADE = mt5.COPY_TICKS_TRADE
class PositionType(Repr, IntEnum):
"""POSITION_TYPE Enum. Direction of an open position (buy or sell)
Attributes:
BUY (int): Buy
SELL (int): Sell
"""
__enum_name__ = "POSITION_TYPE"
BUY = mt5.POSITION_TYPE_BUY
SELL = mt5.POSITION_TYPE_SELL
class PositionReason(Repr, IntEnum):
"""POSITION_REASON Enum. The reason for opening a position is contained in the POSITION_REASON Enum
Attributes:
CLIENT (int): The position was opened as a result of activation of an order placed from a desktop terminal
MOBILE (int): The position was opened as a result of activation of an order placed from a mobile application
WEB (int): The position was opened as a result of activation of an order placed from the web platform
EXPERT (int): The position was opened as a result of activation of an order placed from an MQL5 program,
i.e. an Expert Advisor or a script
"""
__enum_name__ = "POSITION_REASON"
CLIENT = mt5.POSITION_REASON_CLIENT
MOBILE = mt5.POSITION_REASON_MOBILE
WEB = mt5.POSITION_REASON_WEB
EXPERT = mt5.POSITION_REASON_EXPERT
class DealType(Repr, IntEnum):
"""DEAL_TYPE enum. Each deal is characterized by a type, allowed values are enumerated in this enum
Attributes:
BUY (int): Buy
SELL (int): Sell
BALANCE (int): Balance
CREDIT (int): Credit
CHARGE (int): Additional Charge
CORRECTION (int): Correction
BONUS (int): Bonus
COMMISSION (int): Additional Commission
COMMISSION_DAILY (int): Daily Commission
COMMISSION_MONTHLY (int): Monthly Commission
COMMISSION_AGENT_DAILY (int): Daily Agent Commission
COMMISSION_AGENT_MONTHLY (int): Monthly Agent Commission
INTEREST (int): Interest Rate
DEAL_DIVIDEND (int): Dividend Operations
DEAL_DIVIDEND_FRANKED (int): Franked (non-taxable) dividend operations
DEAL_TAX (int): Tax Charges
BUY_CANCELED (int): Canceled buy deal. There can be a situation when a previously executed buy deal is canceled.
In this case, the type of the previously executed deal (DEAL_TYPE_BUY) is changed to DEAL_TYPE_BUY_CANCELED,
and its profit/loss is zeroized. Previously obtained profit/loss is charged/withdrawn using a separated
balance operation
SELL_CANCELED (int): Canceled sell deal. There can be a situation when a previously executed sell deal is
canceled. In this case, the type of the previously executed deal (DEAL_TYPE_SELL) is changed to
DEAL_TYPE_SELL_CANCELED, and its profit/loss is zeroized. Previously obtained profit/loss is
charged/withdrawn using a separated balance operation.
"""
__enum_name__ = "DEAL_TYPE"
BUY = mt5.DEAL_TYPE_BUY
SELL = mt5.DEAL_TYPE_SELL
BALANCE = mt5.DEAL_TYPE_BALANCE
CREDIT = mt5.DEAL_TYPE_CREDIT
CHARGE = mt5.DEAL_TYPE_CHARGE
CORRECTION = mt5.DEAL_TYPE_CORRECTION
BONUS = mt5.DEAL_TYPE_BONUS
COMMISSION = mt5.DEAL_TYPE_COMMISSION
COMMISSION_DAILY = mt5.DEAL_TYPE_COMMISSION_DAILY
COMMISSION_MONTHLY = mt5.DEAL_TYPE_COMMISSION_MONTHLY
COMMISSION_AGENT_DAILY = mt5.DEAL_TYPE_COMMISSION_AGENT_DAILY
COMMISSION_AGENT_MONTHLY = mt5.DEAL_TYPE_COMMISSION_AGENT_MONTHLY
INTEREST = mt5.DEAL_TYPE_INTEREST
BUY_CANCELED = mt5.DEAL_TYPE_BUY_CANCELED
SELL_CANCELED = mt5.DEAL_TYPE_SELL_CANCELED
DEAL_DIVIDEND = mt5.DEAL_DIVIDEND
DEAL_DIVIDEND_FRANKED = mt5.DEAL_DIVIDEND_FRANKED
DEAL_TAX = mt5.DEAL_TAX
def __str__(self):
if self.name in ('DEAL_DIVIDEND', 'DEAL_DIVIDEND_FRANKED', 'DEAL_TAX'):
return self.name
return super().__str__()
class DealEntry(Repr, IntEnum):
"""DEAL_ENTRY Enum. Deals differ not only in their types set in DEAL_TYPE enum, but also in the way they change
positions. This can be a simple position opening, or accumulation of a previously opened position (market entering),
position closing by an opposite deal of a corresponding volume (market exiting), or position reversing, if the
opposite-direction deal covers the volume of the previously opened position.
Attributes:
IN (int): Entry In
OUT (int): Entry Out
INOUT (int): Reverse
OUT_BY (int): Close a position by an opposite one
"""
__enum_name__ = "DEAL_ENTRY"
IN = mt5.DEAL_ENTRY_IN
OUT = mt5.DEAL_ENTRY_OUT
INOUT = mt5.DEAL_ENTRY_INOUT
OUT_BY = mt5.DEAL_ENTRY_OUT_BY
class DealReason(Repr, IntEnum):
"""DEAL_REASON Enum. The reason for deal execution is contained in the DEAL_REASON property. A deal can be executed
as a result of triggering of an order placed from a mobile application or an MQL5 program, as well as a result
of the StopOut event, variation margin calculation, etc.
Attributes:
CLIENT (int): The deal was executed as a result of activation of an order placed from a desktop terminal
MOBILE (int): The deal was executed as a result of activation of an order placed from a desktop terminal
WEB (int): The deal was executed as a result of activation of an order placed from the web platform
EXPERT (int): The deal was executed as a result of activation of an order placed from an MQL5 program, i.e.
an Expert Advisor or a script
SL (int): The deal was executed as a result of Stop Loss activation
TP (int): The deal was executed as a result of Take Profit activation
SO (int): The deal was executed as a result of the Stop Out event
ROLLOVER (int): The deal was executed due to a rollover
VMARGIN (int): The deal was executed after charging the variation margin
SPLIT (int): The deal was executed after the split (price reduction) of an instrument, which had an open
position during split announcement
"""
__enum_name__ = "DEAL_REASON"
CLIENT = mt5.DEAL_REASON_CLIENT
MOBILE = mt5.DEAL_REASON_MOBILE
WEB = mt5.DEAL_REASON_WEB
EXPERT = mt5.DEAL_REASON_EXPERT
SL = mt5.DEAL_REASON_SL
TP = mt5.DEAL_REASON_TP
SO = mt5.DEAL_REASON_SO
ROLLOVER = mt5.DEAL_REASON_ROLLOVER
VMARGIN = mt5.DEAL_REASON_VMARGIN
SPLIT = mt5.DEAL_REASON_SPLIT
class OrderReason(Repr, IntEnum):
"""ORDER_REASON Enum.
Attributes:
CLIENT (int): The order was placed from a desktop terminal
MOBILE (int): The order was placed from a mobile application
WEB (int): The order was placed from a web platform
EXPERT (int): The order was placed from an MQL5-program, i.e. by an Expert Advisor or a script
SL (int): The order was placed as a result of Stop Loss activation
TP (int): The order was placed as a result of Take Profit activation
SO (int): The order was placed as a result of the Stop Out event
"""
__enum_name__ = "ORDER_REASON"
CLIENT = mt5.ORDER_REASON_CLIENT
MOBILE = mt5.ORDER_REASON_MOBILE
WEB = mt5.ORDER_REASON_WEB
EXPERT = mt5.ORDER_REASON_EXPERT
SL = mt5.ORDER_REASON_SL
TP = mt5.ORDER_REASON_TP
SO = mt5.ORDER_REASON_SO
class SymbolChartMode(Repr, IntEnum):
"""SYMBOL_CHART_MODE Enum. A symbol price chart can be based on Bid or Last prices. The price selected for symbol
charts also affects the generation and display of bars in the terminal.
Possible values of the SYMBOL_CHART_MODE property are described in this enum
Attributes:
BID (int): Bars are based on Bid prices
LAST (int): Bars are based on last prices
"""
__enum_name__ = "SYMBOL_CHART_MODE"
BID = mt5.SYMBOL_CHART_MODE_BID
LAST = mt5.SYMBOL_CHART_MODE_LAST
class SymbolCalcMode(Repr, IntEnum):
"""SYMBOL_CALC_MODE Enum. The SYMBOL_CALC_MODE enumeration is used for obtaining information about how the margin
requirements for a symbol are calculated.
Attributes:
FOREX (int): Forex mode - calculation of profit and margin for Forex
FOREX_NO_LEVERAGE (int): Forex No Leverage mode calculation of profit and margin for Forex symbols without
taking into account the leverage
FUTURES (int): Futures mode - calculation of margin and profit for futures
CFD (int): CFD mode - calculation of margin and profit for CFD
CFDINDEX (int): CFD index mode - calculation of margin and profit for CFD by indexes
CFDLEVERAGE (int): CFD Leverage mode - calculation of margin and profit for CFD at leverage trading
EXCH_STOCKS (int): Calculation of margin and profit for trading securities on a stock exchange
EXCH_FUTURES (int): Calculation of margin and profit for trading futures contracts on a stock exchange
EXCH_OPTIONS (int): value is 34
EXCH_OPTIONS_MARGIN (int): value is 36
EXCH_BONDS (int): Exchange Bonds mode calculation of margin and profit for trading bonds on a stock exchange
EXCH_STOCKS_MOEX (int): Exchange MOEX Stocks mode calculation of margin and profit for trading securities on
MOEX
EXCH_BONDS_MOEX (int): Exchange MOEX Bonds mode calculation of margin and profit for trading bonds on MOEX
SERV_COLLATERAL (int): Collateral mode - a symbol is used as a non-tradeable asset on a trading account.
The market value of an open position is calculated based on the volume, current market price, contract size
and liquidity ratio. The value is included into Assets, which are added to Equity. Open positions of such
symbols increase the Free Margin amount and are used as additional margin (collateral) for open positions
"""
__enum_name__ = "SYMBOL_CALC_MODE"
FOREX = mt5.SYMBOL_CALC_MODE_FOREX
FOREX_NO_LEVERAGE = mt5.SYMBOL_CALC_MODE_FOREX_NO_LEVERAGE
FUTURES = mt5.SYMBOL_CALC_MODE_FUTURES
CFD = mt5.SYMBOL_CALC_MODE_CFD
CFDINDEX = mt5.SYMBOL_CALC_MODE_CFDINDEX
CFDLEVERAGE = mt5.SYMBOL_CALC_MODE_CFDLEVERAGE
EXCH_STOCKS = mt5.SYMBOL_CALC_MODE_EXCH_STOCKS
EXCH_FUTURES = mt5.SYMBOL_CALC_MODE_EXCH_FUTURES
EXCH_OPTIONS = mt5.SYMBOL_CALC_MODE_EXCH_OPTIONS
EXCH_OPTIONS_MARGIN = mt5.SYMBOL_CALC_MODE_EXCH_OPTIONS_MARGIN
EXCH_BONDS = mt5.SYMBOL_CALC_MODE_EXCH_BONDS
EXCH_STOCKS_MOEX = mt5.SYMBOL_CALC_MODE_EXCH_STOCKS_MOEX
EXCH_BONDS_MOEX = mt5.SYMBOL_CALC_MODE_EXCH_BONDS_MOEX
SERV_COLLATERAL = mt5.SYMBOL_CALC_MODE_SERV_COLLATERAL
class SymbolTradeMode(Repr, IntEnum):
"""SYMBOL_TRADE_MODE Enum. There are several symbol trading modes. Information about trading modes of a certain
symbol is reflected in the values this enumeration
Attributes:
DISABLED (int): Trade is disabled for the symbol
LONGONLY (int): Allowed only long positions
SHORTONLY (int): Allowed only short positions
CLOSEONLY (int): Allowed only position close operations
FULL (int): No trade restrictions
"""
__enum_name__ = "SYMBOL_TRADE_MODE"
DISABLED = mt5.SYMBOL_TRADE_MODE_DISABLED
LONGONLY = mt5.SYMBOL_TRADE_MODE_LONGONLY
SHORTONLY = mt5.SYMBOL_TRADE_MODE_SHORTONLY
CLOSEONLY = mt5.SYMBOL_TRADE_MODE_CLOSEONLY
FULL = mt5.SYMBOL_TRADE_MODE_FULL
class SymbolTradeExecution(Repr, IntEnum):
"""SYMBOL_TRADE_EXECUTION Enum. The modes, or execution policies, define the rules for cases when the price has
changed or the requested volume cannot be completely fulfilled at the moment.
Attributes:
REQUEST (int): Executing a market order at the price previously received from the broker. Prices for a certain
market order are requested from the broker before the order is sent. Upon receiving the prices, order
execution at the given price can be either confirmed or rejected.
INSTANT (int): Executing a market order at the specified price immediately. When sending a trade request to be
executed, the platform automatically adds the current prices to the order.
- If the broker accepts the price, the order is executed.
- If the broker does not accept the requested price, a "Requote" is sent — the broker returns prices,
at which this order can be executed.
MARKET (int): A broker makes a decision about the order execution price without any additional discussion with
the trader.
Sending the order in such a mode means advance consent to its execution at this price.
EXCHANGE (int): Trade operations are executed at the prices of the current market offers.
"""
__enum_name__ = "SYMBOL_TRADE_EXECUTION"
REQUEST = mt5.SYMBOL_TRADE_EXECUTION_REQUEST
INSTANT = mt5.SYMBOL_TRADE_EXECUTION_INSTANT
MARKET = mt5.SYMBOL_TRADE_EXECUTION_MARKET
EXCHANGE = mt5.SYMBOL_TRADE_EXECUTION_EXCHANGE
class SymbolSwapMode(Repr, IntEnum):
"""SYMBOL_SWAP_MODE Enum. Methods of swap calculation at position transfer are specified in enumeration
ENUM_SYMBOL_SWAP_MODE. The method of swap calculation determines the units of measure of the SYMBOL_SWAP_LONG and
SYMBOL_SWAP_SHORT parameters. For example, if swaps are charged in the client deposit currency, then the values of
those parameters are specified as an amount of money in the client deposit currency.
Attributes:
DISABLED (int): Swaps disabled (no swaps)
POINTS (int): Swaps are charged in points
CURRENCY_SYMBOL (int): Swaps are charged in money in base currency of the symbol
CURRENCY_MARGIN (int): Swaps are charged in money in margin currency of the symbol
CURRENCY_DEPOSIT (int): Swaps are charged in money, in client deposit currency
INTEREST_CURRENT (int): Swaps are charged as the specified annual interest from the instrument price at
calculation of swap (standard bank year is 360 days)
INTEREST_OPEN (int): Swaps are charged as the specified annual interest from the open price of position
(standard bank year is 360 days)
REOPEN_CURRENT (int): Swaps are charged by reopening positions. At the end of a trading day the position is
closed. Next day it is reopened by the close price +/- specified number of points
(parameters SYMBOL_SWAP_LONG and SYMBOL_SWAP_SHORT)
REOPEN_BID (int): Swaps are charged by reopening positions. At the end of a trading day the position is closed.
Next day it is reopened by the current Bid price +/- specified number of
points (parameters SYMBOL_SWAP_LONG and SYMBOL_SWAP_SHORT)
"""
__enum_name__ = "SYMBOL_SWAP_MODE"
DISABLED = mt5.SYMBOL_SWAP_MODE_DISABLED
POINTS = mt5.SYMBOL_SWAP_MODE_POINTS
CURRENCY_SYMBOL = mt5.SYMBOL_SWAP_MODE_CURRENCY_SYMBOL
CURRENCY_MARGIN = mt5.SYMBOL_SWAP_MODE_CURRENCY_MARGIN
CURRENCY_DEPOSIT = mt5.SYMBOL_SWAP_MODE_CURRENCY_DEPOSIT
INTEREST_CURRENT = mt5.SYMBOL_SWAP_MODE_INTEREST_CURRENT
INTEREST_OPEN = mt5.SYMBOL_SWAP_MODE_INTEREST_OPEN
REOPEN_CURRENT = mt5.SYMBOL_SWAP_MODE_REOPEN_CURRENT
REOPEN_BID = mt5.SYMBOL_SWAP_MODE_REOPEN_BID
class DayOfWeek(Repr, IntEnum):
"""DAY_OF_WEEK Enum.
Attributes:
SUNDAY (int): Sunday
MONDAY (int): Monday
TUESDAY (int): Tuesday
WEDNESDAY (int): Wednesday
THURSDAY (int): Thursday
FRIDAY (int): Friday
SATURDAY (int): Saturday
"""
__enum__name__ = "DAY_OF_WEEK"
SUNDAY = mt5.DAY_OF_WEEK_SUNDAY
MONDAY = mt5.DAY_OF_WEEK_MONDAY
TUESDAY = mt5.DAY_OF_WEEK_TUESDAY
WEDNESDAY = mt5.DAY_OF_WEEK_WEDNESDAY
THURSDAY = mt5.DAY_OF_WEEK_THURSDAY
FRIDAY = mt5.DAY_OF_WEEK_FRIDAY
SATURDAY = mt5.DAY_OF_WEEK_SATURDAY
class SymbolOrderGTCMode(Repr, IntEnum):
"""SYMBOL_ORDER_GTC_MODE Enum. If the SYMBOL_EXPIRATION_MODE property is set to SYMBOL_EXPIRATION_GTC
(good till canceled), the expiration of pending orders, as well as of
Stop Loss/Take Profit orders should be additionally set using the ENUM_SYMBOL_ORDER_GTC_MODE enumeration.
Attributes:
GTC (int): Pending orders and Stop Loss/Take Profit levels are valid for an unlimited period
until theirConstants, Enumerations and explicit cancellation
DAILY (int): Orders are valid during one trading day. At the end of the day, all Stop Loss and
Take Profit levels, as well as pending orders are deleted.
DAILY_NO_STOPS (int): When a trade day changes, only pending orders are deleted,
while Stop Loss and Take Profit levels are preserved
"""
__enum_name__ = "SYMBOL_ORDERS"
GTC = mt5.SYMBOL_ORDERS_GTC
DAILY = mt5.SYMBOL_ORDERS_DAILY
DAILY_NO_STOPS = mt5.SYMBOL_ORDERS_DAILY_NO_STOPS
class SymbolOptionRight(Repr, IntEnum):
"""SYMBOL_OPTION_RIGHT Enum. An option is a contract, which gives the right, but not the obligation,
to buy or sell an underlying asset (goods, stocks, futures, etc.) at a specified price on or before a specific date.
The following enumerations describe option properties, including the option type and the right arising from it.
Attributes:
CALL (int): A call option gives you the right to buy an asset at a specified price.
PUT (int): A put option gives you the right to sell an asset at a specified price.
"""
__enum_name__ = "SYMBOL_OPTION_RIGHT"
CALL = mt5.SYMBOL_OPTION_RIGHT_CALL
PUT = mt5.SYMBOL_OPTION_RIGHT_PUT
class SymbolOptionMode(Repr, IntEnum):
"""SYMBOL_OPTION_MODE Enum.
Attributes:
EUROPEAN (int): European option may only be exercised on a specified date
(expiration, execution date, delivery date)
AMERICAN (int): American option may be exercised on any trading day or before expiry. The period within which
a buyer can exercise the option is specified for it.
"""
__enum_name__ = "SYMBOL_OPTION_MODE"
EUROPEAN = mt5.SYMBOL_OPTION_MODE_EUROPEAN
AMERICAN = mt5.SYMBOL_OPTION_MODE_AMERICAN
class AccountTradeMode(Repr, IntEnum):
"""ACCOUNT_TRADE_MODE Enum. There are several types of accounts that can be opened on a trade server.
The type of account on which an MQL5 program is running can be found out using
the ENUM_ACCOUNT_TRADE_MODE enumeration.
Attributes:
DEMO: Demo account
CONTEST: Contest account
REAL: Real Account
"""
__enum_name__ = "ACCOUNT_TRADE_MODE"
DEMO = mt5.ACCOUNT_TRADE_MODE_DEMO
CONTEST = mt5.ACCOUNT_TRADE_MODE_CONTEST
REAL = mt5.ACCOUNT_TRADE_MODE_REAL
class TickFlag(Repr, IntFlag):
"""TICK_FLAG Enum. TICK_FLAG defines possible flags for ticks. The Flags are used to describe ticks obtained by the
copy_ticks_from() and copy_ticks_range() functions.
Attributes:
BID (int): Bid price changed
ASK (int): Ask price changed
LAST (int): Last price changed
VOLUME (int): Volume changed
BUY (int): last Buy price changed
SELL (int): last Sell price changed
"""
__enum_name__ = "TICK_FLAG"
BID = mt5.TICK_FLAG_BID
ASK = mt5.TICK_FLAG_ASK
LAST = mt5.TICK_FLAG_LAST
VOLUME = mt5.TICK_FLAG_VOLUME
BUY = mt5.TICK_FLAG_BUY
SELL = mt5.TICK_FLAG_SELL
class TradeRetcode(Repr, IntEnum):
"""TRADE_RETCODE Enum. Return codes for order send/check operations
Attributes:
REQUOTE (int): Requote
REJECT (int): Request rejected
CANCEL (int): Request canceled by trader
PLACED (int): Order placed
DONE (int): Request completed
DONE_PARTIAL (int): Only part of the request was completed
ERROR (int): Request processing error
TIMEOUT (int): Request canceled by timeout
INVALID (int): Invalid request
INVALID_VOLUME (int): Invalid volume in the request
INVALID_PRICE (int): Invalid price in the request
INVALID_STOPS (int): Invalid stops in the request
TRADE_DISABLED (int): Trade is disabled
MARKET_CLOSED (int): Market is closed
NO_MONEY (int): There is not enough money to complete the request
PRICE_CHANGED (int): Prices changed
PRICE_OFF (int): There are no quotes to process the request
INVALID_EXPIRATION (int): Invalid order expiration date in the request
ORDER_CHANGED (int): Order state changed
TOO_MANY_REQUESTS (int): Too frequent requests
NO_CHANGES (int): No changes in request
SERVER_DISABLES_AT (int): Autotrading disabled by server
CLIENT_DISABLES_AT (int): Autotrading disabled by client terminal
LOCKED (int): Request locked for processing
FROZEN (int): Order or position frozen
INVALID_FILL (int): Invalid order filling type
CONNECTION (int): No connection with the trade server
ONLY_REAL (int): Operation is allowed only for live accounts
LIMIT_ORDERS (int): The number of pending orders has reached the limit
LIMIT_VOLUME (int): The volume of orders and positions for the symbol has reached the limit
INVALID_ORDER (int): Incorrect or prohibited order type
POSITION_CLOSED (int): Position with the specified POSITION_IDENTIFIER has already been closed
INVALID_CLOSE_VOLUME (int): A close volume exceeds the current position volume
CLOSE_ORDER_EXIST (int): A close order already exists for a specified position. This may happen when working in
the hedging system:
· when attempting to close a position with an opposite one, while close orders for the position already
exist
· when attempting to fully or partially close a position if the total volume of the already present close
orders and the newly placed one exceeds the current position volume
LIMIT_POSITIONS (int): The number of open positions simultaneously present on an account can be limited by the
server settings.After a limit is reached, the server returns the TRADE_RETCODE_LIMIT_POSITIONS error when
attempting to place an order. The limitation operates differently depending on the position accounting type:
· Netting — number of open positions is considered. When a limit is reached, the platform does not let
placing new orders whose execution may increase the number of open positions. In fact, the platform
allows placing orders only for the symbols that already have open positions.
The current pending orders are not considered since their execution may lead to changes in the current
positions but it cannot increase their number.
· Hedging — pending orders are considered together with open positions, since a pending order activation
always leads to opening a new position. When a limit is reached, the platform does not allow placing
both new market orders for opening positions and pending orders.
REJECT_CANCEL (int): The pending order activation request is rejected, the order is canceled.
LONG_ONLY (int): The request is rejected, because the "Only long positions are allowed" rule is set for the
symbol (POSITION_TYPE_BUY)
SHORT_ONLY (int): The request is rejected, because the "Only short positions are allowed" rule is set for the
symbol (POSITION_TYPE_SELL)
CLOSE_ONLY (int): The request is rejected, because the "Only position closing is allowed" rule is set for the
symbol
FIFO_CLOSE (int): The request is rejected, because "Position closing is allowed only by FIFO rule" flag is set
for the trading account (ACCOUNT_FIFO_CLOSE=true)
"""
__enum_name__ = "TRADE_RETCODE"
REQUOTE = mt5.TRADE_RETCODE_REQUOTE
REJECT = mt5.TRADE_RETCODE_REJECT
CANCEL = mt5.TRADE_RETCODE_CANCEL
PLACED = mt5.TRADE_RETCODE_PLACED
DONE = mt5.TRADE_RETCODE_DONE
DONE_PARTIAL = mt5.TRADE_RETCODE_DONE_PARTIAL
ERROR = mt5.TRADE_RETCODE_ERROR
TIMEOUT = mt5.TRADE_RETCODE_TIMEOUT
INVALID = mt5.TRADE_RETCODE_INVALID
INVALID_VOLUME = mt5.TRADE_RETCODE_INVALID_VOLUME
INVALID_PRICE = mt5.TRADE_RETCODE_INVALID_PRICE
INVALID_STOPS = mt5.TRADE_RETCODE_INVALID_STOPS
TRADE_DISABLED = mt5.TRADE_RETCODE_TRADE_DISABLED
MARKET_CLOSED = mt5.TRADE_RETCODE_MARKET_CLOSED
NO_MONEY = mt5.TRADE_RETCODE_NO_MONEY
PRICE_CHANGED = mt5.TRADE_RETCODE_PRICE_CHANGED
PRICE_OFF = mt5.TRADE_RETCODE_PRICE_OFF
INVALID_EXPIRATION = mt5.TRADE_RETCODE_INVALID_EXPIRATION
ORDER_CHANGED = mt5.TRADE_RETCODE_ORDER_CHANGED
TOO_MANY_REQUESTS = mt5.TRADE_RETCODE_TOO_MANY_REQUESTS
NO_CHANGES = mt5.TRADE_RETCODE_NO_CHANGES
SERVER_DISABLES_AT = mt5.TRADE_RETCODE_SERVER_DISABLES_AT
CLIENT_DISABLES_AT = mt5.TRADE_RETCODE_CLIENT_DISABLES_AT
LOCKED = mt5.TRADE_RETCODE_LOCKED
FROZEN = mt5.TRADE_RETCODE_FROZEN
INVALID_FILL = mt5.TRADE_RETCODE_INVALID_FILL
CONNECTION = mt5.TRADE_RETCODE_CONNECTION
ONLY_REAL = mt5.TRADE_RETCODE_ONLY_REAL
LIMIT_ORDERS = mt5.TRADE_RETCODE_LIMIT_ORDERS
LIMIT_VOLUME = mt5.TRADE_RETCODE_LIMIT_VOLUME
INVALID_ORDER = mt5.TRADE_RETCODE_INVALID_ORDER
POSITION_CLOSED = mt5.TRADE_RETCODE_POSITION_CLOSED
INVALID_CLOSE_VOLUME = mt5.TRADE_RETCODE_INVALID_CLOSE_VOLUME
CLOSE_ORDER_EXIST = mt5.TRADE_RETCODE_CLOSE_ORDER_EXIST
LIMIT_POSITIONS = mt5.TRADE_RETCODE_LIMIT_POSITIONS
REJECT_CANCEL = mt5.TRADE_RETCODE_REJECT_CANCEL
LONG_ONLY = mt5.TRADE_RETCODE_LONG_ONLY
SHORT_ONLY = mt5.TRADE_RETCODE_SHORT_ONLY
CLOSE_ONLY = mt5.TRADE_RETCODE_CLOSE_ONLY
FIFO_CLOSE = mt5.TRADE_RETCODE_FIFO_CLOSE
class AccountStopOutMode(Repr, IntEnum):
"""ACCOUNT_STOPOUT_MODE Enum.
Attributes:
PERCENT (int): Account stop out mode in percents
MONEY (int): Account stop out mode in money
"""
__enum_name__ = "ACCOUNT_STOPOUT_MODE"
PERCENT = mt5.ACCOUNT_STOPOUT_MODE_PERCENT
MONEY = mt5.ACCOUNT_STOPOUT_MODE_MONEY
class AccountMarginMode(Repr, IntEnum):
"""ACCOUNT_MARGIN_MODE Enum.
Attributes:
RETAIL_NETTING (int): Used for the OTC markets to interpret positions in the "netting"
mode (only one position can exist for one symbol). The margin is calculated based on the symbol
type (SYMBOL_TRADE_CALC_MODE).
EXCHANGE (int): Used for the exchange markets. Margin is calculated based on the discounts specified in
symbol settings. Discounts are set by the broker, but not less than the values set by the exchange.
RETAIL_HEDGING (int): Used for the exchange markets where individual positions are possible
(hedging, multiple positions can exist for one symbol). The margin is calculated based on the symbol
type (SYMBOL_TRADE_CALC_MODE) taking into account the hedged margin (SYMBOL_MARGIN_HEDGED).
"""
__enum_name__ = "ACCOUNT_MARGIN_MODE"
RETAIL_NETTING = mt5.ACCOUNT_MARGIN_MODE_RETAIL_NETTING
EXCHANGE = mt5.ACCOUNT_MARGIN_MODE_EXCHANGE
RETAIL_HEDGING = mt5.ACCOUNT_MARGIN_MODE_RETAIL_HEDGING
+33
View File
@@ -0,0 +1,33 @@
class Error:
"""Error class for handling errors from MetaTrader 5."""
descriptions = {
# common errors
1: 'Successful',
-1: 'generic fail',
-2: 'invalid arguments/parameters',
-3: 'no memory condition',
-4: 'no history',
-5: 'invalid version',
-6: 'authorization failed',
-7: 'unsupported method',
-8: 'auto-trading disabled',
# internal errors
-10000: 'internal IPC general error',
-10001: 'internal IPC send failed',
-10002: 'internal IPC recv failed',
-10003: 'internal IPC initialization fail',
-10004: 'internal IPC no ipc',
-10005: 'internal timeout',
}
conn_errors = (-10000, -10001, -10002, -10003, -10004, -10005)
def __init__(self, code: int, description: str = ''):
self.code = code
self.description = description or self.descriptions.get(code, 'Unknown Error')
def is_connection_error(self):
return self.code in self.conn_errors
def __repr__(self):
return f"{self.code}: {self.description}"
+19
View File
@@ -0,0 +1,19 @@
"""Exceptions for the aiomql package."""
__all__ = ['LoginError', 'VolumeError', 'SymbolError', 'OrderError']
class LoginError(Exception):
"""Raised when an error occurs when logging in."""
pass
class VolumeError(Exception):
"""Raised when a volume is not valid or out of range for a symbol."""
pass
class SymbolError(Exception):
"""Raised when a symbol is not provided where required or not available in the Market Watch."""
class OrderError(Exception):
"""Raised when an error occurs when working with the order class."""
+349
View File
@@ -0,0 +1,349 @@
from datetime import datetime
import asyncio
from logging import getLogger
from typing import Callable
import MetaTrader5
from MetaTrader5 import BookInfo, SymbolInfo, AccountInfo, Tick, TerminalInfo, TradeOrder, TradeDeal, \
TradePosition, OrderSendResult, OrderCheckResult
from .constants import TimeFrame, CopyTicks, OrderType
from .errors import Error
from .config import Config
logger = getLogger()
class BaseMeta(type):
def __new__(mcs, cls_name, bases, cls_dict):
defaults = MetaTrader5.__dict__
defaults = {f'_{key}': value for key, value in defaults.items() if not key.startswith('_')}
cls_dict |= defaults
return super().__new__(mcs, cls_name, bases, cls_dict)
class MetaTrader(metaclass=BaseMeta):
_account_info: Callable
_copy_rates_from: Callable
_copy_rates_from_pos: Callable
_copy_rates_range: Callable
_copy_ticks_from: Callable
_copy_ticks_range: Callable
_history_deals_get: Callable
_history_deals_total: Callable
_history_orders_get: Callable
_history_orders_total: Callable
_initialize: Callable
_last_error: Callable
_login: Callable
_market_book_add: Callable
_market_book_get: Callable
_market_book_release: Callable
_order_calc_margin: Callable
_order_calc_profit: Callable
_order_check: Callable
_order_send: Callable
_orders_get: Callable
_orders_total: Callable
_positions_get: Callable
_positions_total: Callable
_shutdown: Callable
_symbol_info: Callable
_symbol_info_tick: Callable
_symbol_select: Callable
_symbols_get: Callable
_symbols_total: Callable
_terminal_info: Callable
_version: Callable
error: Error
config: Config
def __init__(self):
self.config = Config()
async def __aenter__(self) -> 'MetaTrader':
"""
Async context manager entry point.
Initializes the connection to the MetaTrader terminal.
Returns:
MetaTrader: An instance of the MetaTrader class.
"""
await self.initialize(**Config().account_info())
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
"""
Async context manager exit point. Closes the connection to the MetaTrader terminal.
"""
await self.shutdown()
async def login(self, login: int, password: str, server: str, timeout: int = 60000) -> bool:
"""
Connects to the MetaTrader terminal using the specified login, password and server.
Args:
login (int): The trading account number.
password (str): The trading account password.
server (str): The trading server name.
timeout (int): The timeout for the connection in seconds.
Returns:
bool: True if successful, False otherwise.
"""
return await asyncio.to_thread(self._login, login, password=password, server=server, timeout=timeout)
async def initialize(self, path: str = "", login: int = 0, password: str = "", server: str = "",
timeout: int | None = None, portable=False) -> bool:
"""
Initializes the connection to the MetaTrader terminal. All parameters are optional.
Keyword Args:
path (str): The path to the MetaTrader terminal executable.
login (int): The trading account number.
password (str): The trading account password.
server (str): The trading server name.
timeout (int): The timeout for the connection in seconds.
portable (bool): If True, the terminal will be launched in portable mode.
Returns:
bool: True if successful, False otherwise.
"""
args = (str(path),) if path else ()
kwargs = {key: value for key, value in (('login', login), ('password', password), ('server', server),
('timeout', timeout), ('portable', portable)) if value}
return await asyncio.to_thread(self._initialize, *args, **kwargs)
async def shutdown(self) -> None:
"""
Closes the connection to the MetaTrader terminal.
Returns:
None: None
"""
return await asyncio.to_thread(self._shutdown)
async def last_error(self) -> tuple[int, str]:
try:
return await asyncio.to_thread(self._last_error)
except Exception as err:
logger.warning(f'Error in obtaining last error.')
return -1, str(err)
async def version(self) -> tuple[int, int, str] | None:
""""""
res = await asyncio.to_thread(self._version)
if res is None:
err = await self.last_error()
self.error = Error(*err)
logger.warning(f'Error in obtaining version information.{self.error.description}')
return res
async def account_info(self) -> AccountInfo | None:
""""""
res = await asyncio.to_thread(self._account_info)
if res is None:
err = await self.last_error()
self.error = Error(*err)
logger.warning(f'Error in obtaining account information.{self.error.description}')
return res
async def terminal_info(self) -> TerminalInfo | None:
res = await asyncio.to_thread(self._terminal_info)
if res is None:
err = await self.last_error()
self.error = Error(*err)
logger.warning(f'Error in obtaining terminal information.{self.error.description}')
return res
return res
async def symbols_total(self) -> int:
return await asyncio.to_thread(self._symbols_total)
async def symbols_get(self, group: str = "") -> tuple[SymbolInfo] | None:
kwargs = {'group': group} if group else {}
res = await asyncio.to_thread(self._symbols_get, **kwargs)
if res is None:
err = await self.last_error()
self.error = Error(*err)
logger.warning(f'Error in obtaining symbols.{self.error.description}')
return res
return res
async def symbol_info(self, symbol: str) -> SymbolInfo | None:
res = await asyncio.to_thread(self._symbol_info, symbol)
if res is None:
err = await self.last_error()
self.error = Error(*err)
logger.warning(f'Error in obtaining information for {symbol}.{self.error.description}')
return res
return res
async def symbol_info_tick(self, symbol: str) -> Tick | None:
res = await asyncio.to_thread(self._symbol_info_tick, symbol)
if res is None:
err = await self.last_error()
self.error = Error(*err)
logger.warning(f'Error in obtaining tick for {symbol}.{self.error.description}')
return res
return res
async def symbol_select(self, symbol: str, enable: bool) -> bool:
return await asyncio.to_thread(self._symbol_select, symbol, enable)
async def market_book_add(self, symbol: str) -> bool:
return await asyncio.to_thread(self._market_book_add, symbol)
async def market_book_get(self, symbol: str) -> tuple[BookInfo] | None:
res = await asyncio.to_thread(self._market_book_get, symbol)
if res is None:
err = await self.last_error()
self.error = Error(*err)
logger.warning(f'Error in obtaining market depth content for {symbol}.{self.error.description}')
return res
return res
async def market_book_release(self, symbol: str) -> bool:
return await asyncio.to_thread(self._market_book_release, symbol)
async def copy_rates_from(self, symbol: str, timeframe: TimeFrame, date_from: datetime | float, count: int):
res = await asyncio.to_thread(self._copy_rates_from, symbol, timeframe, date_from, count)
if res is None:
err = await self.last_error()
self.error = Error(*err)
logger.warning(f'Error in obtaining rates for {symbol}.{self.error.description}')
return res
return res
async def copy_rates_from_pos(self, symbol: str, timeframe: TimeFrame, start_pos: int, count: int):
res = await asyncio.to_thread(self._copy_rates_from_pos, symbol, timeframe, start_pos, count)
if res is None:
err = await self.last_error()
self.error = Error(*err)
logger.warning(f'Error in obtaining rates for {symbol}.{self.error.description}')
return res
return res
async def copy_rates_range(self, symbol: str, timeframe: TimeFrame, date_from: datetime | float,
date_to: datetime | float):
res = await asyncio.to_thread(self._copy_rates_range, symbol, timeframe, date_from, date_to)
if res is None:
err = await self.last_error()
self.error = Error(*err)
logger.warning(f'Error in obtaining rates for {symbol}.{self.error.description}')
return res
return res
async def copy_ticks_from(self, symbol: str, date_from: datetime | float, count: int, flags: CopyTicks):
res = await asyncio.to_thread(self._copy_ticks_from, symbol, date_from, count, flags)
if res is None:
err = await self.last_error()
self.error = Error(*err)
logger.warning(f'Error in obtaining ticks for {symbol}.{self.error.description}')
return res
return res
async def copy_ticks_range(self, symbol: str, date_from: datetime | float, date_to: datetime | float,
flags: CopyTicks):
res = await asyncio.to_thread(self._copy_ticks_range, symbol, date_from, date_to, flags)
if res is None:
err = await self.last_error()
self.error = Error(*err)
logger.warning(f'Error in obtaining ticks for {symbol}.{self.error.description}')
return res
return res
async def orders_total(self) -> int:
return await asyncio.to_thread(self._orders_total)
async def orders_get(self, group: str = "", ticket: int = 0, symbol: str = "") -> tuple[TradeOrder] | None:
"""Get active orders with the ability to filter by symbol or ticket. There are three call options.
Call without parameters. Return active orders on all symbols
Keyword Args:
symbol (str): Symbol name. Optional named parameter. If a symbol is specified, the ticket parameter is ignored.
group (str): The filter for arranging a group of necessary symbols. Optional named parameter. If the group is specified, the function
returns only active orders meeting a specified criteria for a symbol name.
ticket (int): Order ticket (ORDER_TICKET). Optional named parameter.
Returns:
tuple[TradeOrder]: A list of active trade orders as TradeOrder objects
"""
kwargs = {key: value for key, value in (('group', group), ('ticket', ticket), ('symbol', symbol)) if value}
res = await asyncio.to_thread(self._orders_get, **kwargs)
if res is None:
err = await self.last_error()
self.error = Error(*err)
logger.warning(f'Error in obtaining orders.{self.error.description}')
return res
return res
async def order_calc_margin(self, action: OrderType, symbol: str, volume: float, price: float) -> float | None:
res = await asyncio.to_thread(self._order_calc_margin, action, symbol, volume, price)
if res is None:
err = await self.last_error()
self.error = Error(*err)
logger.warning(f'Error in calculating margin.{self.error.description}')
return res
return res
async def order_calc_profit(self, action: OrderType, symbol: str, volume: float, price_open: float,
price_close: float) -> float | None:
res = await asyncio.to_thread(self._order_calc_profit, action, symbol, volume, price_open, price_close)
if res is None:
err = await self.last_error()
self.error = Error(*err)
logger.warning(f'Error in calculating profit.{self.error.description}')
return res
return res
async def order_check(self, request: dict) -> OrderCheckResult:
return await asyncio.to_thread(self._order_check, request)
async def order_send(self, request: dict) -> OrderSendResult:
return await asyncio.to_thread(self._order_send, request)
async def positions_total(self) -> int:
return await asyncio.to_thread(self._positions_total)
async def positions_get(self, group: str = "", ticket: int = None, symbol: str = "") -> tuple[TradePosition] | None:
kwargs = {key: value for key, value in (('group', group), ('ticket', ticket), ('symbol', symbol)) if value}
res = await asyncio.to_thread(self._positions_get, **kwargs)
if res is None:
err = await self.last_error()
self.error = Error(*err)
logger.warning(f'Error in obtaining open positions.{self.error.description}')
return res
return res
async def history_orders_total(self, date_from: datetime | float, date_to: datetime | float) -> int:
return await asyncio.to_thread(self._history_orders_total, date_from, date_to)
async def history_orders_get(self, date_from: datetime | float = None, date_to: datetime | float = None,
group: str = '', ticket: int = None, position: int = None) -> tuple[TradeOrder] | None:
kwargs = {key: value for key, value in (('group', group), ('ticket', ticket), ('position', position)) if value}
args = tuple(arg for arg in (date_from, date_to) if arg)
res = await asyncio.to_thread(self._history_orders_get, *args, **kwargs)
if res is None:
err = await self.last_error()
self.error = Error(*err)
logger.warning(f'Error in getting orders.{self.error.description}')
return res
return res
async def history_deals_total(self, date_from: datetime | float, date_to: datetime | float) -> int:
return await asyncio.to_thread(self._history_deals_total, date_from, date_to)
async def history_deals_get(self, date_from: datetime | float = None, date_to: datetime | float = None,
group: str = '', ticket: int = None, position: int = None) -> tuple[TradeDeal] | None:
kwargs = {key: value for key, value in (('group', group), ('ticket', ticket), ('position', position)) if value}
args = tuple(arg for arg in (date_from, date_to) if arg)
res = await asyncio.to_thread(self._history_deals_get, *args, **kwargs)
if res is None:
err = await self.last_error()
self.error = Error(*err)
logger.warning(f'Error in getting deals.{self.error}')
return res
return res
+617
View File
@@ -0,0 +1,617 @@
import MetaTrader5 as mt5
from .constants import BookType, TradeAction, OrderType, OrderTime, OrderFilling, PositionReason, DealType, DealEntry, \
DealReason, SymbolChartMode, SymbolTradeMode, SymbolCalcMode, SymbolOptionMode, SymbolOrderGTCMode, \
SymbolOptionRight, \
SymbolTradeExecution, SymbolSwapMode, DayOfWeek, AccountTradeMode, AccountStopOutMode, AccountMarginMode, \
OrderReason
from .base import Base
"""
This module contains data models used in this library.
They are used as base classes to other classes having the same properties but with more methods.
"""
class AccountInfo(Base):
"""Account Information Class.
Attributes:
login: int
password: str
server: str
trade_mode: AccountTradeMode
balance: float
leverage: float
profit: float
point: float
amount: float
equity: float
credit: float
margin: float
margin_level: float
margin_free: float
margin_mode: AccountMarginMode
margin_so_mode: AccountStopoutMode
margin_so_call: float
margin_so_so: float
margin_initial: float
margin_maintenance: float
fifo_close: bool
limit_orders: float
currency: str = "USD"
trade_allowed: bool = True
trade_expert: bool = True
currency_digits: int
assets: float
liabilities: float
commission_blocked: float
name: str
company: str
"""
login: int = 0
password: str = ''
server: str = ''
trade_mode: AccountTradeMode
balance: float
leverage: float
profit: float
point: float
amount: float = 0
equity: float
credit: float
margin: float
margin_level: float
margin_free: float
margin_mode: AccountMarginMode
margin_so_mode: AccountStopOutMode
margin_so_call: float
margin_so_so: float
margin_initial: float
margin_maintenance: float
fifo_close: bool
limit_orders: float
currency: str = "USD"
trade_allowed: bool = True
trade_expert: bool = True
currency_digits: int
assets: float
liabilities: float
commission_blocked: float
name: str
company: str
class TerminalInfo(Base):
"""Terminal information class. Holds information about the terminal.
Attributes:
community_account: bool
community_connection: bool
connected: bool
dlls_allowed: bool
trade_allowed: bool
tradeapi_disabled: bool
email_enabled: bool
ftp_enabled: bool
notifications_enabled: bool
mqid: bool
build: int
maxbars: int
codepage: int
ping_last: int
community_balance: float
retransmission: float
company: str
name: str
language: str
path: str
data_path: str
commondata_path: str
"""
community_account: bool
community_connection: bool
connected: bool
dlls_allowed: bool
trade_allowed: bool
tradeapi_disabled: bool
email_enabled: bool
ftp_enabled: bool
notifications_enabled: bool
mqid: bool
build: int
maxbars: int
codepage: int
ping_last: int
community_balance: float
retransmission: float
company: str
name: str
language: str
path: str
data_path: str
commondata_path: str
class SymbolInfo(Base):
"""Symbol Information Class. Symbols are financial instruments available for trading in the MetaTrader 5 terminal.
Attributes:
name: str
custom: bool
chart_mode: SymbolChartMode
select: bool
visible: bool
session_deals: int
session_buy_orders: int
session_sell_orders: int
volume: float
volumehigh: float
volumelow: float
time: int
digits: int
spread: float
spread_float: bool
ticks_bookdepth: int
trade_calc_mode: SymbolCalcMode
trade_mode: SymbolTradeMode
start_time: int
expiration_time: int
trade_stops_level: int
trade_freeze_level: int
trade_exemode: SymbolTradeExecution
swap_mode: SymbolSwapMode
swap_rollover3days: DayOfWeek
margin_hedged_use_leg: bool
expiration_mode: int
filling_mode: int
order_mode: int
order_gtc_mode: SymbolOrderGTCMode
option_mode: SymbolOptionMode
option_right: SymbolOptionRight
bid: float
bidhigh: float
bidlow: float
ask: float
askhigh: float
asklow: float
last: float
lasthigh: float
lastlow: float
volume_real: float
volumehigh_real: float
volumelow_real: float
option_strike: float
point: float
trade_tick_value: float
trade_tick_value_profit: float
trade_tick_value_loss: float
trade_tick_size: float
trade_contract_size: float
trade_accrued_interest: float
trade_face_value: float
trade_liquidity_rate: float
volume_min: float
volume_max: float
volume_step: float
volume_limit: float
swap_long: float
swap_short: float
margin_initial: float
margin_maintenance: float
session_volume: float
session_turnover: float
session_interest: float
session_buy_orders_volume: float
session_sell_orders_volume: float
session_open: float
session_close: float
session_aw: float
session_price_settlement: float
session_price_limit_min: float
session_price_limit_max: float
margin_hedged: float
price_change: float
price_volatility: float
price_theoretical: float
price_greeks_delta: float
price_greeks_theta: float
price_greeks_gamma: float
price_greeks_vega: float
price_greeks_rho: float
price_greeks_omega: float
price_sensitivity: float
basis: str
category: str
currency_base: str
currency_profit: str
currency_margin: Any
bank: str
description: str
exchange: str
formula: Any
isin: Any
name: str
page: str
path: str
"""
custom: bool
chart_mode: SymbolChartMode
select: bool
visible: bool
session_deals: int
session_buy_orders: int
session_sell_orders: int
volume: float
volumehigh: float
volumelow: float
time: int
digits: int
spread: float
spread_float: bool
ticks_bookdepth: int
trade_calc_mode: SymbolCalcMode
trade_mode: SymbolTradeMode
start_time: int
expiration_time: int
trade_stops_level: int
trade_freeze_level: int
trade_exemode: SymbolTradeExecution
swap_mode: SymbolSwapMode
swap_rollover3days: DayOfWeek
margin_hedged_use_leg: bool
expiration_mode: int
filling_mode: int
order_mode: int
order_gtc_mode: SymbolOrderGTCMode
option_mode: SymbolOptionMode
option_right: SymbolOptionRight
bid: float
bidhigh: float
bidlow: float
ask: float
askhigh: float
asklow: float
last: float
lasthigh: float
lastlow: float
volume_real: float
volumehigh_real: float
volumelow_real: float
option_strike: float
point: float
trade_tick_value: float
trade_tick_value_profit: float
trade_tick_value_loss: float
trade_tick_size: float
trade_contract_size: float
trade_accrued_interest: float
trade_face_value: float
trade_liquidity_rate: float
volume_min: float
volume_max: float
volume_step: float
volume_limit: float
swap_long: float
swap_short: float
margin_initial: float
margin_maintenance: float
session_volume: float
session_turnover: float
session_interest: float
session_buy_orders_volume: float
session_sell_orders_volume: float
session_open: float
session_close: float
session_aw: float
session_price_settlement: float
session_price_limit_min: float
session_price_limit_max: float
margin_hedged: float
price_change: float
price_volatility: float
price_theoretical: float
price_greeks_delta: float
price_greeks_theta: float
price_greeks_gamma: float
price_greeks_vega: float
price_greeks_rho: float
price_greeks_omega: float
price_sensitivity: float
basis: str
category: str
currency_base: str
currency_profit: str
currency_margin: str
bank: str
description: str
exchange: str
formula: str
isin: str
name: str
page: str
path: str
def __init__(self, **kwargs):
if 'name' not in kwargs:
raise AttributeError('Symbol Object Must be initialized with a name')
self.name = kwargs.pop('name')
super().__init__(**kwargs)
def __repr__(self):
return '%(class)s(name=%(name)s)' % {'class': self.__class__.__name__, 'name': self.name}
def __str__(self):
return self.name
def __eq__(self, other: "SymbolInfo"):
return self.name == other.name
def __hash__(self):
return hash(self.name)
class BookInfo(Base):
"""Book Information Class.
Attributes:
type: BookType
price: float
volume: float
volume_dbl: float
"""
type: BookType
price: float
volume: float
volume_dbl: float
class TradeOrder(Base):
"""Trade Order Class.
Attributes:
ticket: int
time_setup: int
time_setup_msc: int
time_expiration: int
time_done: int
time_done_msc: int
type: OrderType
type_time: OrderTime
type_filling: OrderFilling
state: int
magic: int
position_id: int
position_by_id: int
reason: OrderReason
volume_current: float
volume_initial: float
price_open: float
sl: float
tp: float
price_current: float
price_stoplimit: float
symbol: str
comment: str
external_id: str
"""
ticket: int
time_setup: int
time_setup_msc: int
time_expiration: int
time_done: int
time_done_msc: int
type: OrderType
type_time: OrderTime
type_filling: OrderFilling
state: int
magic: int
position_id: int
position_by_id: int
reason: OrderReason
volume_current: float
volume_initial: float
price_open: float
sl: float
tp: float
price_current: float
price_stoplimit: float
symbol: str
comment: str
external_id: str
class TradeRequest(Base):
"""Trade Request Class.
Attributes:
action: TradeAction
type: OrderType
order: int
symbol: str
volume: float
sl: float
tp: float
price: float
deviation: float
stop_limit: float
type_time: OrderTime
type_filling: OrderFilling
expiration: int
position: int
position_by: int
comment: str
magic: int
deviation: int
comment: str
"""
action: TradeAction
type: OrderType
order: int
symbol: str
volume: float
sl: float
tp: float
price: float
deviation: float
stop_limit: float
type_time: OrderTime
type_filling: OrderFilling
expiration: int
position: int
position_by: int
comment: str
magic: int
deviation: int
comment: str
class OrderCheckResult(Base):
"""Order Check Result
Attributes:
retcode: int
balance: float
equity: float
profit: float
margin: float
margin_free: float
margin_level: float
comment: str
request: TradeRequest
"""
retcode: int
balance: float
equity: float
profit: float
margin: float
margin_free: float
margin_level: float
comment: str
request: mt5.TradeRequest
class OrderSendResult(Base):
"""Order Send Result
Attributes:
retcode: int
deal: int
order: int
volume: float
price: float
bid: float
ask: float
profit: float
loss: float
comment: str
request: TradeRequest
request_id: int
retcode_external: int
"""
retcode: int
deal: int
order: int
volume: float
price: float
bid: float
ask: float
comment: str
request: mt5.TradeRequest
request_id: int
retcode_external: int
profit: float = None
loss: float = None
class TradePosition(Base):
"""Trade Position
Attributes:
ticket: int
time: int
time_msc: int
time_update: int
time_update_msc: int
type: OrderType
magic: float
identifier: int
reason: PositionReason
volume: float
price_open: float
sl: float
tp: float
price_current: float
swap: float
profit: float
symbol: str
comment: str
external_id: str
"""
ticket: int
time: int
time_msc: int
time_update: int
time_update_msc: int
type: OrderType
magic: float
identifier: int
reason: PositionReason
volume: float
price_open: float
sl: float
tp: float
price_current: float
swap: float
profit: float
symbol: str
comment: str
external_id: str
class TradeDeal(Base):
"""Trade Deal
Attributes:
ticket: int
order: int
time: int
time_msc: int
type: DealType
entry: DealEntry
magic: int
position_id: int
reason: DealReason
volume: float
price: float
commission: float
swap: float
profit: float
fee: float
sl: float
tp: float
symbol: str
comment: str
external_id: str
"""
ticket: int
order: int
time: int
time_msc: int
type: DealType
entry: DealEntry
magic: int
position_id: int
reason: DealReason
volume: float
price: float
commission: float
swap: float
profit: float
fee: float
sl: float
tp: float
symbol: str
comment: str
external_id: str
+49
View File
@@ -0,0 +1,49 @@
import asyncio
from typing import Coroutine, Callable, Awaitable
from logging import getLogger
logger = getLogger(__name__)
class QueueItem:
def __init__(self, task: Callable | Awaitable | Coroutine, *args, **kwargs):
self.task = task
self.args = args
self.kwargs = kwargs
async def run(self):
try:
if asyncio.iscoroutinefunction(self.task):
return await self.task(*self.args, **self.kwargs)
else:
return self.task(*self.args, **self.kwargs)
except Exception as err:
logger.error(f"Error in running {getattr(self.task, '__name__', str(self.task))}"
f" with {str(self.args)}, {self.kwargs}: {err}")
class TaskQueue:
def __init__(self):
self.queue = asyncio.Queue()
def add(self, item: QueueItem):
try:
self.queue.put_nowait(item)
except asyncio.QueueFull:
return
async def worker(self):
while True:
try:
item: QueueItem = self.queue.get_nowait()
await item.run()
self.queue.task_done()
except asyncio.QueueEmpty:
return
def add_task(self, item: Callable | Awaitable | Coroutine, *args, **kwargs):
self.add(QueueItem(item, *args, **kwargs))
asyncio.create_task(self.worker())
async def start(self):
await self.queue.join()
+90
View File
@@ -0,0 +1,90 @@
import asyncio
from concurrent.futures import ThreadPoolExecutor
from typing import Sequence, Coroutine, Callable
from logging import getLogger
from .strategy import Strategy
logger = getLogger(__name__)
class Executor:
"""Executor class for running multiple strategies on multiple symbols concurrently.
Attributes:
executor (ThreadPoolExecutor): The executor object.
workers (list): List of strategies.
coroutines (dict[Coroutine, dict]): A dictionary of coroutines to run in the executor
functions (dict[Callable, dict]): A dictionary of functions to run in the executor
"""
def __init__(self):
self.executor = ThreadPoolExecutor
self.workers: list[type(Strategy)] = []
self.coroutines: dict[Coroutine | Callable: dict] = {}
self.functions: dict[Callable: dict] = {}
def add_function(self, func: Callable, kwargs: dict):
self.functions[func] = kwargs
def add_coroutine(self, coro: Coroutine, kwargs: dict):
self.coroutines[coro] = kwargs
def add_workers(self, strategies: Sequence[type(Strategy)]):
"""Add multiple strategies at once
Args:
strategies (Sequence[Strategy]): A sequence of strategies.
"""
self.workers.extend(strategies)
def remove_workers(self, *, symbols: set):
"""Removes any worker running on a symbol not successfully initialized."""
self.workers = [worker for worker in self.workers if worker.symbol in symbols]
def add_worker(self, strategy: type(Strategy)):
"""Add a strategy instance to the list of workers
Args:
strategy (Strategy): A strategy object
"""
self.workers.append(strategy)
@staticmethod
def trade(strategy: type(Strategy)):
"""Wraps the coroutine trade method of each strategy with 'asyncio.run'.
Args:
strategy (Strategy): A strategy object
"""
asyncio.run(strategy.trade())
def run(self, func, kwargs: dict):
"""
Run a coroutine function
Args:
func: The coroutine. A variadic function.
kwargs: A dictionary of keyword arguments for the function
"""
try:
asyncio.run(func(**kwargs))
except Exception as err:
logger.error(f'Error: {err}. Unable to run function')
async def execute(self, workers: int = 5):
"""Run the strategies with a threadpool executor.
Args:
workers: Number of workers to use in executor pool. Defaults to 5.
Notes:
No matter the number specified, the executor will always use a minimum of 5 workers.
"""
workers_ = sum([len(self.workers), len(self.functions), len(self.coroutines)])
workers = max(workers, workers_)
loop = asyncio.get_running_loop()
with self.executor(max_workers=workers) as executor:
[loop.run_in_executor(executor, self.trade, worker) for worker in self.workers]
[loop.run_in_executor(executor, self.run, coro, kwargs) for coro, kwargs in self.coroutines.items()]
[loop.run_in_executor(executor, func, kwargs) for func, kwargs in self.functions.items()]
+229
View File
@@ -0,0 +1,229 @@
import asyncio
from datetime import datetime
from logging import getLogger
from pandas import DataFrame
import pandas as pd
from .core.config import Config
from .core.meta_trader import MetaTrader, CopyTicks, OrderType
from .core.models import TradeDeal, TradeOrder
logger = getLogger(__name__)
class History:
"""The history class handles completed trade deals and trade orders in the trading history of an account.
Attributes:
deals (list[TradeDeal]): Iterable of trade deals
orders (list[TradeOrder]): Iterable of trade orders
total_deals: Total number of deals
total_orders (int): Total number orders
group (str): Filter for selecting history by symbols.
ticket (int): Filter for selecting history by ticket number
position (int): Filter for selecting history deals by position
mt5 (MetaTrader): MetaTrader instance
config (Config): Config instance
"""
mt5: MetaTrader
config: Config
def __init__(self, *, date_from: datetime | int = None, date_to: datetime | int = None,
group: str = "", ticket: int = None, position: int = None):
"""
Args:
date_from (datetime, float): Date the orders are requested from. Set by the 'datetime' object or as a
number of seconds elapsed since 1970.01.01. Defaults to twenty-four hours from the current time in 'utc'
date_to (datetime, float): Date up to which the orders are requested. Set by the 'datetime' object or as a
number of seconds elapsed since 1970.01.01. Defaults to the current time in "utc"
group (str): Filter for selecting history by symbols.
ticket (int): Filter for selecting history by ticket number
position (int): Filter for selecting history deals by position
"""
self.config = Config()
self.mt5 = MetaTrader()
self.date_from = date_from
self.date_to = date_to
self.group = group
self.ticket = ticket
self.position = position
self.deals: list[TradeDeal] = []
self.orders: list[TradeOrder] = []
self.total_deals: int = 0
self.total_orders: int = 0
async def init(self, deals=True, orders=True):
"""Get history deals and orders
Keyword Args:
deals (bool): If true get history deals during initial request to terminal
orders (bool): If true get history orders during initial request to terminal
"""
self.deals = await self.get_deals() if deals else tuple()
self.orders = await self.get_orders() if orders else tuple()
self.total_deals = len(self.deals)
self.total_orders = len(self.orders)
async def get_deals(self, *, date_from: datetime | int = None, date_to: datetime | int = None, group: str = '',
retries: int = 3) -> tuple[TradeDeal, ...]:
"""Get deals from trading history using the parameters set in the constructor.
Returns:
tuple[TradeDeal]: A list of trade deals
"""
if retries < 1:
logger.warning(f'Failed to get deals: {self.mt5.error}')
return tuple()
date_from, date_to, group = date_from or self.date_from, date_to or self.date_to, group or self.group
deals = await self.mt5.history_deals_get(date_from=date_from, date_to=date_to, group=group)
if deals is not None:
return tuple(TradeDeal(**deal._asdict()) for deal in deals)
if self.mt5.error.is_connection_error():
await asyncio.sleep(retries)
return await self.get_deals(date_from=date_from, date_to=date_to, group=group, retries=retries-1)
logger.warning(f'Failed to get deals: {self.mt5.error}')
return tuple()
async def get_deals_ticket(self, *, ticket: int = None) -> tuple[TradeDeal, ...]:
"""Call specifying the order ticket. Return all deals having the specified order ticket in the DEAL_ORDER
property.
Args:
ticket (int): The order ticket
Returns:
tuple[TradeDeal]: A tuple of all deals with the order ticket
"""
ticket = ticket or self.ticket
assert ticket is not None, 'ticket not provided'
deals = await self.mt5.history_deals_get(ticket=ticket)
return tuple(sorted([TradeDeal(**deal._asdict()) for deal in deals or []], key=lambda x: x.time_msc))
async def get_deals_position(self, *, position: int = None) -> tuple[TradeDeal, ...]:
"""
Get all deals with the specified position ticket in the DEAL_POSITION_ID property
Args:
position (int): The position ticket
Returns:
tuple[TradeDeal]: A tuple of all deals with the position ticket
"""
position = position or self.position
assert position is not None, 'position not provided'
deals = await self.mt5.history_deals_get(position=position)
return tuple(sorted([TradeDeal(**deal._asdict()) for deal in deals or []], key=lambda x: x.time_msc))
async def deals_total(self, *, date_from: int | datetime = None, date_to: int | datetime = None) -> int:
"""Get total number of deals within the specified period in the constructor.
Args:
date_from (int|datetime): Date the orders are requested from. Set by the 'datetime' object or as a number of
seconds elapsed since 1970.01.01.
date_to (int|datetime): Date up to which the orders are requested. Set by the 'datetime' object or as a
number of seconds elapsed since 1970.01.01.
Returns:
int: Total number of Deals
"""
date_from, date_to = date_from or self.date_from, date_to or self.date_to
assert date_from is not None and date_to is not None, 'date_from and/or date_to not provided'
total_deals = await self.mt5.history_deals_total(date_from, date_to)
return total_deals
async def get_orders(self, *, date_from: datetime | int = None, date_to: datetime | int = None, group: str = '',
retries: int = 3) -> tuple[TradeOrder, ...]:
"""Get orders from trading history using the parameters set in the constructor or the method arguments.
Returns:
list[TradeOrder]: A list of trade orders
"""
if retries < 1:
logger.warning(f'Failed to get orders: {self.mt5.error}')
return tuple()
date_from, date_to, group = date_from or self.date_from, date_to or self.date_to, group or self.group
orders = await self.mt5.history_orders_get(date_from=date_from, date_to=date_to, group=group)
if orders is not None:
return tuple(TradeOrder(**order._asdict()) for order in orders)
if self.mt5.error.is_connection_error():
await asyncio.sleep(retries)
return await self.get_orders(date_from=date_from, date_to=date_to, group=group, retries=retries - 1)
logger.warning(f'Failed to get orders: {self.mt5.error}')
return tuple()
async def get_order_ticket(self, ticket: int | None = None) -> TradeOrder | None:
ticket = ticket or self.ticket
assert isinstance(ticket, int), 'ticket not provided'
orders = await self.mt5.history_orders_get(ticket=ticket)
if orders and (order := orders[0]).ticket == ticket:
return TradeOrder(**order._asdict())
return None
async def get_orders_position(self, position: int = None) -> tuple[TradeOrder, ...]:
"""
Call specifying the position ticket. Return all orders with a position ticket specified in the
ORDER_POSITION_ID property
Args:
position: The position ticket
Returns:
tuple[TradeOrder]: A tuple of all orders with the position ticket
"""
position = position or self.position
assert isinstance(position, int), 'position not provided'
orders = await self.mt5.history_orders_get(position=position)
return tuple(sorted([TradeOrder(**order._asdict()) for order in orders or []], key=lambda x: x.time_done_msc))
async def orders_total(self, date_from: int | datetime = None, date_to: int | datetime = None) -> int:
"""Get total number of orders within the specified period in the constructor.
Returns:
int: Total number of orders
"""
date_from, date_to = date_from or self.date_from, date_to or self.date_to
assert date_from is not None and date_to is not None, 'date_from and/or date_to not provided'
total_orders = await self.mt5.history_orders_total(date_from, date_to)
return total_orders
async def track_order(self, *, position: int = None, end_time: datetime = None) -> DataFrame:
"""
Track an order from the time it was opened to the time it was closed or any given time.
The tracking is done by getting the ticks
for the order symbol from the time the order was opened to the time it was closed. The profit for each tick is
calculated using the order type, symbol, initial volume, open price and the bid or ask price of the tick
depending on the order type.
Args:
end_time (datetime): The time to stop tracking the order. If not provided, the tracking will continue until
the order is closed.
position (int): The position ticket
end_time (int): The time to stop tracking the order in seconds. If not provided, the tracking will continue
until the order is closed.
Returns:
DataFrame: A pandas DataFrame of the ticks and profit for the order.
"""
orders = await self.get_orders_position(position=position)
deals = await self.get_deals_position(position=position)
open_order = orders[0]
open_deal = deals[0]
close_deal = deals[-1]
time_done = datetime.timestamp(end_time) if end_time is not None else close_deal.time
time_done_msc = int(time_done * 1000)
open_order.set_attributes(time_done_msc=time_done_msc, time_done=time_done, price_open=open_deal.price)
ticks = await self.mt5.copy_ticks_range(open_order.symbol, open_order.time_setup, open_order.time_done,
CopyTicks.ALL)
data = pd.DataFrame(ticks)
profit = lambda x: self.mt5._order_calc_profit(open_order.type, open_order.symbol, open_order.volume_initial,
open_order.price_open,
x.ask if open_order.type == OrderType.BUY else x.bid)
data['profits'] = data.apply(profit, axis=1)
data['time'] = pd.to_datetime(data['time'], unit='s')
data.set_index('time', inplace=True)
return data
+3
View File
@@ -0,0 +1,3 @@
from .strategies import *
from .traders import *
from .symbols import *
+2
View File
@@ -0,0 +1,2 @@
from .finger_trap import FingerTrap
from .tracker import Tracker
+111
View File
@@ -0,0 +1,111 @@
import asyncio
import logging
from .tracker import Tracker
from ..traders import SimpleTrader
from ...symbol import Symbol
from ...trader import Trader
from ...candle import Candles
from ...strategy import Strategy
from ...core import TimeFrame, OrderType
from ...sessions import Sessions
from ...utils import find_bearish_fractal, find_bullish_fractal
logger = logging.getLogger(__name__)
class FingerTrap(Strategy):
ttf: TimeFrame
etf: TimeFrame
fast_ema: int
slow_ema: int
entry_ema: int
parameters: dict
ecc: int
tcc: int
trader: Trader
tracker: Tracker
parameters = {"fast_ema": 8, "slow_ema": 20, "etf": TimeFrame.M5,
"ttf": TimeFrame.H1, "entry_ema": 5, "tcc": 672, "ecc": 3360}
def __init__(self, *, symbol: Symbol, params: dict | None = None, trader: Trader = None, sessions: Sessions = None,
name: str = 'FingerTrap'):
super().__init__(symbol=symbol, params=params, sessions=sessions, name=name)
self.trader = trader or SimpleTrader(symbol=self.symbol)
self.tracker: Tracker = Tracker(snooze=self.ttf.time)
async def check_trend(self):
try:
candles: Candles = await self.symbol.copy_rates_from_pos(timeframe=self.ttf, count=self.tcc)
if not ((current := candles[-1].time) >= self.tracker.trend_time):
self.tracker.update(new=False, order_type=None)
return
self.tracker.update(new=True, trend_time=current)
candles.ta.ema(length=self.slow_ema, append=True, fillna=0)
candles.ta.ema(length=self.fast_ema, append=True, fillna=0)
candles.rename(inplace=True, **{f"EMA_{self.fast_ema}": "fast", f"EMA_{self.slow_ema}": "slow"})
fas = candles.ta_lib.above(candles.fast, candles.slow)
fbs = candles.ta_lib.below(candles.fast, candles.slow)
caf = candles.ta_lib.above(candles.close, candles.fast)
cbf = candles.ta_lib.below(candles.close, candles.fast)
current = candles[-2]
if fas.iloc[-1] and caf.iloc[-1] and current.is_bullish():
self.tracker.update(trend="bullish")
elif fbs.iloc[-1] and cbf.iloc[-1] and current.is_bearish():
self.tracker.update(trend="bearish")
else:
self.tracker.update(trend="ranging", snooze=self.ttf.time, order_type=None)
except Exception as err:
logger.error(f"{err} for {self.symbol} in {self.__class__.__name__}.check_trend")
self.tracker.update(snooze=self.ttf.time, order_type=None)
async def confirm_trend(self):
try:
candles = await self.symbol.copy_rates_from_pos(timeframe=self.etf, count=self.ecc)
if not ((current := candles[-1].time) >= self.tracker.entry_time):
self.tracker.update(new=False, order_type=None)
return
self.tracker.update(new=True, entry_time=current)
candles.ta.ema(length=self.entry_ema, append=True)
candles.rename(**{f"EMA_{self.entry_ema}": "ema"})
candles['cae'] = candles.ta_lib.cross(candles.close, candles.ema)
candles['cbe'] = candles.ta_lib.cross(candles.close, candles.ema, above=False)
current = candles[-1]
if self.tracker.bullish and current.cae:
sl = find_bullish_fractal(candles).low
self.tracker.update(snooze=self.ttf.time, order_type=OrderType.BUY, sl=sl)
elif self.tracker.bearish and current.cbe:
sl = find_bearish_fractal(candles).high
self.tracker.update(snooze=self.ttf.time, order_type=OrderType.SELL, sl=sl)
else:
self.tracker.update(snooze=self.etf.time, order_type=None)
except Exception as err:
logger.error(f"{err} for {self.symbol} in {self.__class__.__name__}.confirm_trend")
self.tracker.update(snooze=self.etf.time, order_type=None)
async def watch_market(self):
await self.check_trend()
if not self.tracker.ranging:
await self.confirm_trend()
async def trade(self):
logger.info(f"Trading {self.symbol}")
async with self.sessions as sess:
await self.sleep(self.ttf.time)
while True:
await sess.check()
try:
await self.watch_market()
if not self.tracker.new:
await asyncio.sleep(2)
continue
if self.tracker.order_type is None:
await self.sleep(self.tracker.snooze)
continue
await self.trader.place_trade(order_type=self.tracker.order_type, parameters=self.parameters,
sl=self.tracker.sl)
await self.sleep(self.tracker.snooze)
except Exception as err:
logger.error(f"{err} For {self.symbol} in {self.__class__.__name__}.trade")
await self.sleep(self.ttf.time)
+37
View File
@@ -0,0 +1,37 @@
from dataclasses import dataclass
from typing import Literal
from ...core.constants import OrderType
@dataclass
class Tracker:
"""Keeps track of a strategy's data and state"""
trend: Literal["ranging", "bullish", "bearish"] = "ranging"
bullish: bool = False
bearish: bool = False
ranging: bool = True
snooze: float = 0
trend_time: float = 0
entry_time: float = 0
new: bool = True
order_type: OrderType = None
sl: float = 0
tp: float = 0
def update(self, **kwargs):
fields = self.__dict__
for key in kwargs:
if key in fields:
setattr(self, key, kwargs[key])
if 'trend' in kwargs:
match self.trend:
case "ranging":
self.ranging = True
self.bullish = self.bearish = False
case "bullish":
self.ranging = self.bearish = False
self.bullish = True
case "bearish":
self.ranging = self.bullish = False
self.bearish = True
+1
View File
@@ -0,0 +1 @@
from .forex_symbol import ForexSymbol
+83
View File
@@ -0,0 +1,83 @@
from ...symbol import Symbol
from ...core.exceptions import VolumeError
class ForexSymbol(Symbol):
"""Subclass of Symbol for Forex Symbols. Handles the conversion of currency and the computation of stop loss,
take profit and volume.
"""
def compute_points(self, *, amount: float, volume) -> float:
"""Compute the number of points required for a trade. Given the amount and the volume of the trade.
Args:
amount (float): Amount to trade
volume (float): Volume to trade
"""
points = amount / (volume * self.point * self.trade_contract_size)
return points
async def compute_volume_points(self, *, amount: float, points: float, use_limits=False, round_down: bool = False,
adjust: float = False) -> tuple[float, float]:
"""Compute the volume and points required for a trade. Given the amount and the number of points.
Args:
amount (float): Amount to trade
points (float): Number of points
round_down: round down the computed volume to the nearest step default True
adjust: Adjust the points if the computed volume is outside the range of permitted volumes
use_limits: Adjust the computed volume to the nearest permitted volume if the computed volume is outside
"""
amount = await self.check_amount(amount)
volume = amount / (self.point * points * self.trade_contract_size)
volume = self.round_off_volume(volume, round_down=round_down)
if (chk_vol := self.check_volume(volume))[0]:
if adjust:
points = self.compute_points(amount=amount, volume=volume)
return volume, points
if use_limits:
vol = chk_vol[1]
if adjust:
points = self.compute_points(amount=amount, volume=vol)
return vol, points
raise VolumeError(f"Incorrect Volume. Computed Volume outside the range of permitted volumes")
async def compute_volume_sl(self, *, amount: float, price: float, sl: float, use_limits=False, adjust: bool = False,
round_down: bool = False) -> tuple[float, float]:
amount = await self.check_amount(amount)
volume = amount / ((price - sl) * self.trade_contract_size)
volume = self.round_off_volume(volume, round_down=round_down)
sign = volume / abs(volume) if volume else 1
if (chk_vol := self.check_volume(abs(volume)))[0]:
if adjust:
sl = price - (amount / (volume * self.trade_contract_size))
return abs(volume), sl
return abs(volume), sl
if use_limits:
vol = chk_vol[1] * sign
if adjust:
sl = price - (amount / (vol * self.trade_contract_size))
return abs(vol), sl
raise VolumeError(f"Incorrect Volume. Computed Volume outside the range of permitted volumes")
async def compute_volume(self, *, amount: float, points, use_limits=False, round_down=True) -> float:
"""Compute volume given an amount to risk and target points. Round the computed volume to the nearest step.
Args:
amount (float): Amount to risk. Given in terms of the account currency.
points (float): Target points.
use_limits (bool): If True, the computed volume checked against the maximum and minimum volume.
round_down: round down the computed volume to the nearest step default True
Returns:
float: volume
Raises:
VolumeError: If the computed volume is less than the minimum volume or greater than the maximum volume.
"""
amount = await self.check_amount(amount)
volume = amount / (self.point * points * self.trade_contract_size)
volume = self.round_off_volume(volume, round_down=round_down)
if self.check_volume(volume)[0]:
return volume
if use_limits:
return self.check_volume(volume)[1]
raise VolumeError(f"Incorrect Volume. Computed Volume outside the range of permitted volumes")
+1
View File
@@ -0,0 +1 @@
from .simple_trader import SimpleTrader
+47
View File
@@ -0,0 +1,47 @@
from logging import getLogger
from ..symbols import ForexSymbol
from ...ram import RAM
from ...core.models import OrderType
from ...trader import Trader
logger = getLogger(__name__)
class SimpleTrader(Trader):
"""A simple trader class"""
def __init__(self, *, symbol: ForexSymbol, ram: RAM = None):
"""Initializes the order object and RAM instance
Args:
symbol (Symbol): Financial instrument
ram (RAM): Risk Assessment and Management instance
"""
ram = ram or RAM(risk_to_reward=2)
super().__init__(symbol=symbol, ram=ram)
async def create_order(self, *, order_type: OrderType, sl: float):
amount = await self.ram.get_amount()
await self.symbol.info()
tick = await self.symbol.info_tick()
min_points = self.symbol.trade_stops_level + (self.symbol.spread * 1.5)
points = (tick.ask - sl) / self.symbol.point if order_type == OrderType.BUY else\
(abs(tick.bid - sl) / self.symbol.point)
points = max(points, min_points)
self.order.type = order_type
volume, points = await self.symbol.compute_volume_points(amount=amount, points=points)
self.order.volume = volume
self.order.comment = self.parameters.get('name', self.__class__.__name__)
tick = await self.symbol.info_tick()
self.set_trade_stop_levels(points=points, tick=tick)
async def place_trade(self, order_type: OrderType, sl: float, parameters: dict = None):
"""Places a trade based on the order_type."""
try:
self.parameters |= parameters or {}
await self.create_order(order_type=order_type, sl=sl)
if not await self.check_order():
return
await self.send_order()
except Exception as err:
logger.error(f"{err} in {self.__class__.__name__}.place_trade for {self.symbol.name}")
+146
View File
@@ -0,0 +1,146 @@
import asyncio
from logging import getLogger
from .core.models import TradeRequest, OrderSendResult, OrderCheckResult, TradeOrder
from .core.constants import TradeAction, OrderTime, OrderFilling
from .core.exceptions import OrderError
logger = getLogger(__name__)
class Order(TradeRequest):
"""Trade order related functions and properties. Subclass of TradeRequest."""
def __init__(self, **kwargs):
"""Initialize the order object with keyword arguments, symbol must be provided.
Provide default values for action, type_time and type_filling if not provided.
Args:
**kwargs: Keyword arguments must match the attributes of TradeRequest as well as the attributes of
Order class as specified in the annotations in the class definition.
Default Values:
action (TradeAction.DEAL): Trade action
type_time (OrderTime.DAY): Order time
type_filling (OrderFilling.FOK): Order filling
"""
if 'symbol' in kwargs:
kwargs['symbol'] = str(kwargs['symbol'])
self.action = kwargs.pop('action', TradeAction.DEAL)
self.type_time = kwargs.pop('type_time', OrderTime.DAY)
self.type_filling = kwargs.pop('type_filling', OrderFilling.FOK)
super().__init__(**kwargs)
async def orders_total(self):
"""Get the number of active orders.
Returns:
(int): total number of active orders
"""
return await self.mt5.orders_total()
async def get_order(self, *, ticket: int, retries: int = 3) -> TradeOrder | None:
"""
Get the order by ticket number.
Args:
ticket (int): Order ticket number
retries (int): Number of retries
Returns:
"""
if retries < 1:
return None
orders = await self.mt5.orders_get(ticket=ticket)
if orders and (order := orders[0]).ticket == ticket:
return TradeOrder(**order._asdict())
if self.mt5.error.is_connection_error():
await asyncio.sleep(retries)
return await self.get_order(ticket=ticket, retries=retries-1)
return None
async def get_orders(self, *, ticket: int = 0, symbol: str = '', group: str = '', retries=3)\
-> tuple[TradeOrder, ...]:
"""Get the list of active orders for the current symbol.
Keyword Args:
ticket (int): Order ticket number
symbol (str): Symbol name
group (str): Group name
Returns:
tuple[TradeOrder]: A Tuple of active trade orders as TradeOrder objects
"""
if retries < 1:
return tuple()
symbol = getattr(self, 'symbol', symbol)
orders = await self.mt5.orders_get(symbol=symbol, ticket=ticket, group=group)
if orders is not None:
orders = (TradeOrder(**order._asdict()) for order in orders)
return tuple(orders)
if self.mt5.error.is_connection_error():
await asyncio.sleep(retries)
return await self.get_orders(ticket=ticket, symbol=symbol, group=group, retries=retries-1)
return tuple()
async def check(self, **kwargs) -> OrderCheckResult:
"""Check funds sufficiency for performing a required trading operation and the possibility of executing it.
Returns:
OrderCheckResult: An OrderCheckResult object
Raises:
OrderError: If not successful
"""
req = self.dict | kwargs
res = await self.mt5.order_check(req)
if res is None:
raise OrderError(f'Failed to check order due to {self.mt5.error.description}')
return OrderCheckResult(**res._asdict())
async def send(self) -> OrderSendResult:
"""Send a request to perform a trading operation from the terminal to the trade server.
Returns:
OrderSendResult: An OrderSendResult object
Raises:
OrderError: If not successful
"""
res = await self.mt5.order_send(self.dict)
if res is None:
raise OrderError(f'Failed to send order {self.symbol} due to {self.mt5.error.description}')
res = OrderSendResult(**res._asdict())
try:
profit = await self.calc_profit()
loss = await self.calc_profit(tp=self.sl)
res.loss = loss
res.profit = profit
except Exception as exe:
logger.error(f'Failed to calculate profit and loss for this order due to {exe}')
return res
async def calc_margin(self) -> float:
"""Return the required margin in the account currency to perform a specified trading operation.
Returns:
float: Returns float value if successful
Raises:
OrderError: If not successful
"""
res = await self.mt5.order_calc_margin(self.type, self.symbol, self.volume, self.price)
if res is None:
raise OrderError(f'Failed to calculate margin for {self.symbol} due to {self.mt5.error.description}')
return res
async def calc_profit(self, **kwargs) -> float:
"""Return profit in the account currency for a specified trading operation.
Returns:
float: Returns float value if successful
Raises:
OrderError: If not successful
"""
args = self.get_dict(include={'tp', 'price', 'symbol', 'volume', 'type'})
args |= kwargs
res = await self.mt5.order_calc_profit(args['type'], args['symbol'], args['volume'], args['price'], args['tp'])
if res is None:
raise OrderError(f'Failed to calculate profit for {self.symbol} due to {self.mt5.error.description}')
return res
+126
View File
@@ -0,0 +1,126 @@
"""Handle Open positions."""
import asyncio
from logging import getLogger
from .core import MetaTrader, TradePosition, TradeAction, OrderType
from .order import Order
logger = getLogger(__name__)
class Positions:
"""Get Open Positions.
Attributes:
symbol (str): Financial instrument name.
group (str): The filter for arranging a group of necessary symbols. Optional named parameter.
If the group is specified, the function returns only positions meeting a specified criteria for a symbol.
ticket (int): Position ticket.
mt5 (MetaTrader): MetaTrader instance.
"""
mt5: MetaTrader
def __init__(self, *, symbol: str = "", group: str = "", ticket: int = 0):
"""Get Open Positions.
Keyword Args:
symbol (str): Financial instrument name.
group (str): The filter for arranging a group of necessary symbols. Optional named parameter. If the group
is specified, the function returns only positions meeting a specified criteria for a symbol name.
ticket (int): Position ticket
"""
self.mt5 = MetaTrader()
self.symbol = symbol
self.group = group
self.ticket = ticket
async def positions_total(self) -> int:
"""Get the number of open positions.
Returns:
int: Return total number of open positions
"""
return await self.mt5.positions_total()
async def positions_get(self, symbol: str = '', group: str = '', ticket: int = 0, retries=3) -> list[TradePosition]:
"""Get open positions with the ability to filter by symbol or ticket.
Keyword Args:
symbol (str): Financial instrument name.
group (str): The filter for arranging a group of necessary symbols. Optional named parameter. If the group
is specified, the function returns only positions meeting a specified criteria for a symbol name.
ticket (int): Position ticket
Returns:
list[TradePosition]: A list of open trade positions
"""
if retries < 1:
logger.warning(f'Failed to get positions for {symbol or self.symbol}. {self.mt5.error}')
return []
positions = await self.mt5.positions_get(group=group or self.group, symbol=symbol or self.symbol,
ticket=ticket or self.ticket)
if positions is not None:
return [TradePosition(**pos._asdict()) for pos in positions]
if self.mt5.error.is_connection_error():
await asyncio.sleep(retries)
return await self.positions_get(symbol, group, ticket, retries - 1)
logger.warning(f'Failed to get positions for {symbol or self.symbol}. {self.mt5.error}')
return []
async def position_get(self, *, ticket: int) -> TradePosition | None:
"""Get an open position by ticket.
Args:
ticket (int): Position ticket.
Returns:
TradePosition: Return an open position
"""
positions = await self.positions_get(ticket=ticket)
position = positions[0] if positions else None
if position is None or position.ticket != ticket:
logger.warning(f'Failed to get position for ticket {ticket}. {self.mt5.error}')
return None
return position
async def close(self, *, ticket: int, symbol: str, price: float, volume: float, order_type: OrderType):
"""Close an open position for the trading account using the ticket and other parameters.
Args:
ticket (int): Position ticket.
symbol (str): Financial instrument name.
price (float): Closing price.
volume (float): Volume to close.
order_type (OrderType): Order type.
"""
order = Order(action=TradeAction.DEAL, price=price, position=ticket, symbol=symbol, volume=volume,
type=order_type.opposite)
return await order.send()
async def close_by(self, pos: TradePosition):
"""Close an open position for the trading account."""
order = Order(position=pos.ticket, symbol=pos.symbol, volume=pos.volume, type=pos.type.opposite,
price=pos.price_current)
return await order.send()
async def close_position(self, *, position: TradePosition):
"""Close an open position for the trading account. Using a position object."""
order = Order(position=position.ticket, symbol=position.symbol, volume=position.volume,
type=position.type.opposite, price=position.price_current)
return await order.send()
async def close_all(self, symbol: str = '', group: str = '') -> int:
"""Close all open positions for the trading account. Specify a symbol or group to filter positions.
Keyword Args:
symbol (str): Financial instrument name.
group (str): The filter for specifying a group of symbols.
Returns:
int: Return number of positions closed.
"""
symbol = symbol or self.symbol
group = group or self.group
positions = [pos for pos in await self.positions_get(symbol=symbol, group=group)]
orders = [self.close_position(position=pos) for pos in positions]
results = await asyncio.gather(*[order for order in orders], return_exceptions=True)
return len([res for res in results if (res and res.retcode) == 10009])
+66
View File
@@ -0,0 +1,66 @@
"""Risk Assessment and Management"""
from .account import Account
from .positions import Positions
class RAM:
account: Account
risk_to_reward: float
risk: float
points: float
pips: float
min_amount: float = 0
max_amount: float = 0
risk_level: float = 50
loss_limit: int = 3
open_limit: int = 6
def __init__(self, *, risk_to_reward: float = 1, risk: float = 0.01, **kwargs):
"""Initialize Risk Assessment and Management with the provided keyword arguments.
Keyword Args:
risk_to_reward (float): Risk to reward ratio. Defaults to 1
risk (float): Percentage of account balance to risk per trade 0.01 # 1%
kwargs: extra keyword arguments are set as object attributes
"""
self.risk_to_reward = risk_to_reward
self.risk = risk
self.account = Account()
[setattr(self, key, value) for key, value in kwargs.items()]
async def get_amount(self) -> float:
"""Calculate the amount to risk per trade as a percentage of equity.
Returns:
float: Amount to risk per trade
"""
await self.account.refresh()
amount = self.account.margin_free * self.risk
if self.min_amount and self.max_amount:
return max(self.min_amount, min(self.max_amount, amount))
return amount
async def check_losing_positions(self, *, symbol: str = '') -> bool:
"""Check if the number of losing positions is greater than or equal the loss limit.
Args:
symbol (str): Symbol to check. Defaults to ''.
"""
positions = await Positions().positions_get(symbol=symbol)
loosing = [trade for trade in positions if trade.profit <= 0]
return len(loosing) >= self.loss_limit
async def check_open_positions(self, *, symbol: str = '') -> bool:
"""Check if the number of open positions is greater than or equal the loss limit.
Args:
symbol (str): Symbol to check. Defaults to ''.
"""
positions = await Positions().positions_get(symbol=symbol)
return len(positions) >= self.open_limit
async def check_risk_level(self) -> bool:
"""Check the risk level."""
await self.account.refresh()
risk_level = (1 - (self.account.margin_free / self.account.equity)) * 100
return risk_level >= self.risk_level
+115
View File
@@ -0,0 +1,115 @@
"""This module contains the Records class, which is used to read and update trade records from csv files."""
import asyncio
from pathlib import Path
import csv
import logging
from typing import Iterable
from .core import Config, MetaTrader
logger = logging.getLogger(__name__)
class Records:
"""This utility class read trade records from csv files, and update them based on their closing positions.
Attributes:
config: Config object
records_dir(Path): Absolute path to directory containing record of placed trades, If not given takes the default
from the config
"""
config: Config
mt5: MetaTrader
def __init__(self, records_dir: Path | str = ''):
"""Initialize the Records class. The main method of this class is update_records which you should call to update
all the records specified in the records_dir.
Keyword Args:
records_dir (Path): Absolute path to directory containing record of placed trades.
"""
self.config = Config()
self.mt5 = MetaTrader()
self.records_dir = records_dir or self.config.records_dir
async def get_records(self):
"""Get trade records from records_dir folder
Yields:
files: Trade record files
"""
for file in self.records_dir.iterdir():
if file.is_file() and file.name.endswith('.csv'):
yield file
async def read_update(self, file: Path):
"""Read and update trade records
Args:
file: Trade record file
"""
try:
fr = open(file, mode='r', newline='')
reader: Iterable[dict] | csv.DictReader = csv.DictReader(fr)
rows = [row for row in reader]
rows = await self.update_rows(rows)
fr.close()
fw = open(file, mode='w', newline='')
writer = csv.DictWriter(fw, fieldnames=reader.fieldnames, extrasaction='ignore', restval=None)
writer.writeheader()
writer.writerows(rows)
fw.close()
except Exception as err:
logger.error(f'Error: {err}. Unable to read and update trade records')
async def update_row(self, row: dict) -> dict:
"""Update a single row of entered trade in the csv file with the actual profit.
Args:
row: A dictionary from the dictionary writer object of the csv file.
Returns:
dict: A dictionary with the actual profit and win status.
"""
try:
order = int(row['order'])
deals = await self.mt5.history_deals_get(position=order)
if not deals or len(deals) <= 1:
return row
deals = [deal for deal in deals if (deal.order != deal.position_id and deal.position_id == order
and deal.entry == 1)]
deals.sort(key=lambda x: x.time_msc)
deal = deals[-1]
row.update(actual_profit=deal.profit, win=deal.profit > 0, closed=True)
return row
except Exception as err:
logging.error(f'Error: {err}. Unable to update trade record')
return row
async def update_rows(self, rows: list[dict]) -> list[dict]:
"""Update the rows of entered trades in the csv file with the actual profit.
Args:
rows: A list of dictionaries from the dictionary writer object of the csv file.
Returns:
list[dict]: A list of dictionaries with the actual profit and win status.
"""
closed, unclosed = [], []
for row in rows:
if (row.get('closed', 'FALSE')).title() == 'True':
closed.append(row)
else:
unclosed.append(row)
unclosed = await asyncio.gather(*[self.update_row(row) for row in unclosed])
return closed + list(unclosed)
async def update_records(self):
"""Update trade records in the records_dir folder."""
records = [self.read_update(record) async for record in self.get_records()]
await asyncio.gather(*records)
async def update_record(self, file: Path | str):
"""Update a single trade record file."""
await self.read_update(file)
+94
View File
@@ -0,0 +1,94 @@
import csv
import json
from logging import getLogger
from typing import Iterable, Literal
from .core import Config
from .core.models import OrderSendResult
logger = getLogger(__name__)
class Result:
"""A base class for handling trade results and strategy parameters for record keeping and reference purpose.
The data property must be implemented in the subclass
Attributes:
config (Config): The configuration object
name: Any desired name for the result file object
"""
config: Config
def __init__(self, result: OrderSendResult, parameters: dict = None, name: str = ''):
"""
Prepare result data
Args:
result:
parameters:
name:
"""
self.config = Config()
self.parameters = parameters or {}
self.result = result
self.name = name or parameters.get('name', 'Trades')
def get_data(self) -> dict:
res = self.result.get_dict(exclude={'retcode', 'comment', 'retcode_external', 'request_id', 'request'})
return self.parameters | res | {'actual_profit': 0, 'closed': False, 'win': False}
async def save(self, *, trade_record_mode: Literal['csv', 'json'] = None):
"""Record trade results as a csv or json file
Args:
trade_record_mode (Literal['csv'|'json']): Mode of saving trade records
"""
trade_record_mode = trade_record_mode or self.config.trade_record_mode
if trade_record_mode == 'csv':
await self.to_csv()
else:
await self.to_json()
async def to_csv(self):
"""Record trade results and associated parameters as a csv file
"""
try:
data = self.get_data()
file = self.config.records_dir / f"{self.name}.csv"
file.touch(exist_ok=True) if not file.exists() else ...
reader: Iterable[dict] = csv.DictReader(file.open('r', newline=''))
rows: list[dict] = []
headers = set()
[(rows.append(row), headers.update(row.keys())) for row in reader]
rows.append(data)
headers.update(data.keys())
writer = csv.DictWriter(file.open('w', newline=''), fieldnames=headers, restval=None,
extrasaction='ignore')
writer.writeheader()
writer.writerows(rows)
except Exception as err:
logger.error(f'Unable to save to csv: {err}')
@staticmethod
def serialize(value) -> str:
"""Serialize the trade records and strategy parameters
"""
try:
return str(value)
except (ValueError, TypeError) as _:
return ""
async def to_json(self):
"""Save trades and strategy parameters in a json file
"""
try:
file = self.config.records_dir / f"{self.name}.json"
data = self.get_data()
exists = file.touch(exist_ok=True) if not file.exists() else True
if not exists:
json.dump([], file.open('w'))
with file.open('r') as fh:
rows = json.load(fh)
rows.append(data)
with file.open('w') as fh:
json.dump(rows, fh, indent=2, skipkeys=True, default=self.serialize)
except Exception as err:
logger.error(f"Unable to save as json file: {err}")
+207
View File
@@ -0,0 +1,207 @@
import asyncio
from datetime import time, timedelta, datetime
from asyncio import sleep, iscoroutinefunction
from typing import Literal, Callable
from logging import getLogger
from .positions import Positions
logger = getLogger(__name__)
def delta(obj: time) -> timedelta:
"""Get the timedelta of a datetime.time object.
Args:
obj (datetime.time): A datetime.time object.
"""
return timedelta(hours=obj.hour, minutes=obj.minute, seconds=obj.second, microseconds=obj.microsecond)
class Session:
"""A session is a time period between two datetime.time objects specified in utc.
Attributes:
start (datetime.time): The start time of the session.
end (datetime.time): The end time of the session.
on_start (str): The action to take when the session starts. Default is None.
on_end (str): The action to take when the session ends. Default is None.
custom_start (Callable): A custom function to call when the session starts. Default is None.
custom_end (Callable): A custom function to call when the session ends. Default is None.
name (str): A name for the session. Default is a combination of start and en
"""
def __init__(self, *, start: int | time, end: int | time,
on_start: Literal['close_all', 'close_win', 'close_loss', 'custom_start'] = None,
on_end: Literal['close_all', 'close_win', 'close_loss', 'custom_end'] = None,
custom_start: Callable = None, custom_end: Callable = None, name: str = ''):
"""Create a session.
Keyword Args:
start (int | datetime.time): The start time of the session in UTC.
end (int | datetime.time): The end time of the session in UTC.
on_start (Literal['close_all', 'close_win', 'close_loss', 'custom_start']): The action to take when the
session starts. Default is None.
on_end (Literal['close_all', 'close_win', 'close_loss', 'custom_end']): The action to take when the session
ends. Default is None.
custom_start (Callable): A custom function to call when the session starts. Default is None.
custom_end (Callable): A custom function to call when the session ends. Default is None.
name (str): A name for the session. Default is a combination of start and end.
"""
self.start = start if isinstance(start, time) else time(hour=start)
self.end = end if isinstance(end, time) else time(hour=end)
self.on_start = on_start
self.on_end = on_end
self.custom_start = custom_start
self.custom_end = custom_end
self.name = name or f'{self.start} - {self.end}'
def __contains__(self, item: time):
if self.start > self.end:
m1 = time(hour=23, minute=59, second=59, microsecond=9999)
m2 = time(hour=0)
return self.start <= item <= m1 or m2 <= item < self.end
return self.start <= item < self.end
def __str__(self):
return f'{self.start}-->{self.name}-->{self.end}' if self.name else f'{self.start}-->{self.end}'
def __repr__(self):
return f'{self.start}-->{self.end}'
def __len__(self):
return (delta(self.start) - delta(self.end)).seconds
async def begin(self):
"""Call the action specified in on_start or custom_start."""
await self.action(self.on_start)
async def close(self):
"""Call the action specified in on_end or custom_end."""
await self.action(self.on_end)
async def action(self, action):
"""Used by begin and close to call the action specified.
Args:
action (Literal['close_all', 'close_win', 'close_loss', 'custom_start', 'custom_end']): The action to take.
"""
try:
position = Positions()
positions = await position.positions_get()
match action:
case 'close_all':
await asyncio.gather(*(position.close(price=pos.price_current, ticket=pos.ticket,
order_type=pos.type, volume=pos.volume,
symbol=pos.symbol) for pos in positions),
return_exceptions=True)
case 'close_win':
await asyncio.gather(
*(position.close(price=pos.price_current, ticket=pos.ticket, order_type=pos.type,
volume=pos.volume, symbol=pos.symbol) for pos in positions if pos.profit > 0),
return_exceptions=True)
case 'close_loss':
await asyncio.gather(
*(position.close(price=pos.price_current, ticket=pos.ticket, order_type=pos.type,
volume=pos.volume, symbol=pos.symbol) for pos in positions if
pos.profit < 0), return_exceptions=True)
case 'custom_end':
if iscoroutinefunction(self.custom_end):
await self.custom_end()
self.custom_end()
case 'custom_start':
if iscoroutinefunction(self.custom_start):
await self.custom_start()
self.custom_start()
case _:
pass
except Exception as exe:
logger.warning(f'Failed to call action {action} due to {exe}')
def until(self):
"""Get the seconds until the session starts from the current time in seconds."""
return (delta(self.start) - delta(datetime.utcnow().time())).seconds
class Sessions:
"""Sessions allow you to run code at specific times of the day. It is a collection of Session objects.
Sessions are sorted by start time. The sessions object is an asynchronous context manager.
Attributes:
sessions (list[Session]): A list of Session objects.
current_session (Session): The current session.
Methods:
find: Find a session that contains a datetime.time object.
find_next: Find the next session that contains a datetime.time object.
check: Check if the current session has started and if not, wait until it starts.
"""
def __init__(self, *sessions: Session):
self.sessions = list(sessions)
self.sessions.sort(key=lambda x: (x.start, x.end))
self.current_session = None
def find(self, obj: time) -> Session | None:
"""Find a session that contains a datetime.time object.
Args:
obj (datetime.time): A datetime.time object.
Returns:
Session | None: A Session object or None if not found.
"""
for session in self.sessions:
if obj in session:
return session
return None
def find_next(self, obj: time) -> Session:
"""Find the next session that contains a datetime.time object.
Args:
obj (datetime.time): A datetime.time object.
Returns:
Session: A Session object.
"""
for session in self.sessions:
if obj < session.start:
return session
return self.sessions[0]
def __contains__(self, item: time):
return True if self.find(item) is not None else False
async def __aenter__(self):
await self.check()
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
await self.current_session.close()
async def check(self):
"""Check if the current session has started and if not, wait until it starts."""
now = datetime.utcnow().time()
current_session = self.find(now)
if current_session:
if self.current_session:
if self.current_session == current_session:
return
await self.current_session.close()
self.current_session = current_session
await self.current_session.begin()
return
await self.current_session.close() if self.current_session else ...
current_session = self.find_next(now)
secs = current_session.until() + 10
logger.info(f'sleeping for {secs} seconds until next {current_session} session')
await sleep(secs)
self.current_session = current_session
await self.current_session.begin()
+82
View File
@@ -0,0 +1,82 @@
"""The base class for creating strategies."""
import asyncio
from time import time
from typing import TypeVar
from abc import ABC, abstractmethod
from datetime import time as dtime
from .core.meta_trader import MetaTrader
from .symbol import Symbol as _Symbol
from .core import Config
from .sessions import Sessions, Session
Symbol = TypeVar("Symbol", bound=_Symbol)
class Strategy(ABC):
"""The base class for creating strategies.
Attributes:
name (str): The name of the strategy.
symbol (Symbol): The Financial Instrument as a Symbol Object
parameters (Dict): A dictionary of parameters for the strategy.
sessions (Sessions): The sessions to use for the strategy.
Notes:
Define the name of a strategy as a class attribute. If not provided, the class name will be used as the name.
"""
name: str
symbol: Symbol
sessions: Sessions
mt5: MetaTrader
config: Config
parameters = {}
def __init__(self, *, symbol: Symbol, params: dict = None, sessions: Sessions = None, name=''):
"""Initiate the parameters dict and add name and symbol fields.
Use class name as strategy name if name is not provided
Args:
symbol (Symbol): The Financial instrument
params (Dict): Trading strategy parameters
"""
self.parameters = self.parameters | (params or {})
self.symbol = symbol
self.name = name or self.__class__.__name__
self.parameters["symbol"] = symbol.name
self.parameters["name"] = self.name
self.sessions = sessions or Sessions(Session(start=0, end=dtime(hour=23, minute=59, second=59)))
self.config = Config()
self.mt5 = MetaTrader()
def __repr__(self):
return f"{self.name}({self.symbol!r})"
def __getattr__(self, item):
if item in self.parameters:
return self.parameters[item]
raise AttributeError(f'{item} not an attribute of {self.name}')
def __setattr__(self, key, value):
if key in self.__dict__.get('parameters', {}):
self.parameters[key] = value
super().__setattr__(key, value)
@staticmethod
async def sleep(secs: float):
"""Sleep for the needed amount of seconds in between requests to the terminal.
computes the accurate amount of time needed to sleep ensuring that the next request is made at the start of
a new bar and making cooperative multitasking possible.
Args:
secs (float): The time in seconds. Usually the timeframe you are trading on.
"""
mod = time() % secs
secs = secs - mod if mod != 0 else mod
await asyncio.sleep(secs + 0.2)
@abstractmethod
async def trade(self):
"""Place trades using this method. This is the main method of the strategy.
It will be called by the strategy runner.
"""
+399
View File
@@ -0,0 +1,399 @@
"""Symbol class for handling a financial instrument."""
import asyncio
from datetime import datetime
from logging import getLogger
from .core.constants import TimeFrame, CopyTicks
from .core.models import SymbolInfo, BookInfo
from .ticks import Tick
from .account import Account
from .candle import Candles
from .ticks import Ticks
from .utils import round_off
logger = getLogger(__name__)
class Symbol(SymbolInfo):
"""Main class for handling a financial instrument. A subclass of SymbolInfo it has attributes and methods
for working with a financial instrument.
Attributes:
tick (Tick): Price tick object for instrument
account: An instance of the current trading account
Notes:
Full properties are on the SymbolInfo Object.
Make sure Symbol is always initialized with a name argument
"""
tick: Tick
account: Account
def __init__(self, **kwargs):
"""Initialize the Symbol object with the name of the financial instrument.
Args:
name (str): Name of the financial instrument
"""
super().__init__(**kwargs)
self.account = Account()
@property
def pip(self):
"""Returns the pip value of the symbol. This is ten times the point value for forex symbols.
Returns:
float: The pip value of the symbol.
"""
return self.point * 10
async def info_tick(self, *, name: str = "", retries=3) -> Tick:
"""Get the current price tick of a financial instrument.
Args:
name: if name is supplied get price tick of that financial instrument
Returns:
Tick: Return a Tick Object
Raises:
ValueError: If request was unsuccessful and None was returned
"""
if retries < 1:
raise ValueError(f'Could not get tick for {name or self.name}. {self.mt5.error}')
tick = await self.mt5.symbol_info_tick(name or self.name)
if tick is not None:
tick = Tick(**tick._asdict())
setattr(self, 'tick', tick) if not name else ...
return tick
if self.mt5.error.is_connection_error():
await asyncio.sleep(retries)
return await self.info_tick(name=name, retries=retries - 1)
raise ValueError(f'Could not get tick for {name or self.name}. {self.mt5.error}')
async def symbol_select(self, *, enable: bool = True) -> bool:
"""Select a symbol in the MarketWatch window or remove a symbol from the window.
Update the select property
Args:
enable (bool): Switch. Optional unnamed parameter. If 'false', a symbol should be removed from
the MarketWatch window.
Returns:
bool: True if successful, otherwise False.
"""
self.select = await self.mt5.symbol_select(self.name, enable)
return self.select
async def info(self, retries=3) -> SymbolInfo:
"""Get data on the specified financial instrument and update the symbol object properties
Returns:
(SymbolInfo): SymbolInfo if successful
Raises:
ValueError: If request was unsuccessful and None was returned
"""
if retries < 1:
raise ValueError(f'Could not get info for {self.name}. {self.mt5.error}')
info = await self.mt5.symbol_info(self.name)
if info:
info = info._asdict()
info['swap_rollover3days'] = info.get('swap_rollover3days', 0) % 7
self.set_attributes(**info)
return SymbolInfo(**info)
if self.mt5.error.is_connection_error():
await asyncio.sleep(retries)
return await self.info(retries=retries - 1)
raise ValueError(f'Could not get info for {self.name}. {self.mt5.error}')
async def init(self) -> bool:
"""Initialized the symbol by pulling properties from the terminal
Returns:
bool: Returns True if symbol info was successful initialized
"""
try:
if await self.symbol_select():
await self.book_add()
await self.info()
await self.info_tick()
return True
logger.warning(f'Unable to initialized symbol {self}')
return False
except Exception as err:
self.select = False
logger.warning(err)
return False
async def book_add(self) -> bool:
"""Subscribes the MetaTrader 5 terminal to the Market Depth change events for a specified symbol.
If the symbol is not in the list of instruments for the market, This method will return False
Returns:
bool: True if successful, otherwise False.
"""
return await self.mt5.market_book_add(self.name)
async def book_get(self, retries=3) -> tuple[BookInfo, ...]:
"""Returns a tuple of BookInfo featuring Market Depth entries for the specified symbol.
Returns:
tuple[BookInfo]: Returns the Market Depth contents as a tuples of BookInfo Objects
Raises:
ValueError: If request was unsuccessful and None was returned
"""
if retries < 1:
raise ValueError(f'Could not get book info for {self.name}. {self.mt5.error}')
infos = await self.mt5.market_book_get(self.name)
if infos is not None:
book_infos = (BookInfo(**info._asdict()) for info in infos)
return tuple(book_infos)
if self.mt5.error.is_connection_error():
await asyncio.sleep(retries)
return await self.book_get(retries=retries - 1)
raise ValueError(f'Could not get book info for {self.name}. {self.mt5.error}')
async def book_release(self) -> bool:
"""Cancels subscription of the MetaTrader 5 terminal to the Market Depth change events for a specified symbol.
Returns:
bool: True if successful, otherwise False.
"""
return await self.mt5.market_book_release(self.name)
def check_volume(self, volume) -> tuple[bool, float]:
"""Check if the volume is within the limits of the symbol. If not, return the nearest limit.
Args:
volume (float): Volume to check
Returns: tuple[bool, float]: Returns a tuple of a boolean and a float. The boolean indicates if the volume is
within the limits of the symbol. The float is the volume to use if the volume is not within the limits of the
symbol.
"""
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
def round_off_volume(self, volume: float, round_down: bool = False) -> float:
"""Round off the volume to the nearest volume step.
Args:
volume (float): Volume to round off
round_down (bool): If True, round down. If False, round up. Optional unnamed parameter. Defaults to True.
Returns:
float: Rounded off volume
"""
return round_off(value=volume, step=self.volume_step, round_down=round_down)
async def check_amount(self, amount: float) -> float:
if self.currency_profit != self.account.currency:
amount = await self.convert_currency(amount=amount, base=self.currency_profit, quote=self.account.currency)
return amount
async def compute_volume(self, *args, **kwargs) -> float:
"""Computes the volume required for a trade usually based on the amount and any other keyword arguments.
This is a dummy method that returns the minimum volume of the symbol. It is meant to be overridden by a subclass
that implements the computation of volume.
Keyword Args:
use_limits (bool): round up or round down the computed volume to the nearest volume limit i.e. volume_min
or volume_max
Returns:
float: Returns the volume of the trade
"""
return self.volume_min
async def convert_currency(self, *, amount: float, base: str, quote: str) -> float:
"""Convert from one currency to the other. Alias for currency_conversion"""
return await self.currency_conversion(amount=amount, base=base, quote=quote)
async def currency_conversion(self, *, amount: float, base: str, quote: str) -> float:
"""Convert from one currency to the other.
Args:
amount: amount to convert given in terms of the quote currency
base: The base currency of the pair
quote: The quote currency of the pair
Returns:
float: Amount in terms of the quote currency
Raises:
ValueError: If conversion is impossible
"""
try:
pair = f'{base}{quote}'
if self.account.has_symbol(pair):
tick = await self.info_tick(name=pair)
if tick is not None:
return amount / tick.ask
pair = f'{quote}{base}'
if self.account.has_symbol(pair):
tick = await self.info_tick(name=pair)
if tick is not None:
return amount * tick.bid
except Exception as err:
logger.warning(f'Currency conversion failed: Unable to convert {amount} in {quote} to {base}')
raise ValueError(f'Currency Conversion Failed: {err}')
else:
logger.warning(f'Currency conversion failed: Unable to convert {amount} in {quote} to {base}')
async def copy_rates_from(self, *, timeframe: TimeFrame,
date_from: datetime | int, count: int = 500, retries=3) -> Candles:
"""
Get bars from the MetaTrader 5 terminal starting from the specified date.
Args: timeframe (TimeFrame): Timeframe the bars are requested for. Set by a value from the TimeFrame
enumeration. Required unnamed parameter.
date_from (datetime | int): Date of opening of the first bar from the requested sample. Set by the
'datetime' object or as a number of seconds elapsed since 1970.01.01. Required unnamed parameter.
count (int): Number of bars to receive. Required unnamed parameter.
Returns:
Candles: Returns a Candles object as a collection of rates ordered chronologically
Raises:
ValueError: If request was unsuccessful and None was returned
"""
if retries < 1:
raise ValueError(f'Could not get rates for {self.name}. {self.mt5.error}')
rates = await self.mt5.copy_rates_from(self.name, timeframe, date_from, count)
if rates is not None:
return Candles(data=rates)
if self.mt5.error.is_connection_error():
await asyncio.sleep(retries)
return await self.copy_rates_from(timeframe=timeframe, date_from=date_from,
count=count, retries=retries - 1)
raise ValueError(f'Could not get rates for {self.name}. {self.mt5.error}')
async def copy_rates_from_pos(self, *, timeframe: TimeFrame, count: int = 500,
start_position: int = 0, retries=3) -> Candles:
"""Get bars from the MetaTrader 5 terminal starting from the specified index.
Args:
timeframe (TimeFrame): TimeFrame value from TimeFrame Enum. Required keyword only parameter
count (int): Number of bars to return. Keyword argument defaults to 500
start_position (int): Initial index of the bar the data are requested from. The numbering of bars goes from
present to past. Thus, the zero bar means the current one. Keyword argument defaults to 0.
Returns:
Candles: Returns a Candles object as a collection of rates ordered chronologically.
Raises:
ValueError: If request was unsuccessful and None was returned
"""
if retries < 1:
raise ValueError(f'Could not get rates for {self.name}. {self.mt5.error}')
rates = await self.mt5.copy_rates_from_pos(self.name, timeframe, start_position, count)
if rates is not None:
return Candles(data=rates)
if self.mt5.error.is_connection_error():
await asyncio.sleep(retries)
return await self.copy_rates_from_pos(timeframe=timeframe, count=count,
start_position=start_position, retries=retries - 1)
raise ValueError(f'Could not get rates for {self.name}. {self.mt5.error}')
async def copy_rates_range(self, *, timeframe: TimeFrame, date_from: datetime | int,
date_to: datetime | int, retries=3) -> Candles:
"""Get bars in the specified date range from the MetaTrader 5 terminal.
Args:
timeframe (TimeFrame): Timeframe for the bars using the TimeFrame enumeration. Required unnamed parameter.
date_from (datetime | int): Date the bars are requested from. Set by the 'datetime' object or as a number
of seconds elapsed since 1970.01.01. Bars with the open time >= date_from are returned. Required unnamed
parameter.
date_to (datetime | int): Date, up to which the bars are requested. Set by the 'datetime' object or as a
number of seconds elapsed since 1970.01.01. Bars with the open time <= date_to are returned.
Required unnamed parameter.
Returns:
Candles: Returns a Candles object as a collection of rates ordered chronologically.
Raises:
ValueError: If request was unsuccessful and None was returned
"""
if retries < 1:
raise ValueError(f'Could not get rates for {self.name}. {self.mt5.error}')
rates = await self.mt5.copy_rates_range(symbol=self.name, timeframe=timeframe, date_from=date_from,
date_to=date_to)
if rates is not None:
return Candles(data=rates)
if self.mt5.error.is_connection_error():
await asyncio.sleep(retries)
return await self.copy_rates_range(timeframe=timeframe, date_from=date_from,
date_to=date_to, retries=retries - 1)
raise ValueError(f'Could not get rates for {self.name}. {self.mt5.error}')
async def copy_ticks_from(self, *, date_from: datetime | int, count: int = 100,
flags: CopyTicks = CopyTicks.ALL, retries=3) -> Ticks:
"""
Get ticks from the MetaTrader 5 terminal starting from the specified date.
Args: date_from (datetime | int): Date the ticks are requested from. Set by the 'datetime' object or as a
number of seconds elapsed since 1970.01.01.
count (int): Number of requested ticks. Defaults to 100
flags (CopyTicks): A flag to define the type of the requested ticks from CopyTicks enum. INFO is the default
Returns:
Candles: Returns a Candles object as a collection of ticks ordered chronologically.
Raises:
ValueError: If request was unsuccessful and None was returned
"""
if retries < 1:
raise ValueError(f'Could not get ticks for {self.name}. {self.mt5.error}')
ticks = await self.mt5.copy_ticks_from(self.name, date_from, count, flags)
if ticks is not None:
return Ticks(data=ticks)
if self.mt5.error.is_connection_error():
await asyncio.sleep(retries)
return await self.copy_ticks_from(date_from=date_from, count=count, flags=flags, retries=retries - 1)
raise ValueError(f'Could not get ticks for {self.name}. {self.mt5.error}')
async def copy_ticks_range(self, *, date_from: datetime | int, date_to: datetime | int,
flags: CopyTicks = CopyTicks.ALL, retries=3) -> Ticks:
"""Get ticks for the specified date range from the MetaTrader 5 terminal.
Args:
date_from: Date the bars are requested from. Set by the 'datetime' object or as a number of seconds elapsed
since 1970.01.01. Bars with the open time >= date_from are returned. Required unnamed parameter.
date_to: Date, up to which the bars are requested. Set by the 'datetime' object or as a number of
seconds elapsed since 1970.01.01. Bars with the open time <= date_to are returned.
Required unnamed parameter.
flags (CopyTicks):
Returns:
Candles: Returns a Candles object as a collection of ticks ordered chronologically.
Raises:
ValueError: If request was unsuccessful and None was returned.
"""
if retries < 1:
raise ValueError(f'Could not get ticks for {self.name}. {self.mt5.error}')
ticks = await self.mt5.copy_ticks_range(self.name, date_from, date_to, flags)
if ticks is not None:
return Ticks(data=ticks)
if self.mt5.error.is_connection_error():
await asyncio.sleep(retries)
return await self.copy_ticks_range(date_from=date_from, date_to=date_to, flags=flags, retries=retries - 1)
raise ValueError(f'Could not get ticks for {self.name}. {self.mt5.error}')
+70
View File
@@ -0,0 +1,70 @@
"""Terminal related functions and properties"""
from typing import NamedTuple
from logging import getLogger
from .core.models import TerminalInfo
logger = getLogger(__name__)
class Terminal(TerminalInfo):
"""Terminal Class. Get information about the MetaTrader 5 terminal. The class is a subclass of the TerminalInfo
class. It inherits all the attributes and methods of the TerminalInfo class and adds some useful methods.
Notes:
Other attributes are defined in the TerminalInfo Class
"""
Version = NamedTuple("Version", (('version', str), ('build', int), ('release_date', str)))
async def initialize(self) -> bool:
"""Establish a connection with the MetaTrader 5 terminal. There are three call options. Call without parameters.
The terminal for connection is found automatically. Call specifying the path to the MetaTrader 5 terminal we
want to connect to. word path as a keyword argument Call specifying the trading account path and parameters
i.e. login, password, server, as keyword arguments, path can be omitted.
Returns:
bool: True if successful else False
"""
self.connected = await self.mt5.initialize(**self.config.account_info())
if not self.connected:
err = await self.mt5.last_error()
logger.critical(f'Failed to initialize Terminal. Error Code: {err}')
raise SystemExit
return self.connected
async def version(self):
"""Get the MetaTrader 5 terminal version. This method returns the terminal version, build and release date as
a tuple of three values
Returns:
Version: version of tuple as Version object
Raises:
ValueError: If the terminal version cannot be obtained
"""
res = await self.mt5.version()
if res is None:
raise ValueError('Failed to get terminal version')
return self.Version(*res)
async def info(self):
"""Get the connected MetaTrader 5 client terminal status and settings. gets terminal info in the form of a
named tuple structure (namedtuple). Return None in case of an error. The info on the error can be
obtained using last_error().
Returns:
Terminal: Terminal status and settings as a terminal object.
"""
info = await self.mt5.terminal_info()
self.set_attributes(**info._asdict())
return self
async def symbols_total(self) -> int:
"""Get the number of all financial instruments in the MetaTrader 5 terminal.
Returns:
int: Total number of available symbols
"""
return await self.mt5.symbols_total()
+212
View File
@@ -0,0 +1,212 @@
"""Module for working with price ticks."""
from typing import TypeVar, Iterable
from pandas import DataFrame, Series
import pandas_ta as ta
import mplfinance as mplt
import pandas as pd
from .core.constants import TickFlag
Self = TypeVar('Self', bound='Ticks')
class Tick:
"""Price Tick of a Financial Instrument.
Attributes:
time (int): Time of the last prices update for the symbol
bid (float): Current Bid price
ask (float): Current Ask price
last (float): Price of the last deal (Last)
volume (float): Volume for the current Last price
time_msc (int): Time of the last prices update for the symbol in milliseconds
flags (TickFlag): Tick flags
volume_real (float): Volume for the current Last price
Index (int): Custom attribute representing the position of the tick in a sequence.
"""
time: float
bid: float
ask: float
last: float
volume: float
time_msc: float
flags: float
volume_real: float
Index: int
def __init__(self, **kwargs):
"""Initialize the Tick class. Set attributes from keyword arguments. bid, ask, last, time and volume must be
present"""
if not all(key in kwargs for key in ['bid', 'ask', 'last', 'volume', 'time']):
raise ValueError("bid, ask, last and volume, time must be present in the keyword arguments")
self.Index = kwargs.pop('Index', 0)
self.set_attributes(**kwargs)
def __repr__(self):
return ("%(class)s(Index=%(Index)s, time=%(time)s, bid=%(bid)s, ask=%(ask)s, last=%(last)s, volume=%(volume)s)"
% {"class": self.__class__.__name__, "time": self.time, "bid": self.bid,
"ask": self.ask, "last": self.last, "volume": self.volume, 'Index': self.Index})
def dict(self, exclude: set = None, include: set = None) -> dict:
"""
Returns a dictionary of the instance attributes.
Args:
exclude: A set of attributes to exclude from the dictionary. Defaults to None.
include: A set of attributes to include in the dictionary. Defaults to None.
Returns: dict
"""
exclude = exclude or set()
include = include or set()
keys = include or set(self.__dict__.keys()).difference(exclude)
return {k: v for k, v in self.__dict__.items() if k in keys}
def set_attributes(self, **kwargs):
"""Set attributes from keyword arguments"""
for key, value in kwargs.items():
setattr(self, key, value)
_Ticks = TypeVar('_Ticks', bound='Ticks')
class Ticks:
"""Container class for price ticks. Arrange in chronological order. Supports iteration, slicing and assignment"""
time: Series
bid: Series
ask: Series
last: Series
volume: Series
time_msc: Series
flags: Series
volume_real: Series
Index: Series
def __init__(self, *, data: DataFrame | Iterable, flip=False):
"""Initialize the Ticks class. Creates a DataFrame of price ticks from the data argument.
Args:
data (DataFrame | Iterable): Dataframe of price ticks or any iterable object that can be converted to a
pandas DataFrame
flip (bool): If flip is True reverse data chronological order.
"""
if isinstance(data, DataFrame):
data = data
elif isinstance(data, type(self)):
data = DataFrame(data.data)
elif isinstance(data, Iterable):
data = DataFrame(data)
else:
raise ValueError(f'Cannot create DataFrame from object of {type(data)}')
self._data = data.iloc[::-1] if flip else data
def __repr__(self):
return self._data.__repr__()
def __len__(self):
return self._data.shape[0]
def __contains__(self, item: Tick) -> bool:
return item.time_msc == self[item.Index].time_msc
def __getattr__(self, item):
if item in list(self._data.columns.values):
return self._data[item]
raise AttributeError(f'Attribute {item} not defined on class {self.__class__.__name__}')
def __getitem__(self, index) -> Tick | Self:
if isinstance(index, slice):
cls = self.__class__
data = self._data.iloc[index]
data.reset_index(drop=True, inplace=True)
return cls(data=data)
if isinstance(index, str):
return self._data[index]
item = self._data.iloc[index]
return Tick(**item, Index=index)
def __setitem__(self, index, value: Series):
if isinstance(value, Series):
self._data[index] = value
return
raise TypeError(f'Expected Series got {type(value)}')
def __iter__(self):
return (Tick(**row._asdict()) for row in self._data.itertuples())
@property
def ta(self):
"""Access to the pandas_ta library for performing technical analysis on the underlying data attribute.
Returns:
pandas_ta: The pandas_ta library
"""
return self._data.ta
@property
def ta_lib(self):
"""Access to the ta library for performing technical analysis. Not dependent on the underlying data attribute.
Returns:
ta: The ta library
"""
return ta
@property
def data(self) -> DataFrame:
"""DataFrame of price ticks arranged in chronological order."""
return self._data
def rename(self, inplace=True, **kwargs) -> _Ticks | None:
"""Rename columns of the candle class.
Keyword Args:
inplace (bool): Rename the columns inplace or return a new instance of the class with the renamed columns
**kwargs: The new names of the columns
Returns:
Ticks: A new instance of the class with the renamed columns if inplace is False.
None: If inplace is True
"""
res = self._data.rename(columns=kwargs, inplace=inplace)
return res if inplace else self.__class__(data=res)
def make_addplot(self, *, count: int = 50, columns: list = None, **kwargs) -> dict:
"""
Make subplots for adding to the main plot
Args:
count (int): The numbers of candles to make the addplot for. Defaults to 50.
columns (list[str]): The columns to make the plot from. Defaults to None.
**kwargs: Valid arguments for the mplfinance make_addplot function
"""
columns = columns or []
data = self._data[-count:]
data.index = pd.to_datetime(data['time'], unit='s')
return mplt.make_addplot(data[columns], **kwargs)
def visualize(self, *, count: int = 50, type='candle', savefig: str | dict = None, addplot: dict = None,
style: str = 'charles', ylabel: str = 'Price', title: str = 'Chart', **kwargs):
"""Visualize the candles using the mplfinance library.
Args:
count (int): The number of candles to visualize, counting from behind, i.e the most recent candles.
Defaults to 50.
type: Type of chart, defaults to candle
savefig (str|dict): The path to save the figure or a dictionary of parameters to pass to the savefig method.
addplot: Additional plots to add to the chart. Defaults to None. They should match the dimension of the
original data which is specified via the count parameter.
style (str): The style of the chart. Defaults to 'charles'.
ylabel (str): The label of the y-axis. Defaults to 'Price'.
title (str): The title of the chart. Defaults to 'Chart'.
kwargs: valid kwargs for the plot function.
"""
kwargs |= {key: arg for key, arg in (('savefig', savefig), ('addplot', addplot), ('style', style),
('ylabel', ylabel), ('title', title), ('type', type)) if arg}
data = self._data[-count:]
data.index = pd.to_datetime(data['time'], unit='s')
mplt.plot(data, **kwargs)
+154
View File
@@ -0,0 +1,154 @@
"""This module contains the Records class, which is used to read and update trade records from csv files."""
import asyncio
import json
from pathlib import Path
import csv
import logging
from typing import Iterable
from .core import Config, MetaTrader
logger = logging.getLogger(__name__)
class TradeRecords:
"""This utility class read trade records from csv files, and update them based on their closing positions.
Attributes:
config: Config object
records_dir(Path): Absolute path to directory containing record of placed trades, If not given takes the default
from the config
"""
config: Config
mt5: MetaTrader
def __init__(self, *, records_dir: Path | str = ''):
"""Initialize the Records class. The main method of this class is update_records which you should call to update
all the records specified in the records_dir.
Keyword Args:
records_dir (Path): Absolute path to directory containing record of placed trades.
"""
self.config = Config()
self.mt5 = MetaTrader()
self.records_dir = records_dir or self.config.records_dir
async def get_csv_records(self):
"""Get trade records saved as csv from records_dir folder
Yields:
files: Trade record files
"""
for file in self.records_dir.iterdir():
if file.is_file() and file.name.endswith('.csv'):
yield file
async def get_json_records(self):
"""Get trade records from records_dir folder
Yields:
files: Trade record files
"""
for file in self.records_dir.iterdir():
if file.is_file() and file.name.endswith('.json'):
yield file
async def read_update_csv(self, *, file: Path):
"""Read and update csv trade records
Args:
file: Trade record file in csv format
"""
try:
fr = open(file, mode='r', newline='')
reader: Iterable[dict] | csv.DictReader = csv.DictReader(fr)
rows = [row for row in reader]
rows = await self.update_rows(rows=rows)
fr.close()
fw = open(file, mode='w', newline='')
writer = csv.DictWriter(fw, fieldnames=reader.fieldnames, extrasaction='ignore', restval=None)
writer.writeheader()
writer.writerows(rows)
fw.close()
except Exception as err:
logger.error(f'Error: {err}. Unable to read and update csv trade records')
async def read_update_json(self, *, file: Path):
"""Read and update json trade records
Args:
file: Trade record file in csv format
"""
try:
fh = open(file, mode='r')
data = json.load(fh)
rows = [row for row in data]
rows = await self.update_rows(rows=rows)
fh.close()
fh = open(file, mode='w')
json.dump(rows, fh, indent=2)
fh.close()
except Exception as err:
logger.error(f'Error: {err}. Unable to read and update json trade records')
async def update_row(self, *, row: dict) -> dict:
"""Update a single row of entered trade in the csv or json file with the actual profit.
Args:
row: A dictionary from the dictionary writer object of the csv file.
Returns:
dict: A dictionary with the actual profit and win status.
"""
try:
order = int(row['order'])
deals = await self.mt5.history_deals_get(position=order)
if not deals or len(deals) <= 1:
return row
deals = [deal for deal in deals if (deal.order != deal.position_id and deal.position_id == order
and deal.entry == 1)]
deals.sort(key=lambda x: x.time_msc)
deal = deals[-1]
row.update(actual_profit=deal.profit, win=deal.profit > 0, closed=True)
return row
except Exception as err:
logging.error(f'Error: {err}. Unable to update trade record')
return row
async def update_rows(self, *, rows: list[dict]) -> list[dict]:
"""Update the rows of entered trades in the csv or json file with the actual profit.
Args:
rows: A list of dictionaries.
Returns:
list[dict]: A list of dictionaries with the actual profit and win status.
"""
closed, unclosed = [], []
for row in rows:
closed_ = row.get('closed', False)
closed_ = closed_.title() == 'True' if isinstance(closed_, str) else closed_
if closed_:
closed.append(row)
else:
unclosed.append(row)
unclosed = await asyncio.gather(*[self.update_row(row=row) for row in unclosed])
return closed + list(unclosed)
async def update_csv_records(self):
"""Update csv trade records in the records_dir folder."""
records = [self.read_update_csv(file=record) async for record in self.get_csv_records()]
await asyncio.gather(*records)
async def update_json_records(self):
"""Update json trade records in the records_dir folder."""
records = [self.read_update_json(file=record) async for record in self.get_json_records()]
await asyncio.gather(*records)
async def update_csv_record(self, *, file: Path | str):
"""Update a single trade record csv file."""
await self.read_update_csv(file=file)
async def update_json_record(self, *, file: Path | str):
"""Update a single json trade record file"""
await self.read_update_json(file=file)
+128
View File
@@ -0,0 +1,128 @@
"""Trader class module. Handles the creation of an order and the placing of trades"""
from abc import ABC, abstractmethod
from datetime import datetime
from typing import TypeVar
from logging import getLogger
from zoneinfo import ZoneInfo
from .order import Order
from .symbol import Symbol as _Symbol
from .ticks import Tick
from .ram import RAM
from .core.models import OrderType, OrderSendResult
from .core.config import Config
from .result import Result
logger = getLogger(__name__)
Symbol = TypeVar("Symbol", bound=_Symbol)
class Trader(ABC):
"""Base class for creating a Trader object. Handles the creation of an order and the placing of trades.
Attributes:
symbol (Symbol): The financial instrument.
ram (RAM): RAM instance
order (Order): Trade order
Class Attributes:
config (Config): Config instance.
"""
config: Config
def __init__(self, *, symbol: Symbol, ram: RAM = None):
"""Initializes the order object and RAM instance
Args:
symbol (Symbol): Financial instrument
ram (RAM): Risk Assessment and Management instance
"""
self.config = Config()
self.symbol = symbol
self.order = Order(symbol=symbol.name)
self.ram = ram or RAM()
self.parameters = {}
def set_order_limits(self, *, pips: float, tick: Tick):
"""Sets the stop loss and take profit for the order. This method uses pips as defined for forex instruments.
Args:
pips: Target pips
tick: Tick object
"""
pips = pips * self.symbol.pip
sl, tp = pips, pips * self.ram.risk_to_reward
if self.order.type == OrderType.BUY:
self.order.sl, self.order.tp = round(tick.ask - sl, self.symbol.digits), round(tick.ask + tp,
self.symbol.digits)
self.order.price = tick.ask
elif self.order.type == OrderType.SELL:
self.order.sl, self.order.tp = round(tick.bid + sl, self.symbol.digits), round(tick.bid - tp,
self.symbol.digits)
self.order.price = tick.bid
else:
raise ValueError(f"Invalid order type: {self.order.type}")
def set_trade_stop_levels(self, *, points: float, tick: Tick):
"""Set the stop loss and take profit levels of the order based on the points and price tick.
Args:
points: Target points
tick: Tick object
"""
points = points * self.symbol.point
sl, tp = points, points * self.ram.risk_to_reward
if self.order.type == OrderType.BUY:
self.order.sl, self.order.tp = round(tick.ask - sl, self.symbol.digits), round(tick.ask + tp,
self.symbol.digits)
self.order.price = tick.ask
else:
self.order.sl, self.order.tp = round(tick.bid + sl, self.symbol.digits), round(tick.bid - tp,
self.symbol.digits)
self.order.price = tick.bid
async def check_order(self) -> bool:
"""Check order before sending it to the broker.
Returns:
bool: True if order can go through else false
"""
check = await self.order.check()
if check.retcode != 0:
logger.warning(f"Invalid order for {self.symbol} due to {check.comment}")
return False
return True
async def send_order(self) -> OrderSendResult:
"""Send the order to the broker."""
result = await self.order.send()
if result.retcode != 10009:
logger.warning(f"Unable to place order for {self.symbol} due to {result.comment}")
return result
logger.info(f"Placed Trade for {self.symbol}")
return result
async def record_trade(self, result: OrderSendResult, parameters: dict = None, name: str = '', exclude: set = None):
"""Record the trade in csv or json.
Args:
result (OrderSendResult): Result of the order send
parameters: parameters of the trading strategy used to place the trade
name: Name of the trading strategy
exclude: Exclude these fields from the recorded trade
"""
if result.retcode != 10009 or not self.config.record_trades:
return
params = parameters or self.parameters
params = {k: v for k, v in params.items() if k not in (exclude or set())}
profit = result.profit or await self.order.calc_profit()
params["expected_profit"] = profit
date = datetime.utcnow()
date = date.replace(tzinfo=ZoneInfo("UTC"))
params["date"] = str(date.date())
params["time"] = str(date.time())
res = Result(result=result, parameters=params, name=name)
self.config.task_queue.add_task(res.save)
@abstractmethod
async def place_trade(self, *args, **kwargs):
"""Places a trade based on the order_type."""
+37
View File
@@ -0,0 +1,37 @@
"""Utility functions for aiomql."""
import decimal
from .candle import Candles, Candle
def dict_to_string(data: dict, multi=False) -> str:
"""Convert a dict to a string. Useful for logging.
Args:
data (dict): The dict to convert.
multi (bool, optional): If True, each key-value pair will be on a new line. Defaults to False.
Returns:
str: The string representation of the dict.
"""
sep = '\n' if multi else ', '
return f"{sep}".join(f"{key}: {value}" for key, value in data.items())
def round_off(value: float, step: float, round_down: bool = False) -> float:
"""Round off a number to the nearest step."""
with decimal.localcontext() as ctx:
ctx.rounding = decimal.ROUND_DOWN if round_down else decimal.ROUND_UP
return float(decimal.Decimal(str(value)).quantize(decimal.Decimal(str(step))))
def find_bearish_fractal(candles: Candles) -> Candle | None:
for i in range(len(candles) - 3, 1, -1):
if candles[i].high > max(candles[i - 1].high, candles[i + 1].high, candles[i - 2].high, candles[i + 2].high):
return candles[i]
def find_bullish_fractal(candles: Candles) -> Candle | None:
for i in range(len(candles) - 3, 1, -1):
if candles[i].low < min(candles[i - 1].low, candles[i + 1].low, candles[i - 2].low, candles[i + 2].low):
return candles[i]