This commit is contained in:
Ichinga Samuel
2024-01-18 02:26:27 +01:00
parent 4b5f267509
commit 63f22fc081
43 changed files with 387 additions and 545 deletions
+3 -7
View File
@@ -1,3 +1,4 @@
from .core import *
from .account import Account
from .ram import RAM
from .symbol import Symbol
@@ -14,10 +15,5 @@ from .history import History
from .trader import Trader
from .terminal import Terminal
from .sessions import Session, Sessions
from .core.config import Config
from .core.constants import *
from .core.meta_trader import MetaTrader
from .core.models import *
from .core.exceptions import *
from .lib import *
from .utils import dict_to_string
from .lib import *
+10 -18
View File
@@ -1,5 +1,4 @@
from logging import getLogger
from typing import Type
from .core.models import AccountInfo, SymbolInfo
from .core.exceptions import LoginError
@@ -18,6 +17,7 @@ class Account(AccountInfo):
Notes:
Other Account properties are defined in the AccountInfo class.
"""
_instance: 'Account'
connected: bool
symbols = set()
@@ -26,27 +26,18 @@ class Account(AccountInfo):
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self, **kwargs):
super().__init__(**kwargs)
if not self.login:
acc = self.config.account_info()
self.set_attributes(**acc)
async def refresh(self):
"""Refreshes the account instance with the latest account details from the MetaTrader 5 terminal"""
account_info = await self.mt5.account_info()
acc = account_info._asdict()
self.set_attributes(**acc)
@property
def account_info(self) -> dict:
"""Get account login, server and password details. If the login attribute of the account instance returns
a falsy value, the config instance is used to get the account details.
Returns:
dict: A dict of login, server and password details
Note:
This method will only look for config details in the config instance if the login attribute of the
account Instance returns a falsy value
"""
acc_info = self.get_dict(include={'login', 'server', 'password'})
return acc_info if acc_info['login'] else self.config.account_info()
async def __aenter__(self) -> 'Account':
"""Connect to a trading account and return the account instance.
Async context manager for the Account class.
@@ -72,8 +63,9 @@ class Account(AccountInfo):
Returns:
bool: True if login was successful else False
"""
await self.mt5.initialize(**self.account_info)
self.connected = await self.mt5.login(**self.account_info)
acc = self.get_dict(include={'login', 'server', 'password'})
await self.mt5.initialize(**acc, path=self.config.path)
self.connected = await self.mt5.login(**acc)
if self.connected:
await self.refresh()
self.symbols = await self.symbols_get()
+9 -2
View File
@@ -4,6 +4,7 @@ import logging
from .executor import Executor
from .account import Account
from .core.config import Config
from .symbol import Symbol as _Symbol
from .strategy import Strategy as _Strategy
@@ -20,11 +21,17 @@ class Bot:
account (Account): Account Object.
executor: The default thread executor.
symbols (list[Symbols]): A set of symbols for the trading session
"""
config (Config): Config instance
account: Account = Account()
"""
config: Config
account: Account
symbols: set
executor: Executor
def __init__(self):
self.config = Config()
self.account = Account()
self.symbols = set()
self.executor = Executor(bot=self)
-1
View File
@@ -2,7 +2,6 @@
from typing import Type, TypeVar, Generic, Iterable
from logging import getLogger
import reprlib
from pandas import DataFrame, Series
import pandas_ta as ta
+9 -29
View File
@@ -16,16 +16,15 @@ class Base:
Keyword Args:
**kwargs: Object attributes and values as keyword arguments. Only added if they are annotated on the class body.
Class Attributes:
mt5 (MetaTrader): An instance of the MetaTrader class
config (Config): An instance of the Config class
Meta (Type[Meta]): The Meta class for configuration of the data model class
"""
mt5: MetaTrader = MetaTrader()
config = Config()
mt5: MetaTrader
config: Config
def __init__(self, **kwargs):
self.config = Config()
self.mt5 = MetaTrader()
self.exclude = {'mt5', "config", 'exclude', 'include', 'annotations', 'class_vars', 'dict'}
self.include = set()
self.set_attributes(**kwargs)
def __repr__(self):
@@ -114,27 +113,8 @@ class Base:
dict: A dictionary of instance and class attributes
"""
try:
_filter = self.exclude.difference(self.include)
return {key: value for key, value in (self.class_vars | self.__dict__).items() if
key not in self.Meta.filter}
key not in _filter}
except Exception as err:
logger.warning(err)
class Meta:
"""A class for defining class attributes to be excluded or included in the dict property
Attributes:
exclude (set): A set of attributes to be excluded
include (set): Specific attributes to be returned. Include supercedes exclude.
"""
exclude = {'mt5', "Config"}
include = set()
@classmethod
@property
def filter(cls) -> set:
"""Combine the exclude and include attributes to return a set of attributes to be excluded.
Returns:
set: A set of attributes to be excluded
"""
return cls.exclude.difference(cls.include)
logger.warning(err)
+16 -12
View File
@@ -25,7 +25,7 @@ class Config:
server (str): Broker server
path (str): Path to terminal file
timeout (int): Timeout for terminal connection
_initialize (bool): First time initialization flag
Notes:
By default, the config class looks for a file named aiomql.json.
You can change this by passing the filename keyword argument to the constructor.
@@ -38,10 +38,11 @@ class Config:
path: str = ""
timeout: int = 60000
record_trades: bool = True
filename: str = "aiomql.json"
filename: str
win_percentage: float = 0.85
records_dir = Path.home() / "Documents" / "Aiomql" / "Trade Records"
_load = 1
config_dir: str = ''
_initialize = True
def __new__(cls, *args, **kwargs):
if not hasattr(cls, "_instance"):
@@ -49,8 +50,10 @@ class Config:
return cls._instance
def __init__(self, **kwargs):
self.load_config(reload=False)
[setattr(self, key, value) for key, value in kwargs]
self.filename = kwargs.pop('filename', "aiomql.json")
self.config_dir = kwargs.pop('config_dir', '')
self.load_config(reload=kwargs.pop('reload', False))
[setattr(self, key, value) for key, value in kwargs.items()]
@staticmethod
def walk_to_root(path: str) -> Iterator[str]:
@@ -76,6 +79,7 @@ class Config:
frame = frame.f_back
frame_filename = frame.f_code.co_filename
path = os.path.dirname(os.path.abspath(frame_filename))
path = os.path.join(path, self.config_dir) if self.config_dir else path
for dirname in self.walk_to_root(path):
check_path = os.path.join(dirname, self.filename)
@@ -83,14 +87,14 @@ class Config:
return check_path
return None
def load_config(self, file: str = None, reload: bool = True):
if reload:
self._load = 1
if self._load != 1:
def load_config(self, file: str = None, reload: bool = True, filename: str = None, config_dir: str = ''):
"""Load configuration settings from a file."""
if not (self._initialize or reload):
return
self._load = 0
self._initialize = False
data = {}
self.filename = filename or self.filename
self.config_dir = config_dir or self.config_dir
if (file := (file or self.find_config())) is None:
logger.warning("No Config File Found")
else:
@@ -100,7 +104,7 @@ class Config:
[setattr(self, key, value) for key, value in data.items()]
self.records_dir.mkdir(parents=True, exist_ok=True) if self.records_dir else ...
def account_info(self) -> dict["login", "password", "server"]:
def account_info(self) -> dict[str, int | str]:
"""Returns Account login details as found in the config object if available
Returns:
+1
View File
@@ -19,6 +19,7 @@ class Error:
-10004: 'internal IPC no ipc',
-10005: 'internal timeout',
}
def __init__(self, code: int, description: str = ''):
self.code = code
self.description = description or self.descriptions.get(code, 'Unknown Error')
+56 -58
View File
@@ -56,6 +56,11 @@ class MetaTrader(metaclass=BaseMeta):
_symbols_total: Callable
_terminal_info: Callable
_version: Callable
error: Error
config: Config
def __init__(self):
self.config = Config()
async def __aenter__(self) -> 'MetaTrader':
"""
@@ -120,33 +125,37 @@ class MetaTrader(metaclass=BaseMeta):
return await asyncio.to_thread(self._shutdown)
async def last_error(self) -> tuple[int, str]:
return await asyncio.to_thread(self._last_error)
try:
return await asyncio.to_thread(self._last_error)
except Exception as err:
logger.warning(f'Error in obtaining last error.')
return 0, str(err)
async def version(self) -> tuple[int, int, str] | None:
""""""
res = await asyncio.to_thread(self._version)
if res is None:
err = await self.last_error()
logger.warning(f'Error in obtaining version information.{Error(*err)}')
self.error = Error(*err)
logger.warning(f'Error in obtaining version information.{self.error.description}')
return res
async def account_info(self) -> AccountInfo | None:
""""""
res = await asyncio.to_thread(self._account_info)
if res is None:
err = await self.last_error()
logger.warning(f'Error in obtaining account information.{Error(*err)}')
self.error = Error(*err)
logger.warning(f'Error in obtaining account information.{self.error.description}')
return res
async def terminal_info(self) -> TerminalInfo | None:
res = await asyncio.to_thread(self._terminal_info)
if res is None:
err = await self.last_error()
logger.warning(f'Error in obtaining terminal information.{Error(*err)}')
self.error = Error(*err)
logger.warning(f'Error in obtaining terminal information.{self.error.description}')
return res
return res
async def symbols_total(self) -> int:
@@ -155,32 +164,29 @@ class MetaTrader(metaclass=BaseMeta):
async def symbols_get(self, group: str = "") -> tuple[SymbolInfo] | None:
kwargs = {'group': group} if group else {}
res = await asyncio.to_thread(self._symbols_get, **kwargs)
if res is None:
err = await self.last_error()
logger.warning(f'Error in obtaining symbols.{Error(*err)}')
self.error = Error(*err)
logger.warning(f'Error in obtaining symbols.{self.error.description}')
return res
return res
async def symbol_info(self, symbol: str) -> SymbolInfo | None:
res = await asyncio.to_thread(self._symbol_info, symbol)
if res is None:
err = await self.last_error()
logger.warning(f'Error in obtaining information for {symbol}.{Error(*err)}')
self.error = Error(*err)
logger.warning(f'Error in obtaining information for {symbol}.{self.error.description}')
return res
return res
async def symbol_info_tick(self, symbol: str) -> Tick | None:
res = await asyncio.to_thread(self._symbol_info_tick, symbol)
if res is None:
err = await self.last_error()
logger.warning(f'Error in obtaining tick for {symbol}.{Error(*err)}')
self.error = Error(*err)
logger.warning(f'Error in obtaining tick for {symbol}.{self.error.description}')
return res
return res
async def symbol_select(self, symbol: str, enable: bool) -> bool:
@@ -191,23 +197,22 @@ class MetaTrader(metaclass=BaseMeta):
async def market_book_get(self, symbol: str) -> tuple[BookInfo] | None:
res = await asyncio.to_thread(self._market_book_get, symbol)
if res is None:
err = await self.last_error()
logger.warning(f'Error in obtaining market depth content for {symbol}.{Error(*err)}')
self.error = Error(*err)
logger.warning(f'Error in obtaining market depth content for {symbol}.{self.error.description}')
return res
return res
async def market_book_release(self, symbol: str) -> bool:
return await asyncio.to_thread(self._market_book_release, symbol)
async def copy_rates_from(self, symbol: str, timeframe: TimeFrame, date_from: datetime | int, count: int):
async def copy_rates_from(self, symbol: str, timeframe: TimeFrame, date_from: datetime | float, count: int):
res = await asyncio.to_thread(self._copy_rates_from, symbol, timeframe, date_from, count)
if res is None:
err = await self.last_error()
logger.warning(f'Error in obtaining rates for {symbol}.{Error(*err)}')
self.error = Error(*err)
logger.warning(f'Error in obtaining rates for {symbol}.{self.error.description}')
return res
return res
@@ -215,39 +220,37 @@ class MetaTrader(metaclass=BaseMeta):
res = await asyncio.to_thread(self._copy_rates_from_pos, symbol, timeframe, start_pos, count)
if res is None:
err = await self.last_error()
logger.warning(f'Error in obtaining rates for {symbol}.{Error(*err)}')
self.error = Error(*err)
logger.warning(f'Error in obtaining rates for {symbol}.{self.error.description}')
return res
return res
async def copy_rates_range(self, symbol: str, timeframe: TimeFrame, date_from: datetime | int,
date_to: datetime | int):
async def copy_rates_range(self, symbol: str, timeframe: TimeFrame, date_from: datetime | float,
date_to: datetime | float):
res = await asyncio.to_thread(self._copy_rates_range, symbol, timeframe, date_from, date_to)
if res is None:
err = await self.last_error()
logger.warning(f'Error in obtaining rates for {symbol}.{Error(*err)}')
self.error = Error(*err)
logger.warning(f'Error in obtaining rates for {symbol}.{self.error.description}')
return res
return res
async def copy_ticks_from(self, symbol: str, date_from: datetime | int, count: int, flags: CopyTicks):
async def copy_ticks_from(self, symbol: str, date_from: datetime | float, count: int, flags: CopyTicks):
res = await asyncio.to_thread(self._copy_ticks_from, symbol, date_from, count, flags)
if res is None:
err = await self.last_error()
logger.warning(f'Error in obtaining ticks for {symbol}.{Error(*err)}')
self.error = Error(*err)
logger.warning(f'Error in obtaining ticks for {symbol}.{self.error.description}')
return res
return res
async def copy_ticks_range(self, symbol: str, date_from: datetime | int, date_to: datetime | int, flags: CopyTicks):
async def copy_ticks_range(self, symbol: str, date_from: datetime | float, date_to: datetime | float, flags: CopyTicks):
res = await asyncio.to_thread(self._copy_ticks_range, symbol, date_from, date_to, flags)
if res is None:
err = await self.last_error()
logger.warning(f'Error in obtaining ticks for {symbol}.{Error(*err)}')
self.error = Error(*err)
logger.warning(f'Error in obtaining ticks for {symbol}.{self.error.description}')
return res
return res
async def orders_total(self) -> int:
@@ -270,33 +273,30 @@ class MetaTrader(metaclass=BaseMeta):
"""
kwargs = {key: value for key, value in (('group', group), ('ticket', ticket), ('symbol', symbol)) if value}
res = await asyncio.to_thread(self._orders_get, **kwargs)
if res is None:
err = await self.last_error()
logger.warning(f'Error in obtaining orders.{Error(*err)}')
self.error = Error(*err)
logger.warning(f'Error in obtaining orders.{self.error.description}')
return res
return res
async def order_calc_margin(self, action: OrderType, symbol: str, volume: float, price: float) -> float | None:
res = await asyncio.to_thread(self._order_calc_margin, action, symbol, volume, price)
if res is None:
err = await self.last_error()
logger.warning(f'Error in calculating margin.{Error(*err)}')
self.error = Error(*err)
logger.warning(f'Error in calculating margin.{self.error.description}')
return res
return res
async def order_calc_profit(self, action: OrderType, symbol: str, volume: float, price_open: float,
price_close: float) -> float | None:
res = await asyncio.to_thread(self._order_calc_profit, action, symbol, volume, price_open, price_close)
if res is None:
err = await self.last_error()
logger.warning(f'Error in calculating profit.{Error(*err)}')
self.error = Error(*err)
logger.warning(f'Error in calculating profit.{self.error.description}')
return res
return res
async def order_check(self, request: dict) -> OrderCheckResult:
@@ -311,41 +311,39 @@ class MetaTrader(metaclass=BaseMeta):
async def positions_get(self, group: str = "", ticket: int = 0, symbol: str = "") -> tuple[TradePosition] | None:
kwargs = {key: value for key, value in (('group', group), ('ticket', ticket), ('symbol', symbol)) if value}
res = await asyncio.to_thread(self._positions_get, **kwargs)
if res is None:
err = await self.last_error()
logger.warning(f'Error in obtaining open positions.{Error(*err)}')
self.error = Error(*err)
logger.warning(f'Error in obtaining open positions.{self.error.description}')
return res
return res
async def history_orders_total(self, date_from: datetime | int, date_to: datetime | int) -> int:
async def history_orders_total(self, date_from: datetime | float, date_to: datetime | float) -> int:
return await asyncio.to_thread(self._history_orders_total, date_from, date_to)
async def history_orders_get(self, date_from: datetime | int = None, date_to: datetime | int = None, group: str = '',
async def history_orders_get(self, date_from: datetime | float = None, date_to: datetime | float = None, group: str = '',
ticket: int = 0, position: int = 0) -> tuple[TradeOrder] | None:
kwargs = {key: value for key, value in (('date_from', date_from), ('date_to', date_to), ('group', group),
('ticket', ticket), ('position', position)) if value}
res = await asyncio.to_thread(self._history_orders_get, **kwargs)
if res is None:
err = await self.last_error()
logger.warning(f'Error in getting orders.{Error(*err)}')
self.error = Error(*err)
logger.warning(f'Error in getting orders.{self.error.description}')
return res
return res
async def history_deals_total(self, date_from: datetime | int, date_to: datetime | int) -> int:
async def history_deals_total(self, date_from: datetime | float, date_to: datetime | float) -> int:
return await asyncio.to_thread(self._history_deals_total, date_from, date_to)
async def history_deals_get(self, date_from: datetime | int = None, date_to: datetime | int = None, group: str = '',
ticket: int = 0, position: int = 0) -> tuple[TradeDeal] | None:
async def history_deals_get(self, date_from: datetime | float = None, date_to: datetime | float = None,
group: str = '', ticket: int = 0, position: int = 0) -> tuple[TradeDeal] | None:
kwargs = {key: value for key, value in (('date_from', date_from), ('date_to', date_to), ('group', group),
('ticket', ticket), ('position', position)) if value}
res = await asyncio.to_thread(self._history_deals_get, **kwargs)
if res is None:
err = await self.last_error()
logger.warning(f'Error in getting deals.{Error(*err)}')
self.error = Error(*err)
logger.warning(f'Error in getting deals.{self.error.description}')
return res
return res
+12 -8
View File
@@ -24,8 +24,8 @@ class History:
mt5 (MetaTrader): MetaTrader instance
config (Config): Config instance
"""
mt5: MetaTrader = MetaTrader()
config: Config = Config()
mt5: MetaTrader
config: Config
def __init__(self, *, date_from: datetime | float = None, date_to: datetime | float = None,
group: str = "", ticket: int = 0, position: int = 0):
@@ -41,6 +41,8 @@ class History:
ticket (int): Filter for selecting history by ticket number
position (int): Filter for selecting history deals by position
"""
self.config = Config()
self.mt5 = MetaTrader()
self.date_from = date_from
self.date_to = date_to
self.group = group
@@ -77,11 +79,12 @@ class History:
"""
deals = await self.mt5.history_deals_get(date_from=self.date_from, date_to=self.date_to, position=self.position,
group=self.group, ticket=self.ticket)
if deals is not None:
self.deals = [TradeDeal(**deal._asdict()) for deal in deals] if deals else []
self.total_deals = len(self.deals)
return self.deals
if deals is None:
logger.warning(f'Failed to get deals due to {self.mt5.error.description}')
deals = []
self.deals = [TradeDeal(**deal._asdict()) for deal in deals]
self.total_deals = len(self.deals)
return self.deals
async def deals_total(self) -> int:
@@ -103,7 +106,8 @@ class History:
orders = await self.mt5.history_orders_get(date_from=self.date_from, date_to=self.date_to, group=self.group,
position=self.position, ticket=self.ticket)
if orders is None:
return self.orders
logger.warning(f'Failed to get orders due to {self.mt5.error.description}')
orders = []
self.orders = [TradeOrder(**order._asdict()) for order in orders]
self.total_orders = len(self.orders)
@@ -116,4 +120,4 @@ class History:
int: Total number of orders
"""
self.total_orders = await self.mt5.history_orders_total(self.date_from, self.date_to)
return self.total_orders
return self.total_orders
+1 -1
View File
@@ -25,7 +25,7 @@ class FingerTrap(Strategy):
trend_candles_count: int
trader: Trader
tracker: Tracker
_parameters = {"trend": 3, "fast_period": 8, "slow_period": 34, "entry_time_frame": TimeFrame.M5,
parameters = {"trend": 3, "fast_period": 8, "slow_period": 34, "entry_time_frame": TimeFrame.M5,
"trend_time_frame": TimeFrame.H1, "entry_period": 8,
"trend_candles_count": 48, "entry_candles_count": 50}
+1 -1
View File
@@ -12,7 +12,7 @@ class ForexSymbol(Symbol):
Args:
amount (float): Amount to risk. Given in terms of the account currency.
points (float): Target pips.
points (float): Target points.
use_limits (bool): If True, the computed volume checked against the maximum and minimum volume.
Returns:
+15 -23
View File
@@ -1,5 +1,3 @@
"""Trader class module. Handles the creation of an order and the placing of trades"""
from logging import getLogger
from ..symbols import ForexSymbol
@@ -13,49 +11,43 @@ logger = getLogger(__name__)
class SimpleTrader(Trader):
"""A simple trader class. Limits the number of loosing trades per symbol"""
def __init__(self, *, symbol: ForexSymbol, ram: RAM = None, num_trades: int = 1):
def __init__(self, *, symbol: ForexSymbol, ram: RAM = None, loss_limit: int = 3):
"""Initializes the order object and RAM instance
The default risk to reward ratio is 1:1.
Args:
symbol (Symbol): Financial instrument
ram (RAM): Risk Assessment and Management instance
num_trades (int): Number of open trades in loosing positions to allow per symbol
loss_limit (int): Maximum number of losing trades allowed at a time.
"""
ram = ram or RAM(risk_to_reward=1, points=100)
super().__init__(symbol=symbol, ram=ram)
self.positions = Positions(symbol=symbol.name)
self.num_trades = num_trades
self.loss_limit = loss_limit
async def create_order(self, *, order_type: OrderType, points: float = 0):
async def create_order(self, *, order_type: OrderType):
"""Complete the order object with the required values. Creates a simple order.
Args:
order_type (OrderType): Type of order
points (float): Target points
"""
positions = await self.positions.positions_get()
positions.sort(key=lambda pos: pos.time_msc)
positions = await Positions().positions_get()
loosing = [trade for trade in positions if trade.profit < 0]
if (losses := len(loosing)) > self.num_trades:
if (losses := len(loosing)) > self.loss_limit:
raise RuntimeError(f"Last {losses} trades in a losing position")
points = points or self.symbol.trade_stops_level * 2
amount = self.ram.amount or await self.ram.get_amount()
points = self.ram.points or self.symbol.trade_stops_level * 3
amount = await self.ram.get_amount()
self.order.volume = await self.symbol.compute_volume(amount=amount, points=points)
self.order.type = order_type
self.order.comment = self.parameters.get('name', '')
await self.set_trade_stop_levels(points=points)
async def place_trade(self, order_type: OrderType, parameters: dict = None, points: float = 0):
"""Places a trade based on the order_type.
Args:
order_type (OrderType): Type of order
parameters: parameters of the trading strategy used to place the trade
points (float): Target points
"""
async def place_trade(self, order_type: OrderType, parameters: dict = None):
"""Places a trade based on the order_type."""
try:
self.parameters |= parameters or {}
await self.create_order(order_type=order_type, points=points)
await self.create_order(order_type=order_type)
if not await self.check_order():
return
await self.send_order()
except Exception as err:
logger.error(f"{err}. Symbol: {self.order.symbol}\n {self.__class__.__name__}.place_trade")
logger.error(f"{err} in {self.__class__.__name__}.place_trade for {self.symbol.name}")
+6 -5
View File
@@ -51,6 +51,8 @@ class Order(TradeRequest):
tuple[TradeOrder]: A Tuple of active trade orders as TradeOrder objects
"""
orders = await self.mt5.orders_get(symbol=self.symbol)
if orders is None:
raise OrderError(f'Failed to get orders for {self.symbol} due to {self.mt5.error.description}')
orders = (TradeOrder(**order._asdict()) for order in orders)
return tuple(orders)
@@ -65,7 +67,7 @@ class Order(TradeRequest):
"""
res = await self.mt5.order_check(self.dict)
if res is None:
raise OrderError(f'Failed to check order {self.symbol} {self.type} {self.volume} {self.price} {res}')
raise OrderError(f'Failed to check order due to {self.mt5.error.description}')
return OrderCheckResult(**res._asdict())
async def send(self) -> OrderSendResult:
@@ -79,7 +81,7 @@ class Order(TradeRequest):
"""
res = await self.mt5.order_send(self.dict)
if res is None:
raise OrderError(f'Failed to send order {self.symbol} {self.type} {self.volume} {self.price}')
raise OrderError(f'Failed to send order {self.symbol} due to {self.mt5.error.description}')
return OrderSendResult(**res._asdict())
async def calc_margin(self) -> float:
@@ -93,7 +95,7 @@ class Order(TradeRequest):
"""
res = await self.mt5.order_calc_margin(self.type, self.symbol, self.volume, self.price)
if res is None:
raise OrderError(f'Failed to calculate margin for {self.symbol} {self.type} {self.volume} {self.price} {res}')
raise OrderError(f'Failed to calculate margin for {self.symbol} due to {self.mt5.error.description}')
return res
async def calc_profit(self) -> float:
@@ -107,6 +109,5 @@ class Order(TradeRequest):
"""
res = await self.mt5.order_calc_profit(self.type, self.symbol, self.volume, self.price, self.tp)
if res is None:
raise OrderError(
f'Failed to calculate profit for {self.symbol} {self.type} {self.volume} {self.price} {self.tp}')
raise OrderError(f'Failed to calculate profit for {self.symbol} due to {self.mt5.error.description}')
return res
+7 -6
View File
@@ -18,7 +18,7 @@ class Positions:
ticket (int): Position ticket.
mt5 (MetaTrader): MetaTrader instance.
"""
mt5: MetaTrader = MetaTrader()
mt5: MetaTrader
def __init__(self, *, symbol: str = "", group: str = "", ticket: int = 0):
"""Get Open Positions.
@@ -30,6 +30,7 @@ class Positions:
ticket (int): Position ticket
"""
self.mt5 = MetaTrader()
self.symbol = symbol
self.group = group
self.ticket = ticket
@@ -42,7 +43,7 @@ class Positions:
"""
return await self.mt5.positions_total()
async def positions_get(self, symbol: str = '', group: str = '', ticket: int = 0):
async def positions_get(self, symbol: str = '', group: str = '', ticket: int = 0) -> list[TradePosition]:
"""Get open positions with the ability to filter by symbol or ticket.
Keyword Args:
@@ -56,8 +57,9 @@ class Positions:
"""
positions = await self.mt5.positions_get(group=group or self.group, symbol=symbol or self.symbol,
ticket=ticket or self.ticket)
if not positions:
return []
if positions is None:
logger.warning(f'Failed to get positions for {symbol or self.symbol} due to {self.mt5.error.description}')
positions = []
return [TradePosition(**pos._asdict()) for pos in positions]
async def close(self, *, ticket: int, symbol: str, price: float, volume: float, order_type: OrderType):
@@ -84,5 +86,4 @@ class Positions:
symbol=pos.symbol) for pos in positions]
results = await asyncio.gather(*[order for order in orders], return_exceptions=True)
amount_closed = len([res for res in results if res.retcode == 10009])
return amount_closed
return len([res for res in results if res.retcode == 10009])
+6 -7
View File
@@ -3,12 +3,14 @@ from .account import Account
class RAM:
account: Account = Account()
account: Account
risk_to_reward: float
risk: float
amount: float
points: float
pips: float
min_amount: float
max_amount: float
def __init__(self, *, risk_to_reward: float = 1, risk: float = 0.01, amount: float = 0, **kwargs):
"""Initialize Risk Assessment and Management with the provided keyword arguments.
@@ -22,17 +24,14 @@ class RAM:
self.risk_to_reward = risk_to_reward
self.risk = risk
self.amount = amount
self.account = Account()
[setattr(self, key, value) for key, value in kwargs.items()]
async def get_amount(self, risk: float = 0) -> float:
async def get_amount(self) -> float:
"""Calculate the amount to risk per trade as a percentage of equity.
Keyword Args:
risk (float): Percentage of account balance to risk per trade. Defaults to zero.
Returns:
float: Amount to risk per trade
"""
await self.account.refresh()
risk = risk or self.risk
return self.account.equity * risk
return self.account.equity * self.risk
+4 -2
View File
@@ -18,8 +18,8 @@ class Records:
records_dir(Path): Path to directory containing record of placed trades, If not given takes the default
from the config
"""
config: Config = Config()
mt5: MetaTrader = MetaTrader()
config: Config
mt5: MetaTrader
def __init__(self, records_dir: Path = ''):
"""Initialize the Records class. The main method of this class is update_records which you should call to update
@@ -28,6 +28,8 @@ class Records:
Keyword Args:
records_dir (Path): Path to directory containing record of placed trades.
"""
self.config = Config()
self.mt5 = MetaTrader()
self.records_dir = records_dir or self.config.records_dir
async def get_records(self):
+2 -2
View File
@@ -1,4 +1,3 @@
import asyncio
import csv
from logging import getLogger
@@ -16,7 +15,7 @@ class Result:
config (Config): The configuration object
name: Any desired name for the result file object
"""
config = Config()
config: Config
def __init__(self, result: OrderSendResult, parameters: dict = None, name: str = ''):
"""
@@ -26,6 +25,7 @@ class Result:
parameters:
name:
"""
self.config = Config()
self.parameters = parameters or {}
self.result = result
self.name = name or parameters.get('name', 'Trades')
+1 -1
View File
@@ -209,7 +209,7 @@ class Sessions:
await self.current_session.close() if self.current_session else ...
current_session = self.find_next(now)
secs = current_session.until() + 10
print(f'sleeping for {secs} seconds until next {current_session} session')
logger.info(f'sleeping for {secs} seconds until next {current_session} session')
await sleep(secs)
self.current_session = current_session
await self.current_session.begin()
+6 -11
View File
@@ -7,7 +7,6 @@ from datetime import time as dtime
from .core.meta_trader import MetaTrader
from .symbol import Symbol as _Symbol
from .account import Account
from .core import Config
from .sessions import Sessions, Session
@@ -23,21 +22,15 @@ class Strategy(ABC):
parameters (Dict): A dictionary of parameters for the strategy.
sessions (Sessions): The sessions to use for the strategy.
Class Attributes:
account (Account): Account instance.
mt5 (MetaTrader): MetaTrader instance.
config (Config): Config instance.
Notes:
Define the name of a strategy as a class attribute. If not provided, the class name will be used as the name.
"""
name: str
symbol: Symbol
sessions: Sessions
account = Account()
mt5: MetaTrader()
config = Config()
_parameters = {}
mt5: MetaTrader
config: Config
parameters = {}
def __init__(self, *, symbol: Symbol, params: dict = None, sessions: Sessions = None, name=''):
"""Initiate the parameters dict and add name and symbol fields.
@@ -47,12 +40,14 @@ class Strategy(ABC):
symbol (Symbol): The Financial instrument
params (Dict): Trading strategy parameters
"""
self.parameters = self._parameters | (params or {})
self.parameters = self.parameters | (params or {})
self.symbol = symbol
self.name = name or self.__class__.__name__
self.parameters["symbol"] = symbol.name
self.parameters["name"] = self.name
self.sessions = sessions or Sessions(Session(start=0, end=dtime(hour=23, minute=59, second=59)))
self.config = Config()
self.mt5 = MetaTrader()
def __repr__(self):
return f"{self.name}({self.symbol!r})"
+10 -1
View File
@@ -26,7 +26,16 @@ class Symbol(SymbolInfo):
Make sure Symbol is always initialized with a name argument
"""
tick: Tick
account = Account()
account: Account
def __init__(self, **kwargs):
"""Initialize the Symbol object with the name of the financial instrument.
Args:
name (str): Name of the financial instrument
"""
super().__init__(**kwargs)
self.account = Account()
@property
def pip(self):
+2 -2
View File
@@ -4,7 +4,7 @@ from typing import NamedTuple
from logging import getLogger
from .core.models import TerminalInfo
logger = getLogger()
logger = getLogger(__name__)
class Terminal(TerminalInfo):
@@ -67,4 +67,4 @@ class Terminal(TerminalInfo):
Returns:
int: Total number of available symbols
"""
return await self.mt5.symbols_total()
return await self.mt5.symbols_total()
+9 -7
View File
@@ -1,7 +1,6 @@
"""Module for working with price ticks."""
from typing import TypeVar, Iterable
import reprlib
from pandas import DataFrame, Series
import pandas_ta as ta
@@ -31,27 +30,30 @@ class Tick:
ask: float
last: float
volume: float
time_msc:float
time_msc: float
flags: float
volume_real:float
volume_real: float
Index: int
def __init__(self, **kwargs):
self.time = kwargs.pop('time', 0)
self.Index = kwargs.pop('Index', 0)
self.set_attributes(**kwargs)
def __repr__(self):
keys = reprlib.repr(', '.join('%s=%s' % (i, j) for i, j in self.__dict__.items()))[1:-1]
return '%(class)s(%(args)s)' % {'class': self.__class__.__name__, 'args': keys}
return ("%(class)s(Index=%(Index)s, time=%(time)s, bid=%(bid)s, ask=%(ask)s, last=%(last)s, volume=%(volume)s,"
" mid=%(mid)s)") % {"class": self.__class__.__name__, "time": self.time, "bid": self.bid,
"ask": self.ask, "last": self.last, "volume": self.volume, 'Index': self.Index}
def set_attributes(self, **kwargs):
"""Set attributes from keyword arguments"""
for key, value in kwargs.items():
setattr(self, key, value)
_Ticks = TypeVar('_Ticks', bound='Ticks')
class Ticks:
"""Container data class for price ticks. Arrange in chronological order.
Supports iteration, slicing and assignment
@@ -164,4 +166,4 @@ class Ticks:
None: If inplace is True
"""
res = self._data.rename(columns=kwargs, inplace=inplace)
return res if inplace else self.__class__(data=res)
return res if inplace else self.__class__(data=res)
+16 -13
View File
@@ -28,7 +28,7 @@ class Trader(ABC):
Class Attributes:
config (Config): Config instance.
"""
config = Config()
config: Config
def __init__(self, *, symbol: Symbol, ram: RAM = None):
"""Initializes the order object and RAM instance
@@ -37,6 +37,7 @@ class Trader(ABC):
symbol (Symbol): Financial instrument
ram (RAM): Risk Assessment and Management instance
"""
self.config = Config()
self.symbol = symbol
self.order = Order(symbol=symbol.name)
self.ram = ram or RAM()
@@ -92,39 +93,41 @@ class Trader(ABC):
"""
check = await self.order.check()
if check.retcode != 0:
logger.warning(f"Symbol: {self.order.symbol}\nResult:\n"
f"{dict_to_string(check.get_dict(include={'comment', 'retcode'}), multi=True)}")
logger.warning(f"""Unable to place order for {self.symbol}\n
{dict_to_string(check.get_dict(include={'comment', 'retcode'}) | check.request._asdict(), multi=True)}""")
return False
return True
async def send_order(self):
"""Send the order to the broker."""
parameters = self.parameters.copy()
result = await self.order.send()
if result.retcode != 10009:
logger.warning(f"Symbol: {self.order.symbol}\nResult:\n"
f"{dict_to_string(result.get_dict(include={'comment', 'retcode'}), multi=True)}")
logger.warning(f"""Unable to place order for {self.symbol}\n
{dict_to_string(result.get_dict(include={'comment', 'retcode'}) | result.request._asdict(),
multi=True)}\n""")
return
logger.info(f"Symbol: {self.order.symbol}\nOrder: {dict_to_string(result.dict, multi=True)}\n")
await self.record_trade(result, parameters)
logger.info(f"""Placed Trade for {self.symbol}\n{dict_to_string(
result.get_dict(exclude={'request', 'retcode_external', 'retcode', 'request_id'}), multi=True)}\n""")
await self.record_trade(result, parameters=self.parameters.copy())
async def record_trade(self, result: OrderSendResult, parameters: dict):
async def record_trade(self, result: OrderSendResult, parameters: dict = None, name: str = ''):
"""Record the trade in a csv file.
Args:
result (OrderSendResult): Result of the order send
parameters: parameters of the trading strategy used to place the trade
name: Name of the trading strategy
"""
if result.retcode != 10009 or not self.config.record_trades:
return
params = parameters
params = parameters or self.parameters.copy()
profit = await self.order.calc_profit()
params["expected_profit"] = profit
date = datetime.utcnow()
date = date.replace(tzinfo=ZoneInfo("UTC"))
params["date"] = date
params["time"] = date.timestamp()
res = Result(result=result, parameters=params)
params["date"] = str(date.date())
params["time"] = str(date.time())
res = Result(result=result, parameters=params, name=name)
await res.save_csv()
@abstractmethod
+2 -1
View File
@@ -1,5 +1,6 @@
"""Utility functions for aiomql."""
def dict_to_string(data: dict, multi=False) -> str:
"""Convert a dict to a string. Use for logging.
@@ -11,4 +12,4 @@ def dict_to_string(data: dict, multi=False) -> str:
str: The string representation of the dict.
"""
sep = '\n' if multi else ', '
return f"{sep}".join(f"{key}: {value}\n" for key, value in data.items())
return f"{sep}".join(f"{key}: {value}" for key, value in data.items())