This commit is contained in:
Ichinga Samuel
2024-09-26 06:07:04 +01:00
parent 693c475b23
commit a96cf6a4f5
16 changed files with 101 additions and 120 deletions
+1
View File
@@ -0,0 +1 @@
{"a": 5}{"af": 5}
+1 -1
View File
@@ -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 *
+1 -1
View File
@@ -1 +1 @@
from .backtester import *
# from .backtester import *
+1 -1
View File
@@ -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
@@ -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
+17 -38
View File
@@ -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)
View File
+10 -10
View File
@@ -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:
+5 -5
View File
@@ -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:
+14 -18
View File
@@ -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 {}
...
+2 -2
View File
@@ -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)
+15 -6
View File
@@ -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
+1 -1
View File
@@ -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
+3 -3
View File
@@ -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
+9 -9
View File
@@ -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()
+2 -2
View File
@@ -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})"