This commit is contained in:
Ichinga Samuel
2024-01-18 02:26:27 +01:00
parent 4b5f267509
commit 63f22fc081
43 changed files with 387 additions and 545 deletions
+4 -2
View File
@@ -69,5 +69,7 @@ target/
.vscode/
# config file
aiomql.json
# config files
config.json
aiomql.json
config/
+55 -40
View File
@@ -1,27 +1,33 @@
# aiomql
# Aiomql - Bot Building Framework and Asynchronous MetaTrader5 Library
![GitHub](https://img.shields.io/github/license/ichinga-samuel/aiomql?style=plastic)
![GitHub issues](https://img.shields.io/github/issues/ichinga-samuel/aiomql?style=plastic)
![PyPI](https://img.shields.io/pypi/v/aiomql)
## Installation
### Installation
```bash
pip install aiomql
```
## Key Features
- Asynchronous Python Library For MetaTrader 5
### 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
- Record and keep track of trades and strategies in csv files.
- Utility classes for using the MetaTrader 5 Library
- 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
- Trade sessions for managing trading sessions
- Manage Trading periods using Sessions
- Risk Management
- Run multiple bots concurrently with different accounts from the same broker or different brokers
## Simple Usage as an asynchronous MetaTrader5 Libray
### As an asynchronous MetaTrader5 Libray
```python
import asyncio
# import the class
from aiomql import MetaTrader, Account, TimeFrame, OrderType
from aiomql import MetaTrader
async def main():
mt5 = MetaTrader()
await mt5.initialize()
@@ -31,54 +37,63 @@ async def main():
asyncio.run(main())
```
## As a Bot Building FrameWork using a Sample Strategy
### 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.lib import FingerTrap
from aiomql import Bot, Account, ForexSymbol, Session, Sessions, RAM
from aiomql import Bot, ForexSymbol, FingerTrap, Session, Sessions, RAM, SimpleTrader, TimeFrame
logging.basicConfig(level=logging.INFO)
def build_bot():
# Either initialize an account here with your login details here or set them in the aiomql.json file.
# acc = Account(login=1234567, password='*******', server='Broker-Server')
bot = Bot()
# Prebuilt strategy from the library.
# Disclaimer: These strategy is only for demonstration purposes.
# The author of this library is not responsible for any losses incurred from using this strategy.
# using trade sessions is optional. the strategy will run with a default session of 24 hours if not specified.
# session start and end times are in UTC. Make sure to convert to UTC if you are in a different timezone.
# sessions can be used to close positions at the end of a trading session.
sess = Session(name='London', start=8, end=time(hour=15, minute=30), on_end='close_all')
sess2 = Session(name='New York', start=13, end=time(hour=20, minute=30))
sess3 = Session(name='Tokyo', start=23, end=time(hour=6, minute=30))
sessions = Sessions(sess, sess2, sess3)
# configurable parameters for the strategy
params = {'trend_candles_count': 500, 'fast_period': 8}
# 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))
st1 = FingerTrap(symbol=ForexSymbol(name='GBPUSD'), params=params, sessions=sessions)
st3 = FingerTrap(symbol=ForexSymbol(name='AUDUSD'), params=params, sessions=sessions)
st4 = FingerTrap(symbol=ForexSymbol(name='USDCAD'), params=params, sessions=sessions)
st5 = FingerTrap(symbol=ForexSymbol(name='USDJPY'), params=params, sessions=sessions)
st6 = FingerTrap(symbol=ForexSymbol(name='EURGBP'), params=params, sessions=sessions)
# configure the parameters and the trader for a strategy
params = {'trend_candles_count': 500, 'fast_period': 8, 'slow_period': 34, 'entry_timeframe': 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))
# Risk Management
ram = RAM(risk=0.05, risk_to_reward=2)
# change the risk management of a strategy. This is done on the trader attribute of the strategy.
st5.trader.ram = ram
# 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, st3, st4, st5, st6])
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 more a mature package?
Consider supporting the project by buying me a coffee.\
[!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/ichingasamuel)
+31 -31
View File
@@ -1,46 +1,36 @@
## <a id="account"></a> Account
- [Account](#Account)
- [__aenter__](#Account.__aenter__)
- [sign_in](#Account.sign_in)
- [refresh](#Account.refresh)
- [has_symbol](#Account.has_symbol)
- [symbols_get](#Account.symbols_get)
- [AccountInfo](#AccountInfo)
- [Account](#Account)
- [sign_in](#Account.sign_in)
- [has_symbol](#Account.has_symbol)
- [symbols_get](#Account.symbols_get)
-
<a id="Account"></a>
### Account
```python
class Account(AccountInfo)
```
Singleton class for managing a trading account. A subclass of [AccountInfo](#accountinfo).
Singleton class for managing a trading account. A subclass of [AccountInfo](#AccountInfo).
All AccountInfo attributes are available in this class.
### Attributes:
|Name|Type|Description|Default|
|---|---|---|---|
|**connected**|**bool**|Status of connection to MetaTrader 5 Terminal|False|
|symbols|set[SymbolInfo]|A set of available symbols for the financial market.|set()|
### Notes
Other Account properties are defined in the AccountInfo class.
### refresh
```python
async def refresh()
```
Refreshes the account instance with the latest data from the MetaTrader 5 terminal
### account_info
```python
@property
def account_info() -> dict
```
Get account login, server and password details. If the login attribute of the account instance returns
a falsy value, the config instance is used to get the account details.
#### Returns:
|Type|Description|
|---|---|
|**dict**|A dict of login, server and password details|
#### Note:
This method will only look for config details in the config instance if the login attribute of the account Instance returns a falsy value
### __aenter__
<a id="Account.__aenter__"></a>
#### __aenter__
```python
async def __aenter__() -> 'Account'
```
Async context manager for the Account class. Connects to a trading account and returns the account instance.
#### Returns:
|Type|Description|
|---|---|
@@ -50,7 +40,8 @@ Async context manager for the Account class. Connects to a trading account and r
|---|---|
|**LoginError**|If login fails|
### sign_in
<a id="Account.sign_in"></a>
#### sign_in
```python
async def sign_in() -> bool
```
@@ -60,7 +51,15 @@ Connect to a trading account.
|---|---|
|**bool**|True if login was successful else False|
### has_symbol
<a id="Account.refresh"></a>
#### refresh
```python
async def refresh()
```
Refreshes the account instance with the latest data from the MetaTrader 5 terminal
<a id="Account.has_symbol"></a>
#### has_symbol
```python
def has_symbol(symbol: str | Type[SymbolInfo])
```
@@ -74,7 +73,8 @@ Checks to see if a symbol is available for a trading account
|---|---|
|**bool**|True if symbol is available else False|
### symbols_get
<a id="Account.symbols_get"></a>
#### symbols_get
```python
async def symbols_get() -> set[SymbolInfo]
```
+25 -9
View File
@@ -1,6 +1,6 @@
* [MetaTrader](#MetaTrader)
* [\_\_aenter\_\_](#__aenter__)
* [\_\_aexit\_\_](#aexit)
* [\_\_aenter\_\_](#MetaTrader.__aenter__)
* [\_\_aexit\_\_](#MetaTrader.__aexit__)
* [login](#MetaTrader.login)
* [initialize](#MetaTrader.initialize)
* [shutdown](#MetaTrader.shutdown)
@@ -35,14 +35,25 @@
* [history\_deals\_get](#MetaTrader.history_deals_get)
## <a id="MetaTrader"></a> MetaTrader
<a id="MetaTrader"></a>
### MetaTrader
```python
class MetaTrader(metaclass=BaseMeta)
```
The MetaTrader class is a wrapper around the MetaTrader terminal.
It provides methods for connecting to the MetaTrader terminal and retrieving data from it.
#### Attributes:
|Name|Type|Description|Default|
|---|---|---|---|
|error|Error|The last error encountered by the MetaTrader terminal.|Error(0, '')|
### <a id="MetaTrader.__aenter__"></a> \_\_aenter\_\_
#### Notes:
All the attributes, enums and constants of the MetaTrader5 class are also available here. Although, they are more easily
accessible and used via the various enums and models defined in the module.
<a id="MetaTrader.__aenter__"></a>
#### \_\_aenter\_\_
```python
async def __aenter__() -> 'MetaTrader'
```
@@ -54,13 +65,15 @@ Initializes the connection to the MetaTrader terminal.
|---|---|
|**MetaTrader**|An instance of the MetaTrader class|
#### <a id="MetaTrader.__aexit__"></a> \_\_aexit\_\_
<a id="MetaTrader.__aexit__"></a>
#### \_\_aexit\_\_
```python
async def __aexit__(exc_type, exc_val, exc_tb)
```
Async context manager exit point. Closes the connection to the MetaTrader terminal.
#### <a id="MetaTrader.login"></a> login
<a id="MetaTrader.login"></a>
#### login
```python
async def login(login: int,
password: str,
@@ -80,7 +93,8 @@ Connects to the MetaTrader terminal using the specified login, password and serv
|---|---|
|**bool**|True if successful, False otherwise.|
#### <a id="MetaTrader.initialize"></a> initialize
<a id="MetaTrader.initialize"></a>
#### initialize
```python
async def initialize(path: str = "",
login: int = 0,
@@ -104,13 +118,15 @@ Initializes the connection to the MetaTrader terminal. All parameters are option
|---|---|
|**bool**|True if successful, False otherwise.|
#### <a id="MetaTrader.shutdown"></a> shutdown
<a id="MetaTrader.shutdown"></a>
#### shutdown
```python
async def shutdown() -> None
```
Closes the connection to the MetaTrader terminal.
#### <a id="MetaTrader.version"></a> version
<a id="MetaTrader.version"></a>
#### version
```python
async def version() -> tuple[int, int, str] | None
```
+24 -24
View File
@@ -1,39 +1,39 @@
from datetime import time
import logging
from aiomql.lib import FingerTrap
from aiomql import Bot, Account, ForexSymbol, Session, Sessions
from aiomql import Bot, ForexSymbol, FingerTrap, Session, Sessions, RAM, SimpleTrader, TimeFrame
logging.basicConfig(level=logging.INFO)
def build_bot():
# Either initialize an account here with your login details here or set them in the aiomql.json file.
# acc = Account(login=1234567, password='*******', server='Broker-Server')
bot = Bot()
# Prebuilt strategy from the library.
# Disclaimer: These strategy is only for demonstration purposes.
# The author of this library is not responsible for any losses incurred from using this strategy.
# 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))
# using trade sessions is optional. the strategy will run with a default session of 24 hours if not specified.
# session start and end times are in UTC. Make sure to convert to UTC if you are in a different timezone.
# sessions can be used to close positions at the end of a trading session.
sess = Session(name='London', start=8, end=time(hour=15, minute=30), on_end='close_all')
sess2 = Session(name='New York', start=13, end=time(hour=20, minute=30))
sess3 = Session(name='Tokyo', start=23, end=time(hour=6, minute=30))
allsess = Session(name='All', start=0, end=23, on_end='close_all')
sessions = Sessions(sess, sess2, sess3, allsess)
# configure the parameters and the trader for a strategy
params = {'trend_candles_count': 500, 'fast_period': 8, 'slow_period': 34, 'entry_timeframe': 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))
# configurable parameters for the strategy
params = {'trend_candles_count': 500, 'fast_period': 8}
st1 = FingerTrap(symbol=ForexSymbol(name='GBPUSD'), params=params, sessions=sessions)
st3 = FingerTrap(symbol=ForexSymbol(name='AUDUSD'), params=params, sessions=sessions)
st4 = FingerTrap(symbol=ForexSymbol(name='USDCAD'), params=params, sessions=sessions)
st5 = FingerTrap(symbol=ForexSymbol(name='USDJPY'), params=params, sessions=sessions)
st6 = FingerTrap(symbol=ForexSymbol(name='EURGBP'), params=params, sessions=sessions)
bot.add_strategies([st1, st3, st4, st5, st6])
# 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()
build_bot()
# run the bot
build_bot()
+18 -10
View File
@@ -1,25 +1,32 @@
import asyncio
from aiomql import Symbol, TimeFrame, Account
from aiomql import Symbol, TimeFrame, Account, Candle, Candles
async def main():
"""Example of using the Candle and Candles classes.
The candle class is a single price bar. Holding the OHLCV data for a single price bar.
The Candles class is a container of Candle objects. It is an Iterable of Candle objects.
It is sliceable and indexable. It can also be accessed with keywords.
It is a wrapper around a pandas DataFrame. Which is what it uses to store the data.
"""
async with Account():
# create a symbol
sym = Symbol(name="AUDUSD")
sym = Symbol(name="EURUSD")
# Get EURUSD price bars for the past 48 hours
candles = await sym.copy_rates_from_pos(timeframe=TimeFrame.H1, count=48, start_position=0)
candles: Candles = await sym.copy_rates_from_pos(timeframe=TimeFrame.H1, count=48, start_position=0)
# get size of candles
print(len(candles)) # 48
# get the latest candle by accessing the last one.
last = candles[-1] # A Candle object
last: Candle = candles[-1] # A Candle object
print(type(last))
print(last.time)
print(last.Index)
# get the last five hours
last_five = candles[-5:] # A Candles object.
print(type(last_five))
print(last_five)
# slicing returns a Candles object
half = candles[24:]
print(type(half))
print(len(half))
close = candles['close'] # close price of all the candles as a pandas series
print(type(close))
@@ -32,6 +39,7 @@ async def main():
# use talib to compute crossover. This returns a series object that is not part of the candles object.
closeXema = candles.ta_lib.cross(candles.close, candles.ema)
# add to the candles
candles['closeXema'] = closeXema
print(candles)
+3 -3
View File
@@ -7,7 +7,7 @@ async def main():
async with Account():
# create a symbol
sym = ForexSymbol(name="EURUSD")
sym = ForexSymbol(name="EURUSD-T")
# Confirm the symbol is available for this account and initialize with default values.
res = await sym.init()
@@ -15,7 +15,7 @@ async def main():
# I want to place a market buy order, risk only 2usd, and target 10 pips in this trade.
# The ForexSymbol object has a compute_volume method that can be used to compute the volume
# given a target pips and amount.
volume = await sym.compute_volume(amount=2, pips=10)
volume = await sym.compute_volume(amount=2, points=100)
# a risk to reward ratio of 1:2
# get the price tick of the symbol
@@ -34,4 +34,4 @@ async def main():
print(res)
asyncio.run(main())
asyncio.run(main())
+18 -15
View File
@@ -1,25 +1,27 @@
import logging
import asyncio
from datetime import datetime
from aiomql import ForexSymbol, Account, Positions, History, Trader, OrderType, RAM
from aiomql import ForexSymbol, Account, Positions, History, SimpleTrader as Trader, OrderType, RAM
logging.basicConfig(level=logging.INFO, filemode='w', filename='example.log', format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
async def main():
# Account details are in the aiomql.json file
async with Account():
# get start time using local timezone
tz = datetime.now().astimezone().tzinfo
start = datetime.now(tz=tz)
# get start time
start = datetime.now()
# create two symbols and initialize them
sym1 = ForexSymbol(name="EURUSD")
sym2 = ForexSymbol(name="GBPUSD")
sym1 = ForexSymbol(name="EURUSD-T")
sym2 = ForexSymbol(name="GBPUSD-T")
await sym1.init()
await sym2.init()
# Risk Assets Management instance
# fix the amount to be risked at 2 USD. USD is the account currency.
ram = RAM(amount=2)
ram = RAM(amount=2, points=100)
# Create two traders instance
trd = Trader(symbol=sym1, ram=ram)
@@ -38,22 +40,23 @@ async def main():
# close all open positions
await pos.close_all()
end = datetime.now(tz=tz)
end = datetime.now()
# get the number of open positions
total = await pos.positions_total()
print(f'{total} Open positions') # 0
print(f'{total} Open positions')
# get historical trades
his = History(date_from=start, date_to=end)
# get the number of deals
total_deals = await his.deals_total()
print(f'{total_deals} Deals')
start = datetime(day=start.day-1, month=start.month, year=start.year, hour=start.hour, minute=0, second=0)
his = History(date_from=start.timestamp(), date_to=end.timestamp())
# get the number of order
orders = await his.orders_total()
print(f'{orders} orders')
# get the number of deals
# total_deals = await his.deals_total()
# print(f'{total_deals} Deals')
asyncio.run(main())
asyncio.run(main())
+4 -2
View File
@@ -1,10 +1,12 @@
import asyncio
from datetime import datetime
from aiomql import ForexSymbol, Symbol, TimeFrame, Account
from aiomql import ForexSymbol, TimeFrame, Account, Config
config = Config()
async def main():
async with Account():
sym = ForexSymbol(name="EURUSD")
sym = ForexSymbol(name="EURUSD-T")
res = await sym.init()
if not res:
print('Symbol not available')
+1 -1
View File
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "aiomql"
version = "3.12"
version = "3.14"
readme = "README.md"
requires-python = ">=3.10"
classifiers = [
+3 -7
View File
@@ -1,3 +1,4 @@
from .core import *
from .account import Account
from .ram import RAM
from .symbol import Symbol
@@ -14,10 +15,5 @@ from .history import History
from .trader import Trader
from .terminal import Terminal
from .sessions import Session, Sessions
from .core.config import Config
from .core.constants import *
from .core.meta_trader import MetaTrader
from .core.models import *
from .core.exceptions import *
from .lib import *
from .utils import dict_to_string
from .lib import *
+10 -18
View File
@@ -1,5 +1,4 @@
from logging import getLogger
from typing import Type
from .core.models import AccountInfo, SymbolInfo
from .core.exceptions import LoginError
@@ -18,6 +17,7 @@ class Account(AccountInfo):
Notes:
Other Account properties are defined in the AccountInfo class.
"""
_instance: 'Account'
connected: bool
symbols = set()
@@ -26,27 +26,18 @@ class Account(AccountInfo):
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self, **kwargs):
super().__init__(**kwargs)
if not self.login:
acc = self.config.account_info()
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)
@property
def account_info(self) -> dict:
"""Get account login, server and password details. If the login attribute of the account instance returns
a falsy value, the config instance is used to get the account details.
Returns:
dict: A dict of login, server and password details
Note:
This method will only look for config details in the config instance if the login attribute of the
account Instance returns a falsy value
"""
acc_info = self.get_dict(include={'login', 'server', 'password'})
return acc_info if acc_info['login'] else self.config.account_info()
async def __aenter__(self) -> 'Account':
"""Connect to a trading account and return the account instance.
Async context manager for the Account class.
@@ -72,8 +63,9 @@ class Account(AccountInfo):
Returns:
bool: True if login was successful else False
"""
await self.mt5.initialize(**self.account_info)
self.connected = await self.mt5.login(**self.account_info)
acc = self.get_dict(include={'login', 'server', 'password'})
await self.mt5.initialize(**acc, path=self.config.path)
self.connected = await self.mt5.login(**acc)
if self.connected:
await self.refresh()
self.symbols = await self.symbols_get()
+9 -2
View File
@@ -4,6 +4,7 @@ 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
@@ -20,11 +21,17 @@ class Bot:
account (Account): Account Object.
executor: The default thread executor.
symbols (list[Symbols]): A set of symbols for the trading session
"""
config (Config): Config instance
account: Account = Account()
"""
config: Config
account: Account
symbols: set
executor: Executor
def __init__(self):
self.config = Config()
self.account = Account()
self.symbols = set()
self.executor = Executor(bot=self)
-1
View File
@@ -2,7 +2,6 @@
from typing import Type, TypeVar, Generic, Iterable
from logging import getLogger
import reprlib
from pandas import DataFrame, Series
import pandas_ta as ta
+9 -29
View File
@@ -16,16 +16,15 @@ class Base:
Keyword Args:
**kwargs: Object attributes and values as keyword arguments. Only added if they are annotated on the class body.
Class Attributes:
mt5 (MetaTrader): An instance of the MetaTrader class
config (Config): An instance of the Config class
Meta (Type[Meta]): The Meta class for configuration of the data model class
"""
mt5: MetaTrader = MetaTrader()
config = Config()
mt5: MetaTrader
config: Config
def __init__(self, **kwargs):
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):
@@ -114,27 +113,8 @@ class Base:
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 self.Meta.filter}
key not in _filter}
except Exception as err:
logger.warning(err)
class Meta:
"""A class for defining class attributes to be excluded or included in the dict property
Attributes:
exclude (set): A set of attributes to be excluded
include (set): Specific attributes to be returned. Include supercedes exclude.
"""
exclude = {'mt5', "Config"}
include = set()
@classmethod
@property
def filter(cls) -> set:
"""Combine the exclude and include attributes to return a set of attributes to be excluded.
Returns:
set: A set of attributes to be excluded
"""
return cls.exclude.difference(cls.include)
logger.warning(err)
+16 -12
View File
@@ -25,7 +25,7 @@ class Config:
server (str): Broker server
path (str): Path to terminal file
timeout (int): Timeout for terminal connection
_initialize (bool): First time initialization flag
Notes:
By default, the config class looks for a file named aiomql.json.
You can change this by passing the filename keyword argument to the constructor.
@@ -38,10 +38,11 @@ class Config:
path: str = ""
timeout: int = 60000
record_trades: bool = True
filename: str = "aiomql.json"
filename: str
win_percentage: float = 0.85
records_dir = Path.home() / "Documents" / "Aiomql" / "Trade Records"
_load = 1
config_dir: str = ''
_initialize = True
def __new__(cls, *args, **kwargs):
if not hasattr(cls, "_instance"):
@@ -49,8 +50,10 @@ class Config:
return cls._instance
def __init__(self, **kwargs):
self.load_config(reload=False)
[setattr(self, key, value) for key, value in kwargs]
self.filename = kwargs.pop('filename', "aiomql.json")
self.config_dir = kwargs.pop('config_dir', '')
self.load_config(reload=kwargs.pop('reload', False))
[setattr(self, key, value) for key, value in kwargs.items()]
@staticmethod
def walk_to_root(path: str) -> Iterator[str]:
@@ -76,6 +79,7 @@ class Config:
frame = frame.f_back
frame_filename = frame.f_code.co_filename
path = os.path.dirname(os.path.abspath(frame_filename))
path = os.path.join(path, self.config_dir) if self.config_dir else path
for dirname in self.walk_to_root(path):
check_path = os.path.join(dirname, self.filename)
@@ -83,14 +87,14 @@ class Config:
return check_path
return None
def load_config(self, file: str = None, reload: bool = True):
if reload:
self._load = 1
if self._load != 1:
def load_config(self, file: str = None, reload: bool = True, filename: str = None, config_dir: str = ''):
"""Load configuration settings from a file."""
if not (self._initialize or reload):
return
self._load = 0
self._initialize = False
data = {}
self.filename = filename or self.filename
self.config_dir = config_dir or self.config_dir
if (file := (file or self.find_config())) is None:
logger.warning("No Config File Found")
else:
@@ -100,7 +104,7 @@ class Config:
[setattr(self, key, value) for key, value in data.items()]
self.records_dir.mkdir(parents=True, exist_ok=True) if self.records_dir else ...
def account_info(self) -> dict["login", "password", "server"]:
def account_info(self) -> dict[str, int | str]:
"""Returns Account login details as found in the config object if available
Returns:
+1
View File
@@ -19,6 +19,7 @@ class Error:
-10004: 'internal IPC no ipc',
-10005: 'internal timeout',
}
def __init__(self, code: int, description: str = ''):
self.code = code
self.description = description or self.descriptions.get(code, 'Unknown Error')
+56 -58
View File
@@ -56,6 +56,11 @@ class MetaTrader(metaclass=BaseMeta):
_symbols_total: Callable
_terminal_info: Callable
_version: Callable
error: Error
config: Config
def __init__(self):
self.config = Config()
async def __aenter__(self) -> 'MetaTrader':
"""
@@ -120,33 +125,37 @@ class MetaTrader(metaclass=BaseMeta):
return await asyncio.to_thread(self._shutdown)
async def last_error(self) -> tuple[int, str]:
return await asyncio.to_thread(self._last_error)
try:
return await asyncio.to_thread(self._last_error)
except Exception as err:
logger.warning(f'Error in obtaining last error.')
return 0, 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()
logger.warning(f'Error in obtaining version information.{Error(*err)}')
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()
logger.warning(f'Error in obtaining account information.{Error(*err)}')
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()
logger.warning(f'Error in obtaining terminal information.{Error(*err)}')
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:
@@ -155,32 +164,29 @@ class MetaTrader(metaclass=BaseMeta):
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()
logger.warning(f'Error in obtaining symbols.{Error(*err)}')
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()
logger.warning(f'Error in obtaining information for {symbol}.{Error(*err)}')
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()
logger.warning(f'Error in obtaining tick for {symbol}.{Error(*err)}')
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:
@@ -191,23 +197,22 @@ class MetaTrader(metaclass=BaseMeta):
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()
logger.warning(f'Error in obtaining market depth content for {symbol}.{Error(*err)}')
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 | int, count: int):
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()
logger.warning(f'Error in obtaining rates for {symbol}.{Error(*err)}')
self.error = Error(*err)
logger.warning(f'Error in obtaining rates for {symbol}.{self.error.description}')
return res
return res
@@ -215,39 +220,37 @@ class MetaTrader(metaclass=BaseMeta):
res = await asyncio.to_thread(self._copy_rates_from_pos, symbol, timeframe, start_pos, count)
if res is None:
err = await self.last_error()
logger.warning(f'Error in obtaining rates for {symbol}.{Error(*err)}')
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 | int,
date_to: datetime | int):
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()
logger.warning(f'Error in obtaining rates for {symbol}.{Error(*err)}')
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 | int, count: int, flags: CopyTicks):
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()
logger.warning(f'Error in obtaining ticks for {symbol}.{Error(*err)}')
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 | int, date_to: datetime | int, flags: CopyTicks):
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()
logger.warning(f'Error in obtaining ticks for {symbol}.{Error(*err)}')
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:
@@ -270,33 +273,30 @@ class MetaTrader(metaclass=BaseMeta):
"""
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()
logger.warning(f'Error in obtaining orders.{Error(*err)}')
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()
logger.warning(f'Error in calculating margin.{Error(*err)}')
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()
logger.warning(f'Error in calculating profit.{Error(*err)}')
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:
@@ -311,41 +311,39 @@ class MetaTrader(metaclass=BaseMeta):
async def positions_get(self, group: str = "", ticket: int = 0, 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()
logger.warning(f'Error in obtaining open positions.{Error(*err)}')
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 | int, date_to: datetime | int) -> int:
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 | int = None, date_to: datetime | int = None, group: str = '',
async def history_orders_get(self, date_from: datetime | float = None, date_to: datetime | float = None, group: str = '',
ticket: int = 0, position: int = 0) -> tuple[TradeOrder] | None:
kwargs = {key: value for key, value in (('date_from', date_from), ('date_to', date_to), ('group', group),
('ticket', ticket), ('position', position)) if value}
res = await asyncio.to_thread(self._history_orders_get, **kwargs)
if res is None:
err = await self.last_error()
logger.warning(f'Error in getting orders.{Error(*err)}')
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 | int, date_to: datetime | int) -> int:
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 | int = None, date_to: datetime | int = None, group: str = '',
ticket: int = 0, position: int = 0) -> tuple[TradeDeal] | None:
async def history_deals_get(self, date_from: datetime | float = None, date_to: datetime | float = None,
group: str = '', ticket: int = 0, position: int = 0) -> tuple[TradeDeal] | None:
kwargs = {key: value for key, value in (('date_from', date_from), ('date_to', date_to), ('group', group),
('ticket', ticket), ('position', position)) if value}
res = await asyncio.to_thread(self._history_deals_get, **kwargs)
if res is None:
err = await self.last_error()
logger.warning(f'Error in getting deals.{Error(*err)}')
self.error = Error(*err)
logger.warning(f'Error in getting deals.{self.error.description}')
return res
return res
+12 -8
View File
@@ -24,8 +24,8 @@ class History:
mt5 (MetaTrader): MetaTrader instance
config (Config): Config instance
"""
mt5: MetaTrader = MetaTrader()
config: Config = Config()
mt5: MetaTrader
config: Config
def __init__(self, *, date_from: datetime | float = None, date_to: datetime | float = None,
group: str = "", ticket: int = 0, position: int = 0):
@@ -41,6 +41,8 @@ class History:
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
@@ -77,11 +79,12 @@ class History:
"""
deals = await self.mt5.history_deals_get(date_from=self.date_from, date_to=self.date_to, position=self.position,
group=self.group, ticket=self.ticket)
if deals is not None:
self.deals = [TradeDeal(**deal._asdict()) for deal in deals] if deals else []
self.total_deals = len(self.deals)
return self.deals
if deals is None:
logger.warning(f'Failed to get deals due to {self.mt5.error.description}')
deals = []
self.deals = [TradeDeal(**deal._asdict()) for deal in deals]
self.total_deals = len(self.deals)
return self.deals
async def deals_total(self) -> int:
@@ -103,7 +106,8 @@ class History:
orders = await self.mt5.history_orders_get(date_from=self.date_from, date_to=self.date_to, group=self.group,
position=self.position, ticket=self.ticket)
if orders is None:
return self.orders
logger.warning(f'Failed to get orders due to {self.mt5.error.description}')
orders = []
self.orders = [TradeOrder(**order._asdict()) for order in orders]
self.total_orders = len(self.orders)
@@ -116,4 +120,4 @@ class History:
int: Total number of orders
"""
self.total_orders = await self.mt5.history_orders_total(self.date_from, self.date_to)
return self.total_orders
return self.total_orders
+1 -1
View File
@@ -25,7 +25,7 @@ class FingerTrap(Strategy):
trend_candles_count: int
trader: Trader
tracker: Tracker
_parameters = {"trend": 3, "fast_period": 8, "slow_period": 34, "entry_time_frame": TimeFrame.M5,
parameters = {"trend": 3, "fast_period": 8, "slow_period": 34, "entry_time_frame": TimeFrame.M5,
"trend_time_frame": TimeFrame.H1, "entry_period": 8,
"trend_candles_count": 48, "entry_candles_count": 50}
+1 -1
View File
@@ -12,7 +12,7 @@ class ForexSymbol(Symbol):
Args:
amount (float): Amount to risk. Given in terms of the account currency.
points (float): Target pips.
points (float): Target points.
use_limits (bool): If True, the computed volume checked against the maximum and minimum volume.
Returns:
+15 -23
View File
@@ -1,5 +1,3 @@
"""Trader class module. Handles the creation of an order and the placing of trades"""
from logging import getLogger
from ..symbols import ForexSymbol
@@ -13,49 +11,43 @@ logger = getLogger(__name__)
class SimpleTrader(Trader):
"""A simple trader class. Limits the number of loosing trades per symbol"""
def __init__(self, *, symbol: ForexSymbol, ram: RAM = None, num_trades: int = 1):
def __init__(self, *, symbol: ForexSymbol, ram: RAM = None, loss_limit: int = 3):
"""Initializes the order object and RAM instance
The default risk to reward ratio is 1:1.
Args:
symbol (Symbol): Financial instrument
ram (RAM): Risk Assessment and Management instance
num_trades (int): Number of open trades in loosing positions to allow per symbol
loss_limit (int): Maximum number of losing trades allowed at a time.
"""
ram = ram or RAM(risk_to_reward=1, points=100)
super().__init__(symbol=symbol, ram=ram)
self.positions = Positions(symbol=symbol.name)
self.num_trades = num_trades
self.loss_limit = loss_limit
async def create_order(self, *, order_type: OrderType, points: float = 0):
async def create_order(self, *, order_type: OrderType):
"""Complete the order object with the required values. Creates a simple order.
Args:
order_type (OrderType): Type of order
points (float): Target points
"""
positions = await self.positions.positions_get()
positions.sort(key=lambda pos: pos.time_msc)
positions = await Positions().positions_get()
loosing = [trade for trade in positions if trade.profit < 0]
if (losses := len(loosing)) > self.num_trades:
if (losses := len(loosing)) > self.loss_limit:
raise RuntimeError(f"Last {losses} trades in a losing position")
points = points or self.symbol.trade_stops_level * 2
amount = self.ram.amount or await self.ram.get_amount()
points = self.ram.points or self.symbol.trade_stops_level * 3
amount = await self.ram.get_amount()
self.order.volume = await self.symbol.compute_volume(amount=amount, points=points)
self.order.type = order_type
self.order.comment = self.parameters.get('name', '')
await self.set_trade_stop_levels(points=points)
async def place_trade(self, order_type: OrderType, parameters: dict = None, points: float = 0):
"""Places a trade based on the order_type.
Args:
order_type (OrderType): Type of order
parameters: parameters of the trading strategy used to place the trade
points (float): Target points
"""
async def place_trade(self, order_type: OrderType, parameters: dict = None):
"""Places a trade based on the order_type."""
try:
self.parameters |= parameters or {}
await self.create_order(order_type=order_type, points=points)
await self.create_order(order_type=order_type)
if not await self.check_order():
return
await self.send_order()
except Exception as err:
logger.error(f"{err}. Symbol: {self.order.symbol}\n {self.__class__.__name__}.place_trade")
logger.error(f"{err} in {self.__class__.__name__}.place_trade for {self.symbol.name}")
+6 -5
View File
@@ -51,6 +51,8 @@ class Order(TradeRequest):
tuple[TradeOrder]: A Tuple of active trade orders as TradeOrder objects
"""
orders = await self.mt5.orders_get(symbol=self.symbol)
if orders is None:
raise OrderError(f'Failed to get orders for {self.symbol} due to {self.mt5.error.description}')
orders = (TradeOrder(**order._asdict()) for order in orders)
return tuple(orders)
@@ -65,7 +67,7 @@ class Order(TradeRequest):
"""
res = await self.mt5.order_check(self.dict)
if res is None:
raise OrderError(f'Failed to check order {self.symbol} {self.type} {self.volume} {self.price} {res}')
raise OrderError(f'Failed to check order due to {self.mt5.error.description}')
return OrderCheckResult(**res._asdict())
async def send(self) -> OrderSendResult:
@@ -79,7 +81,7 @@ class Order(TradeRequest):
"""
res = await self.mt5.order_send(self.dict)
if res is None:
raise OrderError(f'Failed to send order {self.symbol} {self.type} {self.volume} {self.price}')
raise OrderError(f'Failed to send order {self.symbol} due to {self.mt5.error.description}')
return OrderSendResult(**res._asdict())
async def calc_margin(self) -> float:
@@ -93,7 +95,7 @@ class Order(TradeRequest):
"""
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} {self.type} {self.volume} {self.price} {res}')
raise OrderError(f'Failed to calculate margin for {self.symbol} due to {self.mt5.error.description}')
return res
async def calc_profit(self) -> float:
@@ -107,6 +109,5 @@ class Order(TradeRequest):
"""
res = await self.mt5.order_calc_profit(self.type, self.symbol, self.volume, self.price, self.tp)
if res is None:
raise OrderError(
f'Failed to calculate profit for {self.symbol} {self.type} {self.volume} {self.price} {self.tp}')
raise OrderError(f'Failed to calculate profit for {self.symbol} due to {self.mt5.error.description}')
return res
+7 -6
View File
@@ -18,7 +18,7 @@ class Positions:
ticket (int): Position ticket.
mt5 (MetaTrader): MetaTrader instance.
"""
mt5: MetaTrader = MetaTrader()
mt5: MetaTrader
def __init__(self, *, symbol: str = "", group: str = "", ticket: int = 0):
"""Get Open Positions.
@@ -30,6 +30,7 @@ class Positions:
ticket (int): Position ticket
"""
self.mt5 = MetaTrader()
self.symbol = symbol
self.group = group
self.ticket = ticket
@@ -42,7 +43,7 @@ class Positions:
"""
return await self.mt5.positions_total()
async def positions_get(self, symbol: str = '', group: str = '', ticket: int = 0):
async def positions_get(self, symbol: str = '', group: str = '', ticket: int = 0) -> list[TradePosition]:
"""Get open positions with the ability to filter by symbol or ticket.
Keyword Args:
@@ -56,8 +57,9 @@ class Positions:
"""
positions = await self.mt5.positions_get(group=group or self.group, symbol=symbol or self.symbol,
ticket=ticket or self.ticket)
if not positions:
return []
if positions is None:
logger.warning(f'Failed to get positions for {symbol or self.symbol} due to {self.mt5.error.description}')
positions = []
return [TradePosition(**pos._asdict()) for pos in positions]
async def close(self, *, ticket: int, symbol: str, price: float, volume: float, order_type: OrderType):
@@ -84,5 +86,4 @@ class Positions:
symbol=pos.symbol) for pos in positions]
results = await asyncio.gather(*[order for order in orders], return_exceptions=True)
amount_closed = len([res for res in results if res.retcode == 10009])
return amount_closed
return len([res for res in results if res.retcode == 10009])
+6 -7
View File
@@ -3,12 +3,14 @@ from .account import Account
class RAM:
account: Account = Account()
account: Account
risk_to_reward: float
risk: float
amount: float
points: float
pips: float
min_amount: float
max_amount: float
def __init__(self, *, risk_to_reward: float = 1, risk: float = 0.01, amount: float = 0, **kwargs):
"""Initialize Risk Assessment and Management with the provided keyword arguments.
@@ -22,17 +24,14 @@ class RAM:
self.risk_to_reward = risk_to_reward
self.risk = risk
self.amount = amount
self.account = Account()
[setattr(self, key, value) for key, value in kwargs.items()]
async def get_amount(self, risk: float = 0) -> float:
async def get_amount(self) -> float:
"""Calculate the amount to risk per trade as a percentage of equity.
Keyword Args:
risk (float): Percentage of account balance to risk per trade. Defaults to zero.
Returns:
float: Amount to risk per trade
"""
await self.account.refresh()
risk = risk or self.risk
return self.account.equity * risk
return self.account.equity * self.risk
+4 -2
View File
@@ -18,8 +18,8 @@ class Records:
records_dir(Path): Path to directory containing record of placed trades, If not given takes the default
from the config
"""
config: Config = Config()
mt5: MetaTrader = MetaTrader()
config: Config
mt5: MetaTrader
def __init__(self, records_dir: Path = ''):
"""Initialize the Records class. The main method of this class is update_records which you should call to update
@@ -28,6 +28,8 @@ class Records:
Keyword Args:
records_dir (Path): 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):
+2 -2
View File
@@ -1,4 +1,3 @@
import asyncio
import csv
from logging import getLogger
@@ -16,7 +15,7 @@ class Result:
config (Config): The configuration object
name: Any desired name for the result file object
"""
config = Config()
config: Config
def __init__(self, result: OrderSendResult, parameters: dict = None, name: str = ''):
"""
@@ -26,6 +25,7 @@ class Result:
parameters:
name:
"""
self.config = Config()
self.parameters = parameters or {}
self.result = result
self.name = name or parameters.get('name', 'Trades')
+1 -1
View File
@@ -209,7 +209,7 @@ class Sessions:
await self.current_session.close() if self.current_session else ...
current_session = self.find_next(now)
secs = current_session.until() + 10
print(f'sleeping for {secs} seconds until next {current_session} session')
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()
+6 -11
View File
@@ -7,7 +7,6 @@ from datetime import time as dtime
from .core.meta_trader import MetaTrader
from .symbol import Symbol as _Symbol
from .account import Account
from .core import Config
from .sessions import Sessions, Session
@@ -23,21 +22,15 @@ class Strategy(ABC):
parameters (Dict): A dictionary of parameters for the strategy.
sessions (Sessions): The sessions to use for the strategy.
Class Attributes:
account (Account): Account instance.
mt5 (MetaTrader): MetaTrader instance.
config (Config): Config instance.
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
account = Account()
mt5: MetaTrader()
config = Config()
_parameters = {}
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.
@@ -47,12 +40,14 @@ class Strategy(ABC):
symbol (Symbol): The Financial instrument
params (Dict): Trading strategy parameters
"""
self.parameters = self._parameters | (params or {})
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})"
+10 -1
View File
@@ -26,7 +26,16 @@ class Symbol(SymbolInfo):
Make sure Symbol is always initialized with a name argument
"""
tick: Tick
account = Account()
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):
+2 -2
View File
@@ -4,7 +4,7 @@ from typing import NamedTuple
from logging import getLogger
from .core.models import TerminalInfo
logger = getLogger()
logger = getLogger(__name__)
class Terminal(TerminalInfo):
@@ -67,4 +67,4 @@ class Terminal(TerminalInfo):
Returns:
int: Total number of available symbols
"""
return await self.mt5.symbols_total()
return await self.mt5.symbols_total()
+9 -7
View File
@@ -1,7 +1,6 @@
"""Module for working with price ticks."""
from typing import TypeVar, Iterable
import reprlib
from pandas import DataFrame, Series
import pandas_ta as ta
@@ -31,27 +30,30 @@ class Tick:
ask: float
last: float
volume: float
time_msc:float
time_msc: float
flags: float
volume_real:float
volume_real: float
Index: int
def __init__(self, **kwargs):
self.time = kwargs.pop('time', 0)
self.Index = kwargs.pop('Index', 0)
self.set_attributes(**kwargs)
def __repr__(self):
keys = reprlib.repr(', '.join('%s=%s' % (i, j) for i, j in self.__dict__.items()))[1:-1]
return '%(class)s(%(args)s)' % {'class': self.__class__.__name__, 'args': keys}
return ("%(class)s(Index=%(Index)s, time=%(time)s, bid=%(bid)s, ask=%(ask)s, last=%(last)s, volume=%(volume)s,"
" mid=%(mid)s)") % {"class": self.__class__.__name__, "time": self.time, "bid": self.bid,
"ask": self.ask, "last": self.last, "volume": self.volume, 'Index': self.Index}
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 data class for price ticks. Arrange in chronological order.
Supports iteration, slicing and assignment
@@ -164,4 +166,4 @@ class Ticks:
None: If inplace is True
"""
res = self._data.rename(columns=kwargs, inplace=inplace)
return res if inplace else self.__class__(data=res)
return res if inplace else self.__class__(data=res)
+16 -13
View File
@@ -28,7 +28,7 @@ class Trader(ABC):
Class Attributes:
config (Config): Config instance.
"""
config = Config()
config: Config
def __init__(self, *, symbol: Symbol, ram: RAM = None):
"""Initializes the order object and RAM instance
@@ -37,6 +37,7 @@ class Trader(ABC):
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()
@@ -92,39 +93,41 @@ class Trader(ABC):
"""
check = await self.order.check()
if check.retcode != 0:
logger.warning(f"Symbol: {self.order.symbol}\nResult:\n"
f"{dict_to_string(check.get_dict(include={'comment', 'retcode'}), multi=True)}")
logger.warning(f"""Unable to place order for {self.symbol}\n
{dict_to_string(check.get_dict(include={'comment', 'retcode'}) | check.request._asdict(), multi=True)}""")
return False
return True
async def send_order(self):
"""Send the order to the broker."""
parameters = self.parameters.copy()
result = await self.order.send()
if result.retcode != 10009:
logger.warning(f"Symbol: {self.order.symbol}\nResult:\n"
f"{dict_to_string(result.get_dict(include={'comment', 'retcode'}), multi=True)}")
logger.warning(f"""Unable to place order for {self.symbol}\n
{dict_to_string(result.get_dict(include={'comment', 'retcode'}) | result.request._asdict(),
multi=True)}\n""")
return
logger.info(f"Symbol: {self.order.symbol}\nOrder: {dict_to_string(result.dict, multi=True)}\n")
await self.record_trade(result, parameters)
logger.info(f"""Placed Trade for {self.symbol}\n{dict_to_string(
result.get_dict(exclude={'request', 'retcode_external', 'retcode', 'request_id'}), multi=True)}\n""")
await self.record_trade(result, parameters=self.parameters.copy())
async def record_trade(self, result: OrderSendResult, parameters: dict):
async def record_trade(self, result: OrderSendResult, parameters: dict = None, name: str = ''):
"""Record the trade in a csv file.
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
"""
if result.retcode != 10009 or not self.config.record_trades:
return
params = parameters
params = parameters or self.parameters.copy()
profit = await self.order.calc_profit()
params["expected_profit"] = profit
date = datetime.utcnow()
date = date.replace(tzinfo=ZoneInfo("UTC"))
params["date"] = date
params["time"] = date.timestamp()
res = Result(result=result, parameters=params)
params["date"] = str(date.date())
params["time"] = str(date.time())
res = Result(result=result, parameters=params, name=name)
await res.save_csv()
@abstractmethod
+2 -1
View File
@@ -1,5 +1,6 @@
"""Utility functions for aiomql."""
def dict_to_string(data: dict, multi=False) -> str:
"""Convert a dict to a string. Use for logging.
@@ -11,4 +12,4 @@ def dict_to_string(data: dict, multi=False) -> str:
str: The string representation of the dict.
"""
sep = '\n' if multi else ', '
return f"{sep}".join(f"{key}: {value}\n" for key, value in data.items())
return f"{sep}".join(f"{key}: {value}" for key, value in data.items())
-10
View File
@@ -1,10 +0,0 @@
from aiomql import MetaTrader
import pytest
from .fixtures import *
@pytest.mark.asyncio
class BaseTest:
""""""
mt5 = MetaTrader()
-31
View File
@@ -1,31 +0,0 @@
import json
import os
from aiomql import MetaTrader as mt5, Config
import pytest
@pytest.fixture(scope="session")
def get_default_config():
data = {"win_percentage": 0.90, "record_dir": "Trade Records"}
obj = open('mt5.json', 'w')
json.dump(data, obj)
obj.close()
yield
os.remove('mt5.json')
@pytest.fixture(scope="session")
def get_config():
data = {"win_percentage": 0.8, "record_dir": "Trade_Records"}
obj = open('config.json', 'w')
json.dump(data, obj)
obj.close()
yield
os.remove('config.json')
@pytest.fixture(autouse=True, scope="session")
def init():
config = Config(filename="test_config.json")
mt5._initialize()
mt5._login(login=config.account_number, password=config.password, server=config.server)
-5
View File
@@ -1,5 +0,0 @@
{
"account_number": 160286827,
"password": "TheN@me0fTheW!nd",
"server": "ForexTimeFXTM-Demo01"
}
-13
View File
@@ -1,13 +0,0 @@
from aiomql import config
from . import get_config, get_default_config
def test_default_config_file(get_default_config):
conf = config.Config()
assert conf.win_percentage == 0.90
def test_config_file_name(get_config):
conf = config.Config(filename='config.json')
assert conf.win_percentage == 0.8
-7
View File
@@ -1,7 +0,0 @@
from aiomql import TradeAction
class TestConstants:
def test_trade_action(self):
assert TradeAction.DEAL == 1
View File
-11
View File
@@ -1,11 +0,0 @@
from aiomql.symbol import Symbol
from . import *
class TestSymbol(BaseTest):
sym = Symbol(name="EURJPY")
async def test_init(self):
await self.sym.init()
assert self.sym.select is True
-22
View File
@@ -1,22 +0,0 @@
from . import *
from aiomql import Terminal
class TestTerminal(BaseTest):
terminal = Terminal()
async def test_version(self):
res = await self.terminal.version
assert len(res) == 3
async def test_info(self):
res = await self.terminal.info()
assert res.connected is True
async def test_error(self):
res = await self.terminal.last_error()
assert res.code == 1
async def test_symbols_get(self):
res = await self.terminal.symbols_get()
sym = next(res)
assert isinstance(sym.name, str)
-91
View File
@@ -1,91 +0,0 @@
# from datetime import datetime
# from collections import defaultdict
# from pickle import HIGHEST_PROTOCOL
# import _pickle as pickle
# import lzma
# import asyncio
# from itertools import product
# from typing import Iterable, TypeAlias
#
# from .meta_trader import MetaTrader
# from .constants import TimeFrame
# from .. import account, Account, Ticks, Symbol, Candles
#
# Rates: TypeAlias = dict[Symbol, dict[TimeFrame, Candles]]
# PriceTicks: TypeAlias = dict[Symbol, Ticks]
#
#
# class MetaTester(MetaTrader):
# def __init__(self, *, file=None, data: 'TestData' = None):
# self.file = file
#
# @property
# def data(self):
# return TestData.load(self.file)
#
#
# class TestData:
# rates: Rates
# ticks: PriceTicks
# account: Account
#
# def __init__(self, symbols: Iterable[Symbol], timeframes: Iterable[TimeFrame], start: datetime, end: datetime, file: str):
# self.symbols = symbols
# self.timeframes = timeframes
# self.start = start
# self.end = end
# self.file = file
#
# @property
# async def _account(self) -> Account:
# await account.refresh()
# return account
#
# @property
# async def _ticks(self) -> PriceTicks:
# tasks = []
# symbols = []
# for symbol in self.symbols:
# coro = symbol.copy_ticks_range(date_from=self.start, date_to=self.end)
# symbols.append(symbol)
# tasks.append(asyncio.create_task(coro))
# ticks = await asyncio.gather(*tasks)
# return {symbol: ticks for symbol, ticks in zip(symbols, ticks)}
#
# @property
# async def _rates(self) -> Rates:
# _data = {'tasks': [], 'symbols': [], 'timeframes': []}
# args: Iterable[tuple[Symbol, TimeFrame]] = product(self.symbols, self.timeframes)
# for symbol, timeframe in args:
# coro = symbol.copy_rates_range(date_from=self.start, date_to=self.end, timeframe=timeframe)
# _data['tasks'].append(asyncio.create_task(coro))
# _data['symbols'].append(symbol)
# _data['timeframes'].append(timeframe)
# _data['rates'] = await asyncio.gather(*_data['tasks'])
#
# data = defaultdict(dict)
# for rates, symbol, timeframe in zip(_data['rates'], _data['symbols'], _data['timeframes']):
# data[symbol] |= {timeframe: rates}
# return data
#
# async def copy_data(self):
# self.rates, self.ticks, self.account = await asyncio.gather(self._rates, self._ticks, self._account)
#
# async def dumps(self):
# return pickle.dumps(self, protocol=HIGHEST_PROTOCOL)
#
# async def dump(self):
# await self.copy_data()
# with lzma.open(self.file, 'wb') as fh:
# pickle.dump(self, fh, protocol=HIGHEST_PROTOCOL)
#
# @classmethod
# def load(cls, file) -> 'TestData':
# with lzma.open(file, 'rb') as fh:
# return pickle.load(fh)
#
# @classmethod
# def loads(cls, obj):
# return pickle.loads(obj)
#