add backoff_decorator

This commit is contained in:
Ichinga Samuel
2024-08-20 06:22:51 +01:00
parent 1eeabd99fd
commit 1187bcc2dd
28 changed files with 1109 additions and 89 deletions
+5 -3
View File
@@ -29,9 +29,11 @@ class Account(AccountInfo):
def __init__(self, **kwargs):
super().__init__(**kwargs)
if not self.login:
acc = self.config.account_info()
self.set_attributes(**acc)
acc = self.config.account_info()
acc_details = {k: v for k, v in self.get_dict(include={'login', 'server', 'password'}).items() if v}
acc |= acc_details
self.config.set_attributes(**acc)
self.set_attributes(**acc)
async def refresh(self):
"""Refreshes the account instance with the latest account details from the MetaTrader 5 terminal"""
+2 -5
View File
@@ -60,7 +60,8 @@ class Candle:
return str(self.dict())
def __eq__(self, other: "Candle"):
return self.time == other.time
eq = self.open == other.open and self.high == other.high and self.low == other.low and self.close == other.close
return eq
def __hash__(self):
return hash(self.time)
@@ -218,10 +219,6 @@ class Candles(Generic[_Candle]):
def __iter__(self):
return (self.Candle(**row._asdict()) for row in self._data.itertuples())
def __add__(self, other: _Candles | _Candle):
other = other.data if isinstance(other, type(self)) else other.dict()
return self.__class__(data=self._data.append(other.data, ignore_index=True))
@property
def timeframe(self):
tf = self.time[1] - self.time[0]
+9 -1
View File
@@ -69,6 +69,14 @@ class Config:
value = str(self.root_dir / Path(value).absolute().resolve())
super().__setattr__(key, value)
def set_attributes(self, **kwargs):
"""Set keyword arguments as object attributes
Keyword Args:
**kwargs: Object attributes and values as keyword arguments
"""
[setattr(self, key, value) for key, value in kwargs.items()]
@staticmethod
def walk_to_root(path: str | Path) -> Iterator[str]:
if not os.path.exists(path):
@@ -141,7 +149,7 @@ class Config:
data = json.load(fh)
fh.close()
data |= kwargs
[setattr(self, key, value) for key, value in data.items()]
self.set_attributes(**data)
self._initialize = False
def account_info(self) -> dict[str, int | str]:
+3 -5
View File
@@ -4,7 +4,6 @@ from logging import getLogger
from typing import Callable
import MetaTrader5
from MetaTrader5 import BookInfo, SymbolInfo, AccountInfo, Tick, TerminalInfo, TradeOrder, TradeDeal, \
TradePosition, OrderSendResult, OrderCheckResult
@@ -56,12 +55,11 @@ class MetaTrader(metaclass=BaseMeta):
_symbols_total: Callable
_terminal_info: Callable
_version: Callable
error: Error
config: Config
def __init__(self):
self.config = Config()
self.error = Error(1, 'Successful')
self.error: Error = Error(-4, description='no history')
async def __aenter__(self) -> 'MetaTrader':
"""
@@ -130,7 +128,7 @@ class MetaTrader(metaclass=BaseMeta):
return await asyncio.to_thread(self._last_error)
except Exception as err:
logger.warning(f'Error in obtaining last error.')
return 0, str(err)
return -1, str(err)
async def version(self) -> tuple[int, int, str] | None:
""""""
@@ -346,6 +344,6 @@ class MetaTrader(metaclass=BaseMeta):
if res is None:
err = await self.last_error()
self.error = Error(*err)
logger.warning(f'Error in getting deals.{self.error.description}')
logger.warning(f'Error in getting deals.{self.error}')
return res
return res
+4 -2
View File
@@ -503,11 +503,12 @@ class OrderSendResult(Base):
price: float
bid: float
ask: float
profit: float
loss: float
comment: str
request: TradeRequest
request_id: int
retcode_external: int
profit: float
"""
retcode: int
deal: int
@@ -520,7 +521,8 @@ class OrderSendResult(Base):
request: mt5.TradeRequest
request_id: int
retcode_external: int
profit: float
profit: float = None
loss: float = None
class TradePosition(Base):
+13 -22
View File
@@ -23,7 +23,6 @@ class History:
group (str): Filter for selecting history by symbols.
ticket (int): Filter for selecting history by ticket number
position (int): Filter for selecting history deals by position
initialized (bool): check if initial request has been sent to the terminal to get history.
mt5 (MetaTrader): MetaTrader instance
config (Config): Config instance
"""
@@ -55,24 +54,18 @@ class History:
self.orders: list[TradeOrder] = []
self.total_deals: int = 0
self.total_orders: int = 0
self.initialized = False
async def init(self, deals=True, orders=True) -> bool:
async def init(self, deals=True, orders=True):
"""Get history deals and orders
Keyword Args:
deals (bool): If true get history deals during initial request to terminal
orders (bool): If true get history orders during initial request to terminal
Returns:
bool: True if all requests were successful else False
"""
tasks = []
tasks.append(self.get_deals()) if deals else ...
tasks.append(self.get_orders()) if orders else ...
res = await asyncio.gather(*tasks)
self.initialized = all(res)
return self.initialized
self.deals = await self.get_deals() if deals else tuple()
self.orders = await self.get_orders() if orders else tuple()
self.total_deals = len(self.deals)
self.total_orders = len(self.orders)
async def get_deals(self, *, date_from: datetime | int = None, date_to: datetime | int = None, group: str = '',
retries: int = 3) -> tuple[TradeDeal, ...]:
@@ -89,9 +82,7 @@ class History:
deals = await self.mt5.history_deals_get(date_from=date_from, date_to=date_to, group=group)
if deals is not None:
self.deals = tuple(TradeDeal(**deal._asdict()) for deal in deals)
self.total_deals = len(self.deals)
return self.deals
return tuple(TradeDeal(**deal._asdict()) for deal in deals)
if self.mt5.error.is_connection_error():
await asyncio.sleep(retries)
@@ -113,7 +104,7 @@ class History:
ticket = ticket or self.ticket
assert ticket is not None, 'ticket not provided'
deals = await self.mt5.history_deals_get(ticket=ticket)
return tuple(sorted([TradeDeal(**deal._asdict()) for deal in deals], key=lambda x: x.time_msc))
return tuple(sorted([TradeDeal(**deal._asdict()) for deal in deals or []], key=lambda x: x.time_msc))
async def get_deals_position(self, *, position: int = None) -> tuple[TradeDeal, ...]:
"""
@@ -127,7 +118,7 @@ class History:
position = position or self.position
assert position is not None, 'position not provided'
deals = await self.mt5.history_deals_get(position=position)
return tuple(sorted([TradeDeal(**deal._asdict()) for deal in deals], key=lambda x: x.time_msc))
return tuple(sorted([TradeDeal(**deal._asdict()) for deal in deals or []], key=lambda x: x.time_msc))
async def deals_total(self, *, date_from: int | datetime = None, date_to: int | datetime = None) -> int:
"""Get total number of deals within the specified period in the constructor.
@@ -167,13 +158,13 @@ class History:
logger.warning(f'Failed to get orders: {self.mt5.error}')
return tuple()
async def get_order_ticket(self, ticket: int | None = None) -> TradeOrder:
async def get_order_ticket(self, ticket: int | None = None) -> TradeOrder | None:
ticket = ticket or self.ticket
assert isinstance(ticket, int), 'ticket not provided'
orders = await self.mt5.history_orders_get(ticket=ticket)
order = orders[0]
assert order.ticket == ticket
return TradeOrder(**order._asdict())
if orders and (order := orders[0]).ticket == ticket:
return TradeOrder(**order._asdict())
return None
async def get_orders_position(self, position: int = None) -> tuple[TradeOrder, ...]:
"""
@@ -189,7 +180,7 @@ class History:
position = position or self.position
assert isinstance(position, int), 'position not provided'
orders = await self.mt5.history_orders_get(position=position)
return tuple(sorted([TradeOrder(**order._asdict()) for order in orders], key=lambda x: x.time_done_msc))
return tuple(sorted([TradeOrder(**order._asdict()) for order in orders or []], key=lambda x: x.time_done_msc))
async def orders_total(self, date_from: int | datetime = None, date_to: int | datetime = None) -> int:
"""Get total number of orders within the specified period in the constructor.
+1
View File
@@ -1,3 +1,4 @@
from .strategies import *
from .traders import *
from .symbols import *
from .backtester import *
+3
View File
@@ -0,0 +1,3 @@
from .meta_tester import MetaTester
from .test_data import TestData
from .get_data import GetData
+134
View File
@@ -0,0 +1,134 @@
import pickle
import random
from datetime import datetime
from logging import getLogger
import asyncio
import pytz
from MetaTrader5 import Tick, SymbolInfo
import pandas as pd
from ...core.meta_trader import MetaTrader
from ...core.config import Config
from ...core.errors import Error
from ...core.constants import TimeFrame, CopyTicks, OrderType
from ...core.models import (AccountInfo, SymbolInfo, BookInfo, TradeOrder, OrderCheckResult, OrderSendResult,
TradePosition, TradeDeal)
from ...utils import backoff_decorator
logger = getLogger(__name__)
class GetData(MetaTrader):
def __init__(self, start: datetime, end: datetime, timeframes: set[TimeFrame], symbols: set[str],
interval: int = 60, name: str = '', tz: str = 'Etc/UTC'):
""""""
super().__init__()
self.tz = pytz.timezone(tz)
self.start = start.replace(tzinfo=self.tz)
self.end = end.replace(tzinfo=self.tz)
self.interval = interval
self.symbols = symbols
self.timeframes = timeframes
self.counter = 0
self.name = name or f"{start:%d-%m-%y}_{end:%d-%m-%y}"
diff = int((self.end - self.start).total_seconds())
self.span = range(start := int(self.start.timestamp()), diff + start)
self.mt5 = MetaTrader()
async def get_test_data(self) -> dict:
""""""
data = {}
rates, ticks, prices = await asyncio.gather(self.get_symbols_rates(), self.get_symbols_ticks(),
self.get_symbols_prices())
data['rates'] = rates
data['ticks'] = ticks
data['prices'] = prices
data['symbols'] = await self.get_symbols_info()
data['account'] = self.get_account_info()
return data
async def get_and_save_data(self) -> None:
""""""
data = await self.get_test_data()
fh = open(f'{self.config.root}/data/{self.name}', 'wb')
pickle.dump(data, fh)
fh.close()
def load_data(self, name: str = '') -> dict:
""""""
name = name or self.name
file = open(f'{self.config.root}/data/{name}', 'rb')
data = pickle.load(file)
file.close()
return data
async def get_symbols_info(self):
""""""
tasks = [self.get_symbol_info(symbol) for symbol in self.symbols]
res = await asyncio.gather(*tasks)
return {symbol: info for symbol, info in res}
async def get_symbols_ticks(self):
""""""
tasks = [self.get_symbol_ticks(symbol) for symbol in self.symbols]
res = await asyncio.gather(*tasks)
return {symbol: ticks for symbol, ticks in res}
async def get_symbols_prices(self):
""""""
tasks = [self.get_symbol_prices(symbol) for symbol in self.symbols]
res = await asyncio.gather(*tasks)
return {symbol: prices for symbol, prices in res}
async def get_symbols_rates(self):
""""""
tasks = [self.get_symbol_rates(symbol, timeframe) for symbol in self.symbols for timeframe in self.timeframes]
res = await asyncio.gather(*tasks)
data = {}
for symbol, timeframe, rates in res:
data.setdefault(symbol, {}).setdefault(timeframe.name, rates)
return data
@backoff_decorator(max_retries=5)
async def get_account_info(self) -> AccountInfo | None:
""""""
res = await self.mt5.account_info()
return res._asdict()
@backoff_decorator(max_retries=5)
async def get_symbol_info(self, symbol: str):
""""""
res = await self.mt5.symbol_info(symbol)
return symbol, res._asdict()
@backoff_decorator(max_retries=5)
async def get_symbol_ticks(self, symbol: str):
""""""
res = await self.mt5.copy_ticks_range(symbol, self.start, self.end, CopyTicks.ALL)
res = pd.DataFrame(res)
res.drop_duplicates(subset=['time'], keep='last', inplace=True)
res.set_index('time', inplace=True, drop=False)
return symbol, res
@backoff_decorator(max_retries=5)
async def get_symbol_prices(self, symbol: str):
""""""
res = await self.mt5.copy_ticks_range(symbol, self.start, self.end, CopyTicks.ALL)
res = pd.DataFrame(res)
res.drop_duplicates(subset=['time'], keep='last', inplace=True)
res.set_index('time', inplace=True, drop=False)
res = res.reindex(self.span, method='nearest')
return symbol, res
@backoff_decorator(max_retries=5)
async def get_symbol_rates(self, symbol: str, timeframe: TimeFrame):
""""""
res = await self.mt5.copy_rates_range(symbol, timeframe, self.start, self.end)
res = pd.DataFrame(res)
res.drop_duplicates(subset=['time'], keep='last', inplace=True)
res.set_index('time', inplace=True, drop=False)
return symbol, timeframe, res
+1
View File
@@ -0,0 +1 @@
+317
View File
@@ -0,0 +1,317 @@
import pickle
from datetime import datetime
from logging import getLogger
import asyncio
import pytz
from MetaTrader5 import Tick, SymbolInfo
import pandas as pd
from ...core.meta_trader import MetaTrader
from ...core.config import Config
from ...core.errors import Error
from ...core.constants import TimeFrame, CopyTicks, OrderType
from ...core.models import (AccountInfo, SymbolInfo, BookInfo, TradeOrder, OrderCheckResult, OrderSendResult,
TradePosition, TradeDeal)
logger = getLogger(__name__)
class MetaTester(MetaTrader):
def __init__(self, start: datetime, end: datetime, timeframes: set[TimeFrame], symbols: set[str],
interval: int = 60, name: str = '', tz: str = 'Etc/UTC'):
""""""
super().__init__()
self.tz = pytz.timezone(tz)
self.start = start.replace(tzinfo=self.tz)
self.end = end.replace(tzinfo=self.tz)
self.interval = interval
self.symbols = symbols
self.timeframes = timeframes
self.counter = 0
self.name = name or f"{start:%d-%m-%y}_{end:%d-%m-%y}"
diff = int((self.end - self.start).total_seconds())
self.span = range(start := int(self.start.timestamp()), diff + start)
def __iter__(self):
yield
async def get_test_data(self) -> dict:
""""""
data = {}
rates, ticks, prices = await asyncio.gather(self.get_symbols_rates(), self.get_symbols_ticks(),
self.get_symbols_prices())
data['rates'] = rates
data['ticks'] = ticks
data['prices'] = prices
data['symbols'] = await self.get_symbols_info()
data['account'] = self.get_account_info()
return data
async def get_and_save_data(self) -> None:
""""""
data = await self.get_test_data()
fh = open(f'{self.config.root}/data/{self.name}', 'wb')
data_file = pickle.dump(data, fh)
# data_file.update(data)
# data_file.sync()
# data_file.close()
fh.close()
def load_data(self, name: str = '') -> dict:
""""""
name = name or self.name
data_file = shelve.open(f'{self.config.root}/data/{name}', writeback=True)
data = dict(data_file)
data_file.close()
return data
async def get_symbols_info(self):
""""""
tasks = [self.get_symbol_info(symbol) for symbol in self.symbols]
res = await asyncio.gather(*tasks)
return {symbol: info for symbol, info in res}
async def get_symbols_ticks(self):
""""""
tasks = [self.get_symbol_ticks(symbol) for symbol in self.symbols]
res = await asyncio.gather(*tasks)
return {symbol: ticks for symbol, ticks in res}
async def get_account_info(self):
""""""
res = await super().account_info()
return res._asdict()
async def get_symbols_prices(self):
""""""
tasks = [self.get_symbol_prices(symbol) for symbol in self.symbols]
res = await asyncio.gather(*tasks)
return {symbol: prices for symbol, prices in res}
async def get_symbols_rates(self):
""""""
tasks = [self.get_symbol_rates(symbol, timeframe) for symbol in self.symbols for timeframe in self.timeframes]
res = await asyncio.gather(*tasks)
data = {}
for symbol, timeframe, rates in res:
data.setdefault(symbol, {}).setdefault(timeframe.name, rates)
return data
async def get_symbol_info(self, symbol: str):
""""""
res = await super().symbol_info(symbol)
return symbol, res._asdict()
async def get_symbol_ticks(self, symbol: str):
""""""
res = await super().copy_ticks_range(symbol, self.start, self.end, CopyTicks.ALL)
# res = pd.DataFrame(res)
# res.drop_duplicates(subset=['time'], keep='last', inplace=True)
# res.set_index('time', inplace=True, drop=False)
return symbol, res
async def get_symbol_prices(self, symbol: str):
""""""
res = await super().copy_ticks_range(symbol, self.start, self.end, CopyTicks.ALL)
# res = pd.DataFrame(res)
# res.drop_duplicates(subset=['time'], keep='last', inplace=True)
# res.set_index('time', inplace=True, drop=False)
# res = res.reindex(self.span, method='nearest')
return symbol, res
async def get_symbol_rates(self, symbol: str, timeframe: TimeFrame):
""""""
res = await super().copy_rates_range(symbol, timeframe, self.start, self.end)
# res = pd.DataFrame(res)
# res.drop_duplicates(subset=['time'], keep='last', inplace=True)
# res.set_index('time', inplace=True, drop=False)
return symbol, timeframe, res
async def account_info(self) -> AccountInfo | None:
""""""
res = await asyncio.to_thread(self._account_info)
if res is None:
err = await self.last_error()
self.error = Error(*err)
logger.warning(f'Error in obtaining account information.{self.error.description}')
return res
async def symbols_total(self) -> int:
return await asyncio.to_thread(self._symbols_total)
async def symbols_get(self, group: str = "") -> tuple[SymbolInfo] | None:
kwargs = {'group': group} if group else {}
res = await asyncio.to_thread(self._symbols_get, **kwargs)
if res is None:
err = await self.last_error()
self.error = Error(*err)
logger.warning(f'Error in obtaining symbols.{self.error.description}')
return res
return res
async def symbol_info(self, symbol: str) -> SymbolInfo | None:
res = await asyncio.to_thread(self._symbol_info, symbol)
if res is None:
err = await self.last_error()
self.error = Error(*err)
logger.warning(f'Error in obtaining information for {symbol}.{self.error.description}')
return res
return res
async def symbol_info_tick(self, symbol: str) -> Tick | None:
res = await asyncio.to_thread(self._symbol_info_tick, symbol)
if res is None:
err = await self.last_error()
self.error = Error(*err)
logger.warning(f'Error in obtaining tick for {symbol}.{self.error.description}')
return res
return res
async def symbol_select(self, symbol: str, enable: bool) -> bool:
return await asyncio.to_thread(self._symbol_select, symbol, enable)
async def copy_rates_from(self, symbol: str, timeframe: TimeFrame, date_from: datetime | float, count: int):
res = await asyncio.to_thread(self._copy_rates_from, symbol, timeframe, date_from, count)
if res is None:
err = await self.last_error()
self.error = Error(*err)
logger.warning(f'Error in obtaining rates for {symbol}.{self.error.description}')
return res
return res
async def copy_rates_from_pos(self, symbol: str, timeframe: TimeFrame, start_pos: int, count: int):
res = await asyncio.to_thread(self._copy_rates_from_pos, symbol, timeframe, start_pos, count)
if res is None:
err = await self.last_error()
self.error = Error(*err)
logger.warning(f'Error in obtaining rates for {symbol}.{self.error.description}')
return res
return res
async def copy_rates_range(self, symbol: str, timeframe: TimeFrame, date_from: datetime | float,
date_to: datetime | float):
res = await asyncio.to_thread(self._copy_rates_range, symbol, timeframe, date_from, date_to)
if res is None:
err = await self.last_error()
self.error = Error(*err)
logger.warning(f'Error in obtaining rates for {symbol}.{self.error.description}')
return res
return res
async def copy_ticks_from(self, symbol: str, date_from: datetime | float, count: int, flags: CopyTicks):
res = await asyncio.to_thread(self._copy_ticks_from, symbol, date_from, count, flags)
if res is None:
err = await self.last_error()
self.error = Error(*err)
logger.warning(f'Error in obtaining ticks for {symbol}.{self.error.description}')
return res
return res
async def copy_ticks_range(self, symbol: str, date_from: datetime | float, date_to: datetime | float,
flags: CopyTicks):
res = await asyncio.to_thread(self._copy_ticks_range, symbol, date_from, date_to, flags)
if res is None:
err = await self.last_error()
self.error = Error(*err)
logger.warning(f'Error in obtaining ticks for {symbol}.{self.error.description}')
return res
return res
async def orders_total(self) -> int:
return await asyncio.to_thread(self._orders_total)
async def orders_get(self, group: str = "", ticket: int = 0, symbol: str = "") -> tuple[TradeOrder] | None:
"""Get active orders with the ability to filter by symbol or ticket. There are three call options.
Call without parameters. Return active orders on all symbols
Keyword Args:
symbol (str): Symbol name. Optional named parameter. If a symbol is specified, the ticket parameter is ignored.
group (str): The filter for arranging a group of necessary symbols. Optional named parameter. If the group is specified, the function
returns only active orders meeting a specified criteria for a symbol name.
ticket (int): Order ticket (ORDER_TICKET). Optional named parameter.
Returns:
tuple[TradeOrder]: A list of active trade orders as TradeOrder objects
"""
kwargs = {key: value for key, value in (('group', group), ('ticket', ticket), ('symbol', symbol)) if value}
res = await asyncio.to_thread(self._orders_get, **kwargs)
if res is None:
err = await self.last_error()
self.error = Error(*err)
logger.warning(f'Error in obtaining orders.{self.error.description}')
return res
return res
async def order_calc_margin(self, action: OrderType, symbol: str, volume: float, price: float) -> float | None:
res = await asyncio.to_thread(self._order_calc_margin, action, symbol, volume, price)
if res is None:
err = await self.last_error()
self.error = Error(*err)
logger.warning(f'Error in calculating margin.{self.error.description}')
return res
return res
async def order_calc_profit(self, action: OrderType, symbol: str, volume: float, price_open: float,
price_close: float) -> float | None:
res = await asyncio.to_thread(self._order_calc_profit, action, symbol, volume, price_open, price_close)
if res is None:
err = await self.last_error()
self.error = Error(*err)
logger.warning(f'Error in calculating profit.{self.error.description}')
return res
return res
async def order_check(self, request: dict) -> OrderCheckResult:
return await asyncio.to_thread(self._order_check, request)
async def order_send(self, request: dict) -> OrderSendResult:
return await asyncio.to_thread(self._order_send, request)
async def positions_total(self) -> int:
return await asyncio.to_thread(self._positions_total)
async def positions_get(self, group: str = "", ticket: int = None, symbol: str = "") -> tuple[TradePosition] | None:
kwargs = {key: value for key, value in (('group', group), ('ticket', ticket), ('symbol', symbol)) if value}
res = await asyncio.to_thread(self._positions_get, **kwargs)
if res is None:
err = await self.last_error()
self.error = Error(*err)
logger.warning(f'Error in obtaining open positions.{self.error.description}')
return res
return res
async def history_orders_total(self, date_from: datetime | float, date_to: datetime | float) -> int:
return await asyncio.to_thread(self._history_orders_total, date_from, date_to)
async def history_orders_get(self, date_from: datetime | float = None, date_to: datetime | float = None,
group: str = '', ticket: int = None, position: int = None) -> tuple[TradeOrder] | None:
kwargs = {key: value for key, value in (('group', group), ('ticket', ticket), ('position', position)) if value}
args = tuple(arg for arg in (date_from, date_to) if arg)
res = await asyncio.to_thread(self._history_orders_get, *args, **kwargs)
if res is None:
err = await self.last_error()
self.error = Error(*err)
logger.warning(f'Error in getting orders.{self.error.description}')
return res
return res
async def history_deals_total(self, date_from: datetime | float, date_to: datetime | float) -> int:
return await asyncio.to_thread(self._history_deals_total, date_from, date_to)
async def history_deals_get(self, date_from: datetime | float = None, date_to: datetime | float = None,
group: str = '', ticket: int = None, position: int = None) -> tuple[TradeDeal] | None:
kwargs = {key: value for key, value in (('group', group), ('ticket', ticket), ('position', position)) if value}
args = tuple(arg for arg in (date_from, date_to) if arg)
res = await asyncio.to_thread(self._history_deals_get, *args, **kwargs)
if res is None:
err = await self.last_error()
self.error = Error(*err)
logger.warning(f'Error in getting deals.{self.error}')
return res
return res
+23
View File
@@ -0,0 +1,23 @@
from typing import Literal
from ...core.models import AccountInfo, SymbolInfo
from ...core.constants import TimeFrame
from ...core.meta_trader import MetaTrader
# from ...account import Account
class TestData:
def __init__(self, data):
self._data = data
def __getitem__(self, item: tuple[Literal['ticks', 'rates'], SymbolInfo, TimeFrame]):
type_, symbol, time_frame = item
if type_ == 'ticks':
return self._data[type_][symbol.name]
return self._data[type_][symbol.name][time_frame.name]
# res = pd.DataFrame(res)
# res.drop_duplicates(subset=['time'], keep='last', inplace=True)
# res.set_index('time', inplace=True, drop=False)
+3 -3
View File
@@ -15,7 +15,7 @@ class ForexSymbol(Symbol):
points = amount / (volume * self.point * self.trade_contract_size)
return points
async def compute_volume_points(self, *, amount: float, points: float, use_limits=False, round_down: bool = True,
async def compute_volume_points(self, *, amount: float, points: float, use_limits=False, round_down: bool = False,
adjust: float = False) -> tuple[float, float]:
"""Compute the volume and points required for a trade. Given the amount and the number of points.
@@ -40,8 +40,8 @@ class ForexSymbol(Symbol):
return vol, points
raise VolumeError(f"Incorrect Volume. Computed Volume outside the range of permitted volumes")
async def compute_volume_sl(self, *, amount: float, price: float, sl: float,
use_limits=False, adjust: bool = False, round_down: bool = True) -> tuple[float, float]:
async def compute_volume_sl(self, *, amount: float, price: float, sl: float, use_limits=False, adjust: bool = False,
round_down: bool = False) -> tuple[float, float]:
amount = await self.check_amount(amount)
volume = amount / ((price - sl) * self.trade_contract_size)
volume = self.round_off_volume(volume, round_down=round_down)
+27 -19
View File
@@ -38,7 +38,7 @@ class Order(TradeRequest):
"""
return await self.mt5.orders_total()
async def get_order(self, *, ticket: int, retries: int = 3) -> TradeOrder:
async def get_order(self, *, ticket: int, retries: int = 3) -> TradeOrder | None:
"""
Get the order by ticket number.
Args:
@@ -47,16 +47,14 @@ class Order(TradeRequest):
Returns:
"""
if retries < 1:
raise OrderError(f'Failed to get orders for {self.symbol}: {self.mt5.error}')
return None
orders = await self.mt5.orders_get(ticket=ticket)
if orders is not None:
order = TradeOrder(**orders[0]._asdict())
assert order.ticket == ticket, f'Order ticket mismatch {order.ticket} != {ticket}'
return order
if orders and (order := orders[0]).ticket == ticket:
return TradeOrder(**order._asdict())
if self.mt5.error.is_connection_error():
await asyncio.sleep(retries)
return await self.get_order(ticket=ticket, retries=retries-1)
raise OrderError(f'Failed to get orders for {self.symbol}: {self.mt5.error}')
return None
async def get_orders(self, *, ticket: int = 0, symbol: str = '', group: str = '', retries=3)\
-> tuple[TradeOrder, ...]:
@@ -69,7 +67,7 @@ class Order(TradeRequest):
tuple[TradeOrder]: A Tuple of active trade orders as TradeOrder objects
"""
if retries < 1:
raise OrderError(f'Failed to get orders for {self.symbol}: {self.mt5.error}')
return tuple()
symbol = getattr(self, 'symbol', symbol)
orders = await self.mt5.orders_get(symbol=symbol, ticket=ticket, group=group)
if orders is not None:
@@ -78,9 +76,9 @@ class Order(TradeRequest):
if self.mt5.error.is_connection_error():
await asyncio.sleep(retries)
return await self.get_orders(ticket=ticket, symbol=symbol, group=group, retries=retries-1)
raise OrderError(f'Failed to get orders for {self.symbol}: {self.mt5.error}')
return tuple()
async def check(self) -> OrderCheckResult:
async def check(self, **kwargs) -> OrderCheckResult:
"""Check funds sufficiency for performing a required trading operation and the possibility of executing it.
Returns:
@@ -89,7 +87,8 @@ class Order(TradeRequest):
Raises:
OrderError: If not successful
"""
res = await self.mt5.order_check(self.dict)
req = self.dict | kwargs
res = await self.mt5.order_check(req)
if res is None:
raise OrderError(f'Failed to check order due to {self.mt5.error.description}')
return OrderCheckResult(**res._asdict())
@@ -106,7 +105,15 @@ class Order(TradeRequest):
res = await self.mt5.order_send(self.dict)
if res is None:
raise OrderError(f'Failed to send order {self.symbol} due to {self.mt5.error.description}')
return OrderSendResult(**res._asdict())
res = OrderSendResult(**res._asdict())
try:
profit = await self.calc_profit()
loss = await self.calc_profit(tp=self.sl)
res.loss = loss
res.profit = profit
except Exception as _:
pass
return res
async def calc_margin(self) -> float:
"""Return the required margin in the account currency to perform a specified trading operation.
@@ -122,16 +129,17 @@ class Order(TradeRequest):
raise OrderError(f'Failed to calculate margin for {self.symbol} due to {self.mt5.error.description}')
return res
async def calc_profit(self) -> float:
async def calc_profit(self, **kwargs) -> float | None:
"""Return profit in the account currency for a specified trading operation.
Returns:
float: Returns float value if successful
Raises:
OrderError: If not successful
None: If not successful
"""
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} due to {self.mt5.error.description}')
include = {'tp', 'price', 'symbol', 'volume', 'type'}
args = self.get_dict(include=include)
args |= kwargs
if len(include.intersection(args.keys())) < len(include):
return None
res = await self.mt5.order_calc_profit(args['type'], args['symbol'], args['volume'], args['price'], args['tp'])
return res
+3 -4
View File
@@ -68,7 +68,7 @@ class Positions:
logger.warning(f'Failed to get positions for {symbol or self.symbol}. {self.mt5.error}')
return []
async def position_get(self, *, ticket: int) -> TradePosition:
async def position_get(self, *, ticket: int) -> TradePosition | None:
"""Get an open position by ticket.
Args:
ticket (int): Position ticket.
@@ -78,9 +78,8 @@ class Positions:
"""
positions = await self.positions_get(ticket=ticket)
position = positions[0] if positions else None
if position is None:
raise ValueError(f'Position with ticket {ticket} not found')
assert position.ticket == ticket, f'Position with ticket {ticket} not found'
if position is None or position.ticket != ticket:
return None
return position
async def close(self, *, ticket: int, symbol: str, price: float, volume: float, order_type: OrderType):
+4 -7
View File
@@ -174,15 +174,12 @@ class Symbol(SymbolInfo):
within the limits of the symbol. The float is the volume to use if the volume is not within the limits of the
symbol.
"""
check = self.volume_min <= volume <= self.volume_max
if check:
if check := self.volume_min <= volume <= self.volume_max:
return check, volume
if not check and volume < self.volume_min:
return check, self.volume_min
else:
return check, self.volume_max
return check, self.volume_min if volume <= self.volume_min else self.volume_max
def round_off_volume(self, volume: float, round_down: bool = True) -> float:
def round_off_volume(self, volume: float, round_down: bool = False) -> float:
"""Round off the volume to the nearest volume step.
Args:
@@ -226,7 +223,7 @@ class Symbol(SymbolInfo):
quote: The quote currency of the pair
Returns:
float: Amount in terms of the base currency
float: Amount in terms of the quote currency
Raises:
ValueError: If conversion is impossible
+5 -10
View File
@@ -11,7 +11,6 @@ from .ticks import Tick
from .ram import RAM
from .core.models import OrderType, OrderSendResult
from .core.config import Config
from .utils import dict_to_string
from .result import Result
logger = getLogger(__name__)
@@ -90,8 +89,7 @@ class Trader(ABC):
"""
check = await self.order.check()
if check.retcode != 0:
req = check.request._asdict() | check.get_dict(include={'comment', 'retcode'})
logger.warning(f"Invalid order for {self.symbol}: {dict_to_string(req)}")
logger.warning(f"Invalid order for {self.symbol} due to {check.comment}")
return False
return True
@@ -99,12 +97,9 @@ class Trader(ABC):
"""Send the order to the broker."""
result = await self.order.send()
if result.retcode != 10009:
req = result.request._asdict() | result.get_dict(include={'comment', 'retcode'})
logger.warning(f"Unable to place order for {self.symbol}: {dict_to_string(req)}")
logger.warning(f"Unable to place order for {self.symbol} due to {result.comment}")
return result
res = result.get_dict(exclude={'request', 'retcode_external', 'retcode', 'request_id'})
logger.info(f"Placed Trade for {self.symbol}: {dict_to_string(res)}")
await self.record_trade(result, parameters=self.parameters.copy())
logger.info(f"Placed Trade for {self.symbol}")
return result
async def record_trade(self, result: OrderSendResult, parameters: dict = None, name: str = '', exclude: set = None):
@@ -117,9 +112,9 @@ class Trader(ABC):
"""
if result.retcode != 10009 or not self.config.record_trades:
return
params = parameters or self.parameters.copy()
params = parameters or self.parameters
params = {k: v for k, v in params.items() if k not in (exclude or set())}
profit = await self.order.calc_profit()
profit = result.profit or await self.order.calc_profit()
params["expected_profit"] = profit
date = datetime.utcnow()
date = date.replace(tzinfo=ZoneInfo("UTC"))
+27 -1
View File
@@ -1,6 +1,10 @@
"""Utility functions for aiomql."""
import decimal
import random
from functools import wraps, partial
import asyncio
from .candle import Candles, Candle
@@ -18,7 +22,7 @@ def dict_to_string(data: dict, multi=False) -> str:
return f"{sep}".join(f"{key}: {value}" for key, value in data.items())
def round_off(value: float, step: float, round_down: bool = True) -> float:
def round_off(value: float, step: float, round_down: bool = False) -> float:
"""Round off a number to the nearest step."""
with decimal.localcontext() as ctx:
ctx.rounding = decimal.ROUND_DOWN if round_down else decimal.ROUND_UP
@@ -35,3 +39,25 @@ def find_bullish_fractal(candles: Candles) -> Candle | None:
for i in range(len(candles) - 3, 1, -1):
if candles[i].low < min(candles[i - 1].low, candles[i + 1].low, candles[i - 2].low, candles[i + 2].low):
return candles[i]
def backoff_decorator(func=None, *, max_retries: int = 3, retries: int = 0, delay: int = 1, error=None) -> callable:
if func is None:
return partial(backoff_decorator, max_retries=max_retries, retries=retries, delay=delay, error=error)
@wraps(func)
async def wrapper(*args, **kwargs):
nonlocal delay, retries
try:
res = await func(*args, **kwargs)
if res == error:
raise Exception('Invalid return type')
return res
except Exception as _:
await asyncio.sleep(delay * 2 ** retries + random.uniform(0, 1))
delay += 1
retries += 1
return await wrapper(*args, **kwargs)
return wrapper