diff --git a/.gitignore b/.gitignore
index c2ebdcd..429bef3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -69,5 +69,7 @@ target/
.vscode/
-# config file
-aiomql.json
\ No newline at end of file
+# config files
+config.json
+aiomql.json
+config/
\ No newline at end of file
diff --git a/README.md b/README.md
index 251f09a..d6ffe72 100644
--- a/README.md
+++ b/README.md
@@ -1,27 +1,33 @@
-# aiomql
+# Aiomql - Bot Building Framework and Asynchronous MetaTrader5 Library



-## 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.\
+[](https://www.buymeacoffee.com/ichingasamuel)
\ No newline at end of file
diff --git a/docs/account.md b/docs/account.md
index 678358f..8a8d0e6 100644
--- a/docs/account.md
+++ b/docs/account.md
@@ -1,46 +1,36 @@
-## 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)
+-
+
+
+### 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__
+
+#### __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
+
+#### 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
+
+#### refresh
+```python
+async def refresh()
+```
+Refreshes the account instance with the latest data from the MetaTrader 5 terminal
+
+
+#### 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
+
+#### symbols_get
```python
async def symbols_get() -> set[SymbolInfo]
```
diff --git a/docs/core/meta_trader.md b/docs/core/meta_trader.md
index e16ee1a..c864be5 100644
--- a/docs/core/meta_trader.md
+++ b/docs/core/meta_trader.md
@@ -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)
-## MetaTrader
+
+### 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, '')|
-### \_\_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.
+
+
+
+#### \_\_aenter\_\_
```python
async def __aenter__() -> 'MetaTrader'
```
@@ -54,13 +65,15 @@ Initializes the connection to the MetaTrader terminal.
|---|---|
|**MetaTrader**|An instance of the MetaTrader class|
-#### \_\_aexit\_\_
+
+#### \_\_aexit\_\_
```python
async def __aexit__(exc_type, exc_val, exc_tb)
```
Async context manager exit point. Closes the connection to the MetaTrader terminal.
-#### login
+
+#### 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.|
-#### initialize
+
+#### 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.|
-#### shutdown
+
+#### shutdown
```python
async def shutdown() -> None
```
Closes the connection to the MetaTrader terminal.
-#### version
+
+#### version
```python
async def version() -> tuple[int, int, str] | None
```
diff --git a/examples/bot.py b/examples/bot.py
index 975efe5..318983f 100644
--- a/examples/bot.py
+++ b/examples/bot.py
@@ -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()
\ No newline at end of file
diff --git a/examples/candles.py b/examples/candles.py
index 86480ed..4ab99ac 100644
--- a/examples/candles.py
+++ b/examples/candles.py
@@ -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)
diff --git a/examples/order.py b/examples/order.py
index 02644f2..1e19b78 100644
--- a/examples/order.py
+++ b/examples/order.py
@@ -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())
\ No newline at end of file
diff --git a/examples/positions_history.py b/examples/positions_history.py
index b65bc26..82ca894 100644
--- a/examples/positions_history.py
+++ b/examples/positions_history.py
@@ -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())
\ No newline at end of file
diff --git a/examples/symbol.py b/examples/symbol.py
index 52f1750..8c49e6f 100644
--- a/examples/symbol.py
+++ b/examples/symbol.py
@@ -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')
diff --git a/pyproject.toml b/pyproject.toml
index cc54a55..7c01362 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -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 = [
diff --git a/src/aiomql/__init__.py b/src/aiomql/__init__.py
index 3eb045d..6571a60 100644
--- a/src/aiomql/__init__.py
+++ b/src/aiomql/__init__.py
@@ -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 *
\ No newline at end of file
diff --git a/src/aiomql/account.py b/src/aiomql/account.py
index f749714..1db21e6 100644
--- a/src/aiomql/account.py
+++ b/src/aiomql/account.py
@@ -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()
diff --git a/src/aiomql/bot_builder.py b/src/aiomql/bot_builder.py
index 0d5b9d0..a487538 100644
--- a/src/aiomql/bot_builder.py
+++ b/src/aiomql/bot_builder.py
@@ -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)
diff --git a/src/aiomql/candle.py b/src/aiomql/candle.py
index 43e8190..7c1ed57 100644
--- a/src/aiomql/candle.py
+++ b/src/aiomql/candle.py
@@ -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
diff --git a/src/aiomql/core/base.py b/src/aiomql/core/base.py
index 77ecf10..84a4799 100644
--- a/src/aiomql/core/base.py
+++ b/src/aiomql/core/base.py
@@ -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)
\ No newline at end of file
+ logger.warning(err)
\ No newline at end of file
diff --git a/src/aiomql/core/config.py b/src/aiomql/core/config.py
index 19156f3..6a436d1 100644
--- a/src/aiomql/core/config.py
+++ b/src/aiomql/core/config.py
@@ -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:
diff --git a/src/aiomql/core/errors.py b/src/aiomql/core/errors.py
index 8854476..e648651 100644
--- a/src/aiomql/core/errors.py
+++ b/src/aiomql/core/errors.py
@@ -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')
diff --git a/src/aiomql/core/meta_trader.py b/src/aiomql/core/meta_trader.py
index 8b03328..155cffd 100644
--- a/src/aiomql/core/meta_trader.py
+++ b/src/aiomql/core/meta_trader.py
@@ -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
\ No newline at end of file
diff --git a/src/aiomql/history.py b/src/aiomql/history.py
index cafea13..50e0103 100644
--- a/src/aiomql/history.py
+++ b/src/aiomql/history.py
@@ -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
\ No newline at end of file
diff --git a/src/aiomql/lib/strategies/finger_trap.py b/src/aiomql/lib/strategies/finger_trap.py
index 6c160dd..779b611 100644
--- a/src/aiomql/lib/strategies/finger_trap.py
+++ b/src/aiomql/lib/strategies/finger_trap.py
@@ -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}
diff --git a/src/aiomql/lib/symbols/forex_symbol.py b/src/aiomql/lib/symbols/forex_symbol.py
index 16ea3d9..e2da628 100644
--- a/src/aiomql/lib/symbols/forex_symbol.py
+++ b/src/aiomql/lib/symbols/forex_symbol.py
@@ -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:
diff --git a/src/aiomql/lib/traders/simple_trader.py b/src/aiomql/lib/traders/simple_trader.py
index 4d65d51..0347eda 100644
--- a/src/aiomql/lib/traders/simple_trader.py
+++ b/src/aiomql/lib/traders/simple_trader.py
@@ -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")
\ No newline at end of file
+ logger.error(f"{err} in {self.__class__.__name__}.place_trade for {self.symbol.name}")
\ No newline at end of file
diff --git a/src/aiomql/order.py b/src/aiomql/order.py
index a883dba..63c2e08 100644
--- a/src/aiomql/order.py
+++ b/src/aiomql/order.py
@@ -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
\ No newline at end of file
diff --git a/src/aiomql/positions.py b/src/aiomql/positions.py
index 6a5d0ff..8f05950 100644
--- a/src/aiomql/positions.py
+++ b/src/aiomql/positions.py
@@ -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
\ No newline at end of file
+ return len([res for res in results if res.retcode == 10009])
\ No newline at end of file
diff --git a/src/aiomql/ram.py b/src/aiomql/ram.py
index ba9fb39..184a27e 100644
--- a/src/aiomql/ram.py
+++ b/src/aiomql/ram.py
@@ -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
\ No newline at end of file
+ return self.account.equity * self.risk
\ No newline at end of file
diff --git a/src/aiomql/records.py b/src/aiomql/records.py
index caef3ac..ef80c8e 100644
--- a/src/aiomql/records.py
+++ b/src/aiomql/records.py
@@ -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):
diff --git a/src/aiomql/result.py b/src/aiomql/result.py
index 5b77003..ee6c5da 100644
--- a/src/aiomql/result.py
+++ b/src/aiomql/result.py
@@ -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')
diff --git a/src/aiomql/sessions.py b/src/aiomql/sessions.py
index 4bbce6e..8b19b1e 100644
--- a/src/aiomql/sessions.py
+++ b/src/aiomql/sessions.py
@@ -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()
\ No newline at end of file
diff --git a/src/aiomql/strategy.py b/src/aiomql/strategy.py
index 4976d2b..63473cb 100644
--- a/src/aiomql/strategy.py
+++ b/src/aiomql/strategy.py
@@ -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})"
diff --git a/src/aiomql/symbol.py b/src/aiomql/symbol.py
index cf069cf..60be761 100644
--- a/src/aiomql/symbol.py
+++ b/src/aiomql/symbol.py
@@ -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):
diff --git a/src/aiomql/terminal.py b/src/aiomql/terminal.py
index 6a894de..165b016 100644
--- a/src/aiomql/terminal.py
+++ b/src/aiomql/terminal.py
@@ -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()
\ No newline at end of file
diff --git a/src/aiomql/ticks.py b/src/aiomql/ticks.py
index 729e009..0f7c53c 100644
--- a/src/aiomql/ticks.py
+++ b/src/aiomql/ticks.py
@@ -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)
\ No newline at end of file
diff --git a/src/aiomql/trader.py b/src/aiomql/trader.py
index 62ec5dd..ccf6099 100644
--- a/src/aiomql/trader.py
+++ b/src/aiomql/trader.py
@@ -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
diff --git a/src/aiomql/utils.py b/src/aiomql/utils.py
index eb50558..c69df22 100644
--- a/src/aiomql/utils.py
+++ b/src/aiomql/utils.py
@@ -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())
\ No newline at end of file
+ return f"{sep}".join(f"{key}: {value}" for key, value in data.items())
\ No newline at end of file
diff --git a/tests/__init__.py b/tests/__init__.py
deleted file mode 100644
index 7e57de2..0000000
--- a/tests/__init__.py
+++ /dev/null
@@ -1,10 +0,0 @@
-from aiomql import MetaTrader
-import pytest
-
-from .fixtures import *
-
-
-@pytest.mark.asyncio
-class BaseTest:
- """"""
- mt5 = MetaTrader()
diff --git a/tests/fixtures.py b/tests/fixtures.py
deleted file mode 100644
index 41fab9f..0000000
--- a/tests/fixtures.py
+++ /dev/null
@@ -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)
diff --git a/tests/test_config.json b/tests/test_config.json
deleted file mode 100644
index b5b54f6..0000000
--- a/tests/test_config.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
- "account_number": 160286827,
- "password": "TheN@me0fTheW!nd",
- "server": "ForexTimeFXTM-Demo01"
-}
\ No newline at end of file
diff --git a/tests/test_config.py b/tests/test_config.py
deleted file mode 100644
index 8866ccb..0000000
--- a/tests/test_config.py
+++ /dev/null
@@ -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
diff --git a/tests/test_constants.py b/tests/test_constants.py
deleted file mode 100644
index 32b82cd..0000000
--- a/tests/test_constants.py
+++ /dev/null
@@ -1,7 +0,0 @@
-from aiomql import TradeAction
-
-
-class TestConstants:
- def test_trade_action(self):
- assert TradeAction.DEAL == 1
-
diff --git a/tests/test_order.py b/tests/test_order.py
deleted file mode 100644
index e69de29..0000000
diff --git a/tests/test_symbol.py b/tests/test_symbol.py
deleted file mode 100644
index f5e3b44..0000000
--- a/tests/test_symbol.py
+++ /dev/null
@@ -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
diff --git a/tests/test_terminal.py b/tests/test_terminal.py
deleted file mode 100644
index 80568c8..0000000
--- a/tests/test_terminal.py
+++ /dev/null
@@ -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)
diff --git a/utils/meta_tester.py b/utils/meta_tester.py
deleted file mode 100644
index 6079a5d..0000000
--- a/utils/meta_tester.py
+++ /dev/null
@@ -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)
-#