mirror of
https://github.com/Ichinga-Samuel/aiomql.git
synced 2026-08-06 00:37:45 +00:00
v3.23
This commit is contained in:
+79
@@ -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
@@ -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.
|
||||
@@ -0,0 +1,100 @@
|
||||
# Aiomql - Bot Building Framework and Asynchronous MetaTrader5 Library
|
||||

|
||||

|
||||

|
||||
|
||||
### 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.\
|
||||
[](https://www.buymeacoffee.com/ichingasamuel)
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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 *
|
||||
@@ -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}
|
||||
@@ -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")
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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}
|
||||
@@ -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
|
||||
@@ -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}"
|
||||
@@ -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."""
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -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()]
|
||||
@@ -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
|
||||
@@ -0,0 +1,3 @@
|
||||
from .strategies import *
|
||||
from .traders import *
|
||||
from .symbols import *
|
||||
@@ -0,0 +1,2 @@
|
||||
from .finger_trap import FingerTrap
|
||||
from .tracker import Tracker
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
from .forex_symbol import ForexSymbol
|
||||
@@ -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")
|
||||
@@ -0,0 +1 @@
|
||||
from .simple_trader import SimpleTrader
|
||||
@@ -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}")
|
||||
@@ -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
|
||||
@@ -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])
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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}")
|
||||
@@ -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()
|
||||
@@ -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.
|
||||
"""
|
||||
@@ -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}')
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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."""
|
||||
@@ -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]
|
||||
Reference in New Issue
Block a user