diff --git a/check.json b/check.json new file mode 100644 index 0000000..71fe5fa --- /dev/null +++ b/check.json @@ -0,0 +1 @@ +{"a": 5}{"af": 5} \ No newline at end of file diff --git a/src/aiomql/__init__.py b/src/aiomql/__init__.py index 7677e20..b45e7af 100644 --- a/src/aiomql/__init__.py +++ b/src/aiomql/__init__.py @@ -18,4 +18,4 @@ from .terminal import Terminal from .sessions import Session, Sessions from .utils import dict_to_string, round_off, backoff_decorator, error_handler, error_handler_sync, round_up, round_down from .lib import * -from .contrib import * +# from .contrib import * diff --git a/src/aiomql/contrib/__init__.py b/src/aiomql/contrib/__init__.py index e29223e..f7744cc 100644 --- a/src/aiomql/contrib/__init__.py +++ b/src/aiomql/contrib/__init__.py @@ -1 +1 @@ -from .backtester import * +# from .backtester import * diff --git a/src/aiomql/contrib/backtester/__init__.py b/src/aiomql/contrib/backtester/__init__.py index 65e6eda..39c34cd 100644 --- a/src/aiomql/contrib/backtester/__init__.py +++ b/src/aiomql/contrib/backtester/__init__.py @@ -1,5 +1,5 @@ from .meta_tester import MetaTester -from .test_data import TestData +from .backtest_engine import BackTestEngine from .get_data import GetData from .test_strategy import TestStrategy from .event_manager import EventManager diff --git a/src/aiomql/contrib/backtester/test_data.py b/src/aiomql/contrib/backtester/backtest_engine.py similarity index 96% rename from src/aiomql/contrib/backtester/test_data.py rename to src/aiomql/contrib/backtester/backtest_engine.py index 7c270ff..6a4105d 100644 --- a/src/aiomql/contrib/backtester/test_data.py +++ b/src/aiomql/contrib/backtester/backtest_engine.py @@ -1,16 +1,14 @@ import asyncio -from collections import namedtuple from datetime import datetime from typing import Literal from itertools import zip_longest import random -import json from functools import cached_property +from logging import getLogger import pandas as pd import pytz import numpy as np -from debugpy.common.timestamp import current from pandas import DataFrame from MetaTrader5 import (Tick, SymbolInfo, AccountInfo, TradeOrder, TradePosition, TradeDeal, TradeRequest, OrderCheckResult, OrderSendResult, TerminalInfo) @@ -18,18 +16,18 @@ from MetaTrader5 import (Tick, SymbolInfo, AccountInfo, TradeOrder, TradePositio from ...core.meta_trader import MetaTrader from ...core.constants import TimeFrame, CopyTicks, OrderType, TradeAction, AccountStopOutMode, PositionReason from ...core.config import Config -from ...lib.strategies.finger_trap import logger from ...utils import round_down, round_up, error_handler, error_handler_sync, async_cache -from .get_data import Data, GetData +from .get_data import Data, GetData, Cursor from .test_account import TestAccount from .types import PositionsManager, OrdersManager, DealsManager tz = pytz.timezone('Etc/UTC') -Cursor = namedtuple('Cursor', ['index', 'time']) + +logger = getLogger(__name__) -class TestData: +class BackTestEngine: mt5: MetaTrader = MetaTrader() span: range range: range @@ -45,8 +43,8 @@ class TestData: self.margins: dict[int, float] = {} self.config = Config(test_data=self) self.set_up(start=start, end=end, speed=speed) + self.cursor: Cursor = self._data.cursor or Cursor(index=self.range.start, time=self.span.start) self._data.name = self._data.name or f"{datetime.fromtimestamp(self.span[0]):%d-%m-%y}_{datetime.fromtimestamp(self.span[-1]):%d-%m-%y}" - self.fh = open(f'{self.config.test_data_dir}/data.json', 'a') def set_up(self, start: float | datetime = 0, end: float | datetime = 0, speed: int = 1): span_start = (int(start.timestamp()) if isinstance(start, datetime) else int(start)) or self._data.span.start @@ -54,12 +52,14 @@ class TestData: self.span = range(span_start, span_end, speed) self.range = range(0, span_end - span_start, speed) self.iter = zip_longest(self.range, self.span) - self.cursor: Cursor = Cursor(index=self.range.start, time=self.span.start) def __next__(self) -> Cursor: - index, time = next(self.iter) - self.cursor = Cursor(index=index, time=time) - return self.cursor + try: + index, time = next(self.iter) + self.cursor = Cursor(index=index, time=time) + return self.cursor + except StopIteration: + logger.warning('End of time') def __repr__(self): return f"{self.__class__.__name__}()" @@ -70,15 +70,12 @@ class TestData: @property def data(self): return self._data - - def to_json(self, data): - json.dump(data, self.fh) def reset(self): self.iter = zip_longest(self.range, self.span) self.cursor = Cursor(index=self.range.start, time=self.span.start) - def go_to(self, time: datetime | int): + def go_to(self, *, time: datetime | int): time = int(time.timestamp()) if isinstance(time, datetime) else int(time) steps = time - self.cursor.time @@ -92,7 +89,7 @@ class TestData: self.iter = zip_longest(range_, span) self.cursor = Cursor(index=range_.start, time=span.start) - def fast_forward(self, steps: int): + def fast_forward(self, *, steps: int): for _ in range(steps): self.next() @@ -108,7 +105,6 @@ class TestData: self.update_account(profit=profit) def save(self): - self.fh.close() try: if len(self.orders) or len(self.deals): for symbol in self.orders: @@ -243,7 +239,7 @@ class TestData: osr = {'retcode': 10013, 'comment': 'Invalid request', 'request': TradeRequest(request.get(k, (0 if k != 'comment' else '')) for k in TradeRequest.__match_args__)} - trade_order = {'ticket': order_ticket, **{k: v for k, v in request.items() if k in TradeOrder.__match_args__}} + trade_order = {k: v for k, v in request.items() if k in TradeOrder.__match_args__} current_tick = await self.get_price_tick(request.get('symbol'), self.cursor.time) if current_tick is None: osr['comment'] = 'Market is closed' @@ -297,7 +293,7 @@ class TestData: return OrderSendResult((osr.get(k, 0) for k in OrderSendResult.__match_args__)) # self.to_json(osr) # ToDo: remove later # return OrderSendResult(osr) - price = current_tick.ask if order_type == OrderType.BUY else current.tick.bid + price = current_tick.ask if order_type == OrderType.BUY else current_tick.bid # ToDo: Cross check this values with actual values. position = {'comment': 'Position Opened', 'ticket': order_ticket, 'symbol': symbol, 'volume': volume, 'price_open': price, 'price_current': price, 'type': order_type, 'profit': 0, 'reason': PositionReason.EXPERT, @@ -315,9 +311,9 @@ class TestData: self.positions[order.ticket] = pos self.orders[order.ticket] = order - osr.update({'order': order_ticket, 'price': price, 'volume': volume, 'bid': tick.bid, - 'ask': tick.ask, 'deal': deal_ticket}) - margin = await self.order_calc_margin(action, symbol, volume, price, use_) + osr.update({'order': order_ticket, 'price': price, 'volume': volume, 'bid': current_tick.bid, + 'ask': current_tick.ask, 'deal': deal_ticket}) + margin = await self.order_calc_margin(action, symbol, volume, price) self.margins[order_ticket] = margin self.update_account(margin=margin) self.to_json(osr) # ToDo: remove later diff --git a/src/aiomql/contrib/backtester/check1.py b/src/aiomql/contrib/backtester/check1.py index cba4363..bcac88a 100644 --- a/src/aiomql/contrib/backtester/check1.py +++ b/src/aiomql/contrib/backtester/check1.py @@ -1,42 +1,21 @@ -from functools import cached_property -class Data: +from turtledemo.penrose import start + + +class Tre: def __init__(self): - self.a = 1 - self.b = 2 + self.start = 0 + self.end = 3 + self.span = iter(range(self.start, self.end)) -class TData: - _a: int - _b: int - _dat: dict - - def __init__(self): - self._data = Data() - - def __getattr__(self, item): - if val := self.__annotations__.get(item): - return getattr(self._data, item, val()) - - @property - def a(self): - return self._a - - @a.setter - def a(self, val): - self._a = val - - @property - def dat(self): - return self._dat - - @dat.setter - def dat(self, key, val): - self._dat |= val - - @dat.deleter - def dat(self): - self._dat = {} + def __next__(self): + try: + next(self.span) + except StopIteration: + print('End of range') -f = TData() -# f.data = {'a': 1} -print(f.a) +r = Tre() +next(r) +next(r) +next(r) +next(r) diff --git a/src/aiomql/contrib/backtester/d.pkl b/src/aiomql/contrib/backtester/d.pkl new file mode 100644 index 0000000..e69de29 diff --git a/src/aiomql/contrib/backtester/get_data.py b/src/aiomql/contrib/backtester/get_data.py index 4fa35b7..8b9a0fb 100644 --- a/src/aiomql/contrib/backtester/get_data.py +++ b/src/aiomql/contrib/backtester/get_data.py @@ -5,6 +5,7 @@ import lzma from datetime import datetime from logging import getLogger from typing import Sequence, ClassVar +from collections import namedtuple import pytz import pandas as pd @@ -21,11 +22,10 @@ logger = getLogger(__name__) from MetaTrader5 import TradePosition, TradeOrder, TradeDeal tof = list(TradeOrder.__match_args__) -tof.append('symbol') tpf = list(TradePosition.__match_args__) -tpf.append('symbol') tdf = list(TradeDeal.__match_args__) -tdf.append('symbol') + +Cursor = namedtuple('Cursor', ['index', 'time']) @dataclass @@ -40,12 +40,12 @@ class Data: rates: dict[str, dict[str, DataFrame]] = field(default_factory=dict) span: range = range(0) range: range = range(0) - history_orders: DataFrame = field(default_factory=lambda: DataFrame([], columns=tof)) - history_deals: DataFrame = field(default_factory=lambda: DataFrame([], columns=tdf)) - positions: dict[str, DataFrame] = field(default_factory=dict) - orders: dict[str, DataFrame] = field(default_factory=dict) - - _fields: list[ClassVar[str]] = field(default_factory=list) + orders: dict[int, dict] = field(default_factory=lambda: {}) + deals: dict[int, dict] = field(default_factory=lambda: {}) + positions: dict[int, dict] = field(default_factory=lambda: {}) + active_orders: tuple[int, ...] = field(default_factory=lambda: ()) + open_positions: tuple[int, ...] = field(default_factory=lambda: ()) + cursor: Cursor = None def __str__(self): return f""" @@ -68,7 +68,7 @@ class Data: @property def fields(self): - return self._fields or [name for f in fields(self) if (name := f.name) != '_fields'] + return [f.name for f in fields(self)] class GetData: diff --git a/src/aiomql/contrib/backtester/meta_tester.py b/src/aiomql/contrib/backtester/meta_tester.py index 05ead09..7f078e1 100644 --- a/src/aiomql/contrib/backtester/meta_tester.py +++ b/src/aiomql/contrib/backtester/meta_tester.py @@ -5,7 +5,7 @@ from numpy import ndarray from MetaTrader5 import (Tick, SymbolInfo, AccountInfo, TerminalInfo, TradeOrder, TradePosition, TradeDeal, OrderCheckResult, OrderSendResult) -from .test_data import TestData +from .backtest_engine import BackTestEngine from .get_data import GetData from ...core.meta_trader import MetaTrader @@ -18,17 +18,17 @@ logger = getLogger(__name__) class MetaTester(MetaTrader): """A class for testing trading strategies in the MetaTrader 5 terminal. A subclass of MetaTrader.""" - def __init__(self, test_data: TestData = None): + def __init__(self, test_data: BackTestEngine = None): super().__init__() if test_data is not None: self.config.test_data = test_data @property - def test_data(self) -> TestData | None: + def test_data(self) -> BackTestEngine | None: return self.config.test_data @test_data.setter - def test_data(self, value: TestData): + def test_data(self, value: BackTestEngine): self.config.test_data = value async def last_error(self) -> tuple[int, str]: @@ -47,7 +47,7 @@ class MetaTester(MetaTrader): name = f"{self.config.test_data_dir_name}/{test_data_file}" data = GetData.load_data(name=name, compressed=self.config.compress_test_data) if data is not None: - self.test_data = TestData(data) + self.test_data = BackTestEngine(data) success = True except Exception as err: diff --git a/src/aiomql/contrib/backtester/types.py b/src/aiomql/contrib/backtester/types.py index c9a9b98..c89e8f4 100644 --- a/src/aiomql/contrib/backtester/types.py +++ b/src/aiomql/contrib/backtester/types.py @@ -1,18 +1,24 @@ -from typing import Generic +from typing import TypeVar, Generic from MetaTrader5 import TradePosition, TradeOrder, TradeDeal from aiomql.utils import logger +TradeData = TypeVar('TradeData', bound=TradePosition | TradeOrder | TradeDeal) -class TradingData[Generic]: - _data: dict[int, TradePosition | TradeOrder | TradeDeal] + +class TradingData(Generic[TradeData]): + _data: dict[int, TradeData] _open_items: set[int] + def __init__(self, open_items: set[int] = None, data: dict = None): + self._data = data or {} + self._open_items = open_items or {trade.ticket for trade in self._data.values()} + def __getitem__(self, item): return self._data[item] - def __setitem__(self, key, value: TradePosition | TradeOrder | TradeDeal): + def __setitem__(self, key, value: TradeData): self._open_items.add(value.ticket) self._data[key] = value @@ -23,7 +29,7 @@ class TradingData[Generic]: except KeyError: logger.warning(f'{key} not found') - def __contains__(self, item): + def __contains__(self, item: int): return item in self._open_items def __iter__(self): @@ -32,10 +38,10 @@ class TradingData[Generic]: def __len__(self): return len(self._data) - def get(self, key, default=None): + def get(self, key, default=None) -> TradeData | None: return self._data.get(key, default) if key in self._open_items else default - def pop(self, key, default=None): + def pop(self, key, default=None) -> TradeData | None: self._open_items.discard(key) return self._data.get(key, default) @@ -59,10 +65,6 @@ class TradingData[Generic]: class PositionsManager(TradingData): _data: dict[int, TradePosition] - def __init__(self, open_items: set[int] = None, data: dict = None): - self._open_items = open_items or set() - self._data = data or {} - @property def open_positions(self) -> tuple[TradePosition, ...]: return tuple(position for position in self._data.values() if position.ticket in self.open_items) @@ -71,16 +73,10 @@ class PositionsManager(TradingData): class OrdersManager(TradingData): _data = dict[int, TradeOrder] - def __init__(self, open_items: set[int] = None, data: dict = None): - self._open_items = open_items or set() - self._data = data or {} - @property def active_orders(self) -> tuple[TradeOrder, ...]: return tuple(order for order in self._data.values() if order.ticket in self.open_items) class DealsManager(TradingData): - def __init__(self, open_items: set[int] = None, data: dict = None): - self._open_items = open_items or set() - self._data = data or {} + ... diff --git a/src/aiomql/core/base.py b/src/aiomql/core/base.py index bfa8b90..5f24c74 100644 --- a/src/aiomql/core/base.py +++ b/src/aiomql/core/base.py @@ -4,7 +4,7 @@ from logging import getLogger from .config import Config from .meta_trader import MetaTrader -from ..contrib.backtester import MetaTester +# from ..contrib.backtester import MetaTester logger = getLogger(__name__) @@ -22,7 +22,7 @@ class Base: **kwargs: Set instance attributes with keyword arguments. Only if they are annotated on the class body. """ self.config = Config() - self.mt5 = MetaTrader() if self.config.mode == 'live' else MetaTester() + self.mt5 = MetaTrader() #if self.config.mode == 'live' else MetaTester() self.exclude = {'mt5', "config", 'exclude', 'include', 'annotations', 'class_vars', 'dict'} self.include = set() self.set_attributes(**kwargs) diff --git a/src/aiomql/core/meta_trader.py b/src/aiomql/core/meta_trader.py index 3c7a6fa..42011d7 100644 --- a/src/aiomql/core/meta_trader.py +++ b/src/aiomql/core/meta_trader.py @@ -91,8 +91,8 @@ class MetaTrader(metaclass=BaseMeta): self.error = Error(*err) if self.error.is_connection_error(): - await self.initialize(**self.config.account_info(), path=self.config.path) - await self.login(**self.config.account_info()) + await self.initialize(path=self.config.path) + await self.login() res = await asyncio.to_thread(func, *args, **kwargs) if res is None: @@ -103,7 +103,7 @@ class MetaTrader(metaclass=BaseMeta): logger.warning(f'{error_msg}:{self.error.description}') return res - async def login(self, login: int, password: str, server: str, timeout: int = 60000) -> bool: + async def login(self, *, login: int = 0, password: str = '', server: str = '', timeout: int = 60000) -> bool: """ Connects to the MetaTrader terminal using the specified login, password and server. @@ -116,6 +116,10 @@ class MetaTrader(metaclass=BaseMeta): Returns: bool: True if successful, False otherwise. """ + acc_details = self.config.account_info() + login = login or acc_details.get('login', 0) + password = password or acc_details.get('password', '') + server = server or acc_details.get('server', '') return await asyncio.to_thread(self._login, login, password=password, server=server, timeout=timeout) async def initialize(self, path: str = "", login: int = 0, password: str = "", server: str = "", @@ -128,15 +132,20 @@ class MetaTrader(metaclass=BaseMeta): login (int): The trading account number. password (str): The trading account password. server (str): The trading server name. - timeout (int): The timeout for the connection in seconds. + timeout (int): The timeout for the connection in milliseconds. portable (bool): If True, the terminal will be launched in portable mode. Returns: bool: True if successful, False otherwise. """ + path = path or self.config.path args = (str(path),) if path else () - kwargs = {key: value for key, value in (('login', login), ('password', password), ('server', server), - ('timeout', timeout), ('portable', portable)) if value} + acc = self.config.account_info() + kwargs = {key: value for key, value in (('login', login or acc.get('login')), + ('password', password or acc.get('password')), + ('server', server or acc.get('server')), + ('timeout', timeout or 60000), + ('portable', portable)) if key is not None} res = await asyncio.to_thread(self._initialize, *args, **kwargs) return res diff --git a/src/aiomql/lib/strategies/__init__.py b/src/aiomql/lib/strategies/__init__.py index b65e00a..175ab49 100644 --- a/src/aiomql/lib/strategies/__init__.py +++ b/src/aiomql/lib/strategies/__init__.py @@ -1,3 +1,3 @@ from .finger_trap import FingerTrap from .tracker import Tracker -from .finger_trap_back_test import FingerTrapTest, FingerTrapSingleTest +# from .finger_trap_back_test import FingerTrapTest, FingerTrapSingleTest diff --git a/src/aiomql/positions.py b/src/aiomql/positions.py index ad7d358..45930a5 100644 --- a/src/aiomql/positions.py +++ b/src/aiomql/positions.py @@ -7,7 +7,7 @@ from .core.models import TradePosition, TradeAction from .core.constants import OrderType from .core.config import Config -from .contrib.backtester.meta_tester import MetaTester +# from .contrib.backtester.meta_tester import MetaTester from .order import Order from .utils import backoff_decorator @@ -25,7 +25,7 @@ class Positions: ticket (int): Position ticket. mt5 (MetaTrader): MetaTrader instance. """ - mt5: MetaTrader | MetaTester + mt5: MetaTrader #| MetaTester def __init__(self, *, symbol: str = "", group: str = "", ticket: int = 0): """Get Open Positions. @@ -38,7 +38,7 @@ class Positions: """ self.config = Config() - self.mt5 = MetaTrader() if self.config.mode == 'live' else MetaTester() + self.mt5 = MetaTrader() # if self.config.mode == 'live' else MetaTester() self.symbol = symbol self.group = group self.ticket = ticket diff --git a/src/aiomql/sessions.py b/src/aiomql/sessions.py index b507dde..fa905f2 100644 --- a/src/aiomql/sessions.py +++ b/src/aiomql/sessions.py @@ -6,7 +6,7 @@ from logging import getLogger from .positions import Positions from .core.config import Config -from.contrib.backtester.event_manager import EventManager +# from.contrib.backtester.event_manager import EventManager logger = getLogger(__name__) @@ -20,13 +20,13 @@ def delta(obj: time) -> timedelta: return timedelta(hours=obj.hour, minutes=obj.minute, seconds=obj.second, microseconds=obj.microsecond) -async def backtest_sleep(secs): - """A custom function to call when the session starts.""" - em = EventManager() - - async with em.condition: - while em.config.test_data.cursor.time < (em.config.test_data.cursor.time + secs): - await em.condition.wait() +# async def backtest_sleep(secs): +# """A custom function to call when the session starts.""" +# # em = EventManager() +# +# async with em.condition: +# while em.config.test_data.cursor.time < (em.config.test_data.cursor.time + secs): +# await em.condition.wait() class Session: @@ -213,7 +213,7 @@ class Sessions: current_session = self.find_next(now) secs = current_session.until() + 10 logger.info(f'sleeping for {secs} seconds until next {current_session} session') - sleep_func = sleep if Config().mode == 'live' else backtest_sleep + sleep_func = sleep # if Config().mode == 'live' else backtest_sleep await sleep_func(secs) self.current_session = current_session await self.current_session.begin() diff --git a/src/aiomql/strategy.py b/src/aiomql/strategy.py index b1becef..94550d3 100644 --- a/src/aiomql/strategy.py +++ b/src/aiomql/strategy.py @@ -8,7 +8,7 @@ from datetime import time as dtime from .core.meta_trader import MetaTrader from .symbol import Symbol as _Symbol from .core import Config -from .contrib.backtester.meta_tester import MetaTester +# from .contrib.backtester.meta_tester import MetaTester from .sessions import Sessions, Session Symbol = TypeVar("Symbol", bound=_Symbol) @@ -48,7 +48,7 @@ class Strategy(ABC): 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() if self.config.mode == 'live' else MetaTester() + self.mt5 = MetaTrader() #if self.config.mode == 'live' else MetaTester() def __repr__(self): return f"{self.name}({self.symbol!r})"