mirror of
https://github.com/Ichinga-Samuel/aiomql.git
synced 2026-08-16 13:28:08 +00:00
add backoff_decorator
This commit is contained in:
@@ -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"""
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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,3 +1,4 @@
|
||||
from .strategies import *
|
||||
from .traders import *
|
||||
from .symbols import *
|
||||
from .backtester import *
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
from .meta_tester import MetaTester
|
||||
from .test_data import TestData
|
||||
from .get_data import GetData
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user