From 63f22fc0810b9d519642c9747893253c04d05269 Mon Sep 17 00:00:00 2001 From: Ichinga Samuel Date: Thu, 18 Jan 2024 02:26:27 +0100 Subject: [PATCH] v3.15 --- .gitignore | 6 +- README.md | 95 +++++++++++-------- docs/account.md | 62 ++++++------ docs/core/meta_trader.md | 34 +++++-- examples/bot.py | 48 +++++----- examples/candles.py | 28 ++++-- examples/order.py | 6 +- examples/positions_history.py | 33 ++++--- examples/symbol.py | 6 +- pyproject.toml | 2 +- src/aiomql/__init__.py | 10 +- src/aiomql/account.py | 28 ++---- src/aiomql/bot_builder.py | 11 ++- src/aiomql/candle.py | 1 - src/aiomql/core/base.py | 38 ++------ src/aiomql/core/config.py | 28 +++--- src/aiomql/core/errors.py | 1 + src/aiomql/core/meta_trader.py | 114 +++++++++++------------ src/aiomql/history.py | 20 ++-- src/aiomql/lib/strategies/finger_trap.py | 2 +- src/aiomql/lib/symbols/forex_symbol.py | 2 +- src/aiomql/lib/traders/simple_trader.py | 38 +++----- src/aiomql/order.py | 11 ++- src/aiomql/positions.py | 13 +-- src/aiomql/ram.py | 13 ++- src/aiomql/records.py | 6 +- src/aiomql/result.py | 4 +- src/aiomql/sessions.py | 2 +- src/aiomql/strategy.py | 17 ++-- src/aiomql/symbol.py | 11 ++- src/aiomql/terminal.py | 4 +- src/aiomql/ticks.py | 16 ++-- src/aiomql/trader.py | 29 +++--- src/aiomql/utils.py | 3 +- tests/__init__.py | 10 -- tests/fixtures.py | 31 ------ tests/test_config.json | 5 - tests/test_config.py | 13 --- tests/test_constants.py | 7 -- tests/test_order.py | 0 tests/test_symbol.py | 11 --- tests/test_terminal.py | 22 ----- utils/meta_tester.py | 91 ------------------ 43 files changed, 387 insertions(+), 545 deletions(-) delete mode 100644 tests/__init__.py delete mode 100644 tests/fixtures.py delete mode 100644 tests/test_config.json delete mode 100644 tests/test_config.py delete mode 100644 tests/test_constants.py delete mode 100644 tests/test_order.py delete mode 100644 tests/test_symbol.py delete mode 100644 tests/test_terminal.py delete mode 100644 utils/meta_tester.py 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 ![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) \ 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) -#