mirror of
https://github.com/Ichinga-Samuel/aiomql.git
synced 2026-08-16 13:28:08 +00:00
testdata
This commit is contained in:
@@ -16,5 +16,6 @@ from .history import History
|
||||
from .trader import Trader
|
||||
from .terminal import Terminal
|
||||
from .sessions import Session, Sessions
|
||||
from .utils import dict_to_string, round_off, find_bearish_fractal, find_bullish_fractal
|
||||
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 *
|
||||
|
||||
@@ -58,14 +58,14 @@ class Account(AccountInfo):
|
||||
await self.mt5.shutdown()
|
||||
self.connected = False
|
||||
|
||||
async def sign_in(self) -> bool:
|
||||
async def sign_in(self, **kwargs) -> bool:
|
||||
"""Connect to a trading account.
|
||||
|
||||
Returns:
|
||||
bool: True if login was successful else False
|
||||
"""
|
||||
acc = self.get_dict(include={'login', 'server', 'password'})
|
||||
self.connected = await self._login(acc=acc)
|
||||
self.connected = await self._login(acc=acc, **kwargs)
|
||||
if self.connected:
|
||||
await self.refresh()
|
||||
self.symbols = await self.symbols_get()
|
||||
@@ -73,18 +73,19 @@ class Account(AccountInfo):
|
||||
await self.mt5.shutdown()
|
||||
return False
|
||||
|
||||
async def _login(self, *, acc: dict, tries=3):
|
||||
async def _login(self, *, acc: dict, tries=3, **kwargs) -> bool:
|
||||
res = False
|
||||
if tries == 0:
|
||||
return False
|
||||
ini = await self.mt5.initialize(**acc, path=self.config.path)
|
||||
init_args = {**acc} | {'path': self.config.path} | {**kwargs}
|
||||
ini = await self.mt5.initialize(**init_args)
|
||||
if ini:
|
||||
res = await self.mt5.login(**acc)
|
||||
if ini and res:
|
||||
return True
|
||||
else:
|
||||
await asyncio.sleep(5+tries)
|
||||
return await self._login(acc=acc, tries=tries-1)
|
||||
return await self._login(acc=acc, tries=tries-1, **kwargs)
|
||||
|
||||
def has_symbol(self, symbol: str | SymbolInfo):
|
||||
"""Checks to see if a symbol is available for a trading account.
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
from .backtester import *
|
||||
@@ -0,0 +1,7 @@
|
||||
from .meta_tester import MetaTester
|
||||
from .test_data import TestData
|
||||
from .get_data import GetData
|
||||
from .test_strategy import TestStrategy
|
||||
from .event_manager import EventManager
|
||||
from .strategy_tester import StrategyTester
|
||||
# from .test_executor import FingerTrapTest
|
||||
@@ -0,0 +1,95 @@
|
||||
import signal
|
||||
import asyncio
|
||||
from asyncio import Condition, Task
|
||||
import random
|
||||
|
||||
|
||||
class EventManager:
|
||||
|
||||
def __init__(self, num_tasks: int, lock = None):
|
||||
self.event = Condition(lock=lock)
|
||||
self.num_tasks = num_tasks
|
||||
self.counter = 0
|
||||
self.state = 0
|
||||
self.tasks: list[Task] = []
|
||||
|
||||
async def sleep(self, secs):
|
||||
while secs > self.state:
|
||||
await self.wait()
|
||||
|
||||
async def acquire(self):
|
||||
await self.event.acquire()
|
||||
|
||||
def notify_all(self):
|
||||
self.event.notify_all()
|
||||
|
||||
def sigint_handler(self, sig, frame):
|
||||
for task in self.tasks:
|
||||
print(task.get_name())
|
||||
task.cancel() if not task.done() else ...
|
||||
|
||||
async def event_monitor(self):
|
||||
while True:
|
||||
async with self.event:
|
||||
if self.counter == self.num_tasks:
|
||||
self.counter = 0
|
||||
self.state += 1
|
||||
self.event.notify_all()
|
||||
await asyncio.sleep(0)
|
||||
|
||||
async def wait(self):
|
||||
self.counter += 1
|
||||
await self.event.wait()
|
||||
|
||||
def release(self):
|
||||
self.event.release()
|
||||
|
||||
async def long_task1(event: EventManager):
|
||||
counter = 0
|
||||
while True:
|
||||
await event.acquire()
|
||||
try:
|
||||
await event.wait()
|
||||
sleep = random.randint(1, 2)
|
||||
await asyncio.sleep(sleep)
|
||||
counter += 1
|
||||
print(f'task 1: {event.state}-{counter}')
|
||||
finally:
|
||||
event.release()
|
||||
|
||||
async def long_task2(event: EventManager):
|
||||
counter = 0
|
||||
while True:
|
||||
await event.acquire()
|
||||
try:
|
||||
await event.wait()
|
||||
sleep = random.randint(1, 2)
|
||||
await asyncio.sleep(sleep)
|
||||
counter += 1
|
||||
print(f'task 2: {event.state}-{counter}')
|
||||
finally:
|
||||
event.release()
|
||||
|
||||
async def long_task3(event: EventManager):
|
||||
sleep = 10
|
||||
while True:
|
||||
await event.acquire()
|
||||
try:
|
||||
await event.wait()
|
||||
await event.sleep(sleep)
|
||||
print(f'task 3: {event.state}-{sleep}')
|
||||
sleep += 10
|
||||
finally:
|
||||
event.release()
|
||||
|
||||
async def main():
|
||||
manager = EventManager(num_tasks=3)
|
||||
signal.signal(signal.SIGINT, manager.sigint_handler)
|
||||
task1 = asyncio.create_task(long_task1(manager), name='task1')
|
||||
task2 = asyncio.create_task(long_task2(manager), name='task2')
|
||||
task3 = asyncio.create_task(long_task3(manager), name='task3')
|
||||
control = asyncio.create_task(manager.event_monitor(), name='monitor')
|
||||
manager.tasks.extend([task1, task2, task3, control])
|
||||
res = await asyncio.gather(task1, task2, task3, control)
|
||||
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,43 @@
|
||||
from functools import wraps
|
||||
from dataclasses import dataclass, fields, field
|
||||
from typing import ClassVar
|
||||
|
||||
def dd(func):
|
||||
|
||||
@wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
print(func.__name__)
|
||||
return func(*args, **kwargs)
|
||||
return wrapper
|
||||
|
||||
class C:
|
||||
def __new__(cls, *args, **kwargs):
|
||||
if not hasattr(cls, '_instance'):
|
||||
cls._instance = super().__new__(cls)
|
||||
cls._instance.tasks = []
|
||||
# cls.__init__(a)
|
||||
[setattr(cls._instance, k, v) for k, v in kwargs.items()]
|
||||
return cls._instance
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
print('receiving args')
|
||||
|
||||
|
||||
|
||||
|
||||
@dataclass
|
||||
class D:
|
||||
b: int = 0
|
||||
c: str = ''
|
||||
_fields: list[ClassVar[str]] = field(default_factory=list)
|
||||
|
||||
@dd
|
||||
def setattrs(self, **kwargs):
|
||||
[setattr(self, k, v) for k, v in kwargs.items() if k in self.fields]
|
||||
|
||||
@property
|
||||
def fields(self):
|
||||
return self._fields or [name for f in fields(self) if (name := f.name) != '_fields']
|
||||
|
||||
d = D()
|
||||
d.setattrs(r=3)
|
||||
@@ -0,0 +1,57 @@
|
||||
import asyncio
|
||||
from asyncio import Condition, Task
|
||||
from typing import Self
|
||||
|
||||
from ...core import Config
|
||||
|
||||
|
||||
class EventManager:
|
||||
_instance: Self
|
||||
task_tracker: int
|
||||
config: Config
|
||||
tasks: list[Task]
|
||||
num_main_tasks: int # main tasks that are directly controlled by the Condition Synchronization primitives
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
if not hasattr(cls, "_instance"):
|
||||
cls._instance = super().__new__(cls)
|
||||
cls._instance.config = Config()
|
||||
cls._instance.condition = Condition()
|
||||
cls._instance.num_main_tasks = 0
|
||||
cls._instance.task_tracker = 0
|
||||
cls._instance.tasks = []
|
||||
return cls._instance
|
||||
|
||||
def __init__(self, *, num_tasks: int = 0):
|
||||
self.num_main_tasks = num_tasks or self.num_main_tasks
|
||||
|
||||
def add_task(self, *task: Task):
|
||||
self.tasks.extend(task)
|
||||
|
||||
def sigint_handler(self, sig, frame):
|
||||
for task in self.tasks:
|
||||
task.cancel() if not task.done() else ...
|
||||
|
||||
async def acquire(self):
|
||||
await self.condition.acquire()
|
||||
|
||||
def notify_all(self):
|
||||
self.condition.notify_all()
|
||||
|
||||
async def event_monitor(self):
|
||||
while True:
|
||||
async with self.condition:
|
||||
if self.task_tracker == self.num_main_tasks:
|
||||
self.task_tracker = 0
|
||||
await self.config.test_data.tracker()
|
||||
self.config.test_data.next()
|
||||
print(self.config.test_data.cursor.time)
|
||||
self.condition.notify_all()
|
||||
await asyncio.sleep(0)
|
||||
|
||||
async def wait(self):
|
||||
self.task_tracker += 1
|
||||
await self.condition.wait()
|
||||
|
||||
def release(self):
|
||||
self.condition.release()
|
||||
+84
-38
@@ -1,9 +1,11 @@
|
||||
from dataclasses import dataclass
|
||||
from dataclasses import dataclass, field, fields
|
||||
import pickle
|
||||
from pathlib import Path
|
||||
import lzma
|
||||
from datetime import datetime
|
||||
from logging import getLogger
|
||||
import asyncio
|
||||
from typing import Sequence, ClassVar
|
||||
|
||||
import pytz
|
||||
import pandas as pd
|
||||
@@ -18,83 +20,127 @@ from ...utils import backoff_decorator
|
||||
logger = getLogger(__name__)
|
||||
from MetaTrader5 import TradePosition, TradeOrder, TradeDeal
|
||||
|
||||
tof = list(TradeOrder._fields)
|
||||
tof = list(TradeOrder.__match_args__)
|
||||
tof.append('symbol')
|
||||
tpf = list(TradePosition._fields)
|
||||
tpf = list(TradePosition.__match_args__)
|
||||
tpf.append('symbol')
|
||||
tdf = list(TradeDeal._fields)
|
||||
tdf = list(TradeDeal.__match_args__)
|
||||
tdf.append('symbol')
|
||||
|
||||
|
||||
@dataclass
|
||||
class Data:
|
||||
account: dict
|
||||
symbols: dict[str, dict]
|
||||
prices: dict[str, DataFrame]
|
||||
ticks: dict[str, DataFrame]
|
||||
rates: dict[str, dict[str, DataFrame]]
|
||||
span: range
|
||||
range: range
|
||||
history_orders: DataFrame = DataFrame([], columns=tof)
|
||||
history_deals: DataFrame = DataFrame([], columns=tdf)
|
||||
positions: DataFrame = DataFrame([], columns=tpf)
|
||||
name: str = ''
|
||||
terminal: dict[str, [str | int | bool | float]] = field(default_factory=dict)
|
||||
version: tuple[int, int, str] = (0, 0, '')
|
||||
account: dict = field(default_factory=dict)
|
||||
symbols: dict[str, dict] = field(default_factory=dict)
|
||||
prices: dict[str, DataFrame] = field(default_factory=dict)
|
||||
ticks: dict[str, DataFrame] = field(default_factory=dict)
|
||||
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)
|
||||
|
||||
def setattrs(self, **kwargs):
|
||||
[setattr(self, k, v) for k, v in kwargs.items() if k in self.fields]
|
||||
|
||||
@property
|
||||
def fields(self):
|
||||
return self._fields or [name for f in fields(self) if (name := f.name) != '_fields']
|
||||
|
||||
|
||||
class GetData:
|
||||
data: Data | None
|
||||
|
||||
def __init__(self, *, start: datetime, end: datetime, timeframes: set[TimeFrame], symbols: set[str],
|
||||
name: str = '', tz: str = 'Etc/UTC'):
|
||||
def __init__(self, *, start: datetime, end: datetime, symbols: Sequence[str],
|
||||
timeframes: Sequence[TimeFrame], name: str = '', tz: str = 'Etc/UTC'):
|
||||
""""""
|
||||
self.config = Config()
|
||||
self.tz = pytz.timezone(tz)
|
||||
self.start = start.replace(tzinfo=self.tz)
|
||||
self.end = end.replace(tzinfo=self.tz)
|
||||
self.symbols = symbols
|
||||
self.timeframes = timeframes
|
||||
self.symbols = set(symbols)
|
||||
self.timeframes = set(timeframes)
|
||||
self.name = name or f"{start:%d-%m-%y}_{end:%d-%m-%y}"
|
||||
diff = int((self.end - self.start).total_seconds())
|
||||
self.range = range(diff)
|
||||
self.span = range(start := int(self.start.timestamp()), diff + start)
|
||||
self.data = Data(name=name)
|
||||
self.mt5 = MetaTrader()
|
||||
|
||||
async def get_data(self) -> Data:
|
||||
async def get_data(self):
|
||||
""""""
|
||||
|
||||
rates, ticks, prices, symbols, account = await asyncio.gather(self.get_symbols_rates(), self.get_symbols_ticks(),
|
||||
self.get_symbols_prices(), self.get_symbols_info(),
|
||||
self.get_account_info())
|
||||
return Data(account=account, symbols=symbols, prices=prices, ticks=ticks, rates=rates,
|
||||
span=self.span, range=self.range)
|
||||
terminal, version = await asyncio.gather(self.get_terminal_info(), self.get_version())
|
||||
|
||||
async def pickle_data(self) -> None:
|
||||
self.data.setattrs(account=account, symbols=symbols, prices=prices, ticks=ticks, rates=rates,
|
||||
span=self.span, range=self.range, terminal=terminal, version=version, name=self.name)
|
||||
|
||||
def pickle_data(self):
|
||||
""""""
|
||||
data = await self.get_data()
|
||||
fh = open(f'{self.config.root}/data/{self.name}', 'wb')
|
||||
pickle.dump(data, fh)
|
||||
fh = open(f'{self.config.test_data_dir}/{self.name}', 'wb')
|
||||
pickle.dump(self.data, fh)
|
||||
fh.close()
|
||||
|
||||
async def compress_data(self):
|
||||
""""""
|
||||
data = await self.get_data()
|
||||
bdata = pickle.dumps(data)
|
||||
bdata = pickle.dumps(self.data)
|
||||
name = self.name + 'xz'
|
||||
with lzma.open(name, 'w') as fh:
|
||||
with lzma.open(f'{self.config.test_data_dir}/{name}', 'w') as fh:
|
||||
fh.write(bdata)
|
||||
|
||||
@classmethod
|
||||
def load_data(cls, name: str, compressed=False) -> dict:
|
||||
def dump_data(cls, data: Data, name: str | Path, compress: bool = False) -> None:
|
||||
""""""
|
||||
fo = open(f'{cls.config.root}/data/{name}', 'rb')
|
||||
data = fo.read()
|
||||
try:
|
||||
fo = open(name, 'wb')
|
||||
if compress:
|
||||
data = lzma.compress(pickle.dumps(data))
|
||||
else:
|
||||
data = pickle.dumps(data)
|
||||
|
||||
if compressed:
|
||||
data = lzma.decompress(data)
|
||||
else:
|
||||
data = pickle.loads(data)
|
||||
fo.write(data)
|
||||
fo.close()
|
||||
except Exception as err:
|
||||
logger.error(f"Error in dump_data: {err}")
|
||||
|
||||
fo.close()
|
||||
|
||||
return data
|
||||
@classmethod
|
||||
def load_data(cls, *, name: str | Path, compressed=False) -> Data | None:
|
||||
""""""
|
||||
try:
|
||||
fo = open(name, 'rb')
|
||||
data = fo.read()
|
||||
|
||||
if compressed:
|
||||
data = lzma.decompress(data)
|
||||
else:
|
||||
data = pickle.loads(data)
|
||||
|
||||
fo.close()
|
||||
|
||||
return data
|
||||
except Exception as err:
|
||||
logger.error(f"Error: {err}")
|
||||
return None
|
||||
|
||||
async def get_terminal_info(self) -> dict[str, [str | int | bool | float]]:
|
||||
""""""
|
||||
terminal = await self.mt5.terminal_info()
|
||||
return terminal._asdict()
|
||||
|
||||
async def get_version(self) -> tuple[int, int, str]:
|
||||
""""""
|
||||
version = await self.mt5.version()
|
||||
return version
|
||||
|
||||
async def get_symbols_info(self) -> dict[str, dict]:
|
||||
""""""
|
||||
@@ -0,0 +1,188 @@
|
||||
from datetime import datetime
|
||||
from logging import getLogger
|
||||
|
||||
from numpy import ndarray
|
||||
from MetaTrader5 import (Tick, SymbolInfo, AccountInfo, TerminalInfo, TradeOrder, TradePosition, TradeDeal,
|
||||
OrderCheckResult, OrderSendResult)
|
||||
|
||||
from .test_data import TestData
|
||||
from .get_data import GetData
|
||||
|
||||
from ...core.meta_trader import MetaTrader
|
||||
from ...core.constants import TimeFrame, CopyTicks, OrderType
|
||||
from ...utils import error_handler
|
||||
|
||||
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):
|
||||
super().__init__()
|
||||
if self.test_data:
|
||||
self.config.test_data = test_data
|
||||
|
||||
@property
|
||||
def test_data(self) -> TestData | None:
|
||||
test_data = self.config.test_data
|
||||
if test_data is None:
|
||||
...
|
||||
# logger.error('No Test Data Available')
|
||||
return test_data
|
||||
|
||||
@test_data.setter
|
||||
def test_data(self, value: TestData):
|
||||
self.config.test_data = value
|
||||
|
||||
async def initialize(self, path: str = "", login: int = 0, password: str = "", server: str = "",
|
||||
timeout: int | None = None, portable=False, load_test_data: bool = False,
|
||||
test_data_file: str = '', use_terminal: bool = True) -> bool:
|
||||
|
||||
success = True
|
||||
if self.config.use_terminal_for_backtesting:
|
||||
success = await super().initialize(path=path, login=login, password=password, server=server, timeout=timeout)
|
||||
|
||||
try:
|
||||
if load_test_data:
|
||||
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)
|
||||
success = True
|
||||
|
||||
except Exception as err:
|
||||
logger.error(f'{err}: unable to load test data')
|
||||
success = False
|
||||
|
||||
return success
|
||||
|
||||
async def login(self, login: int, password: str, server: str, timeout: int = 60000) -> bool:
|
||||
return await super().login(login, password, server, timeout) if self.config.use_terminal_for_backtesting else True
|
||||
|
||||
async def shutdown(self) -> None:
|
||||
await super().shutdown() if self.config.use_terminal_for_backtesting else ...
|
||||
self.test_data.save()
|
||||
name = self.test_data.data.name
|
||||
|
||||
if self.config.compress_test_data:
|
||||
name += '.xz'
|
||||
name = self.config.test_data_dir/name
|
||||
GetData.dump_data(data=self.test_data.data, name=name, compress=self.config.compress_test_data)
|
||||
|
||||
@error_handler(msg='test data not available', exe=AttributeError)
|
||||
async def terminal_info(self) -> TerminalInfo:
|
||||
return self.test_data.get_terminal_info()
|
||||
|
||||
@error_handler(msg='test data not available', exe=AttributeError)
|
||||
async def account_info(self) -> AccountInfo:
|
||||
""""""
|
||||
return self.test_data.get_account_info()
|
||||
|
||||
@error_handler(msg='test data not available', exe=AttributeError)
|
||||
async def symbol_select(self, symbol: str, enable: bool = True) -> bool:
|
||||
return symbol in self.test_data.symbols and enable
|
||||
|
||||
@error_handler(msg='test data not available', exe=AttributeError)
|
||||
async def symbols_total(self) -> int:
|
||||
return self.test_data.get_symbols_total()
|
||||
|
||||
@error_handler(msg='test data not available', exe=AttributeError)
|
||||
async def symbols_get(self, group: str = "") -> tuple[SymbolInfo, ...] | None:
|
||||
""""""
|
||||
return self.test_data.get_symbols(group)
|
||||
|
||||
@error_handler(msg='test data not available', exe=AttributeError)
|
||||
async def symbol_info(self, symbol: str) -> SymbolInfo | None:
|
||||
return self.test_data.symbols.get(symbol)
|
||||
|
||||
@error_handler(msg='test data not available', exe=AttributeError)
|
||||
async def symbol_info_tick(self, symbol: str) -> Tick | None:
|
||||
return self.test_data.get_symbol_info_tick(symbol)
|
||||
|
||||
@error_handler(msg='test data not available', exe=AttributeError)
|
||||
async def copy_rates_from(self, symbol: str, timeframe: TimeFrame, date_from: datetime | float,
|
||||
count: int) -> ndarray | None:
|
||||
return self.test_data.get_rates_from(symbol, timeframe, date_from, count)
|
||||
|
||||
@error_handler(msg='test data not available', exe=AttributeError)
|
||||
async def copy_rates_from_pos(self, symbol: str, timeframe: TimeFrame, start_pos: int,
|
||||
count: int) -> ndarray | None:
|
||||
return self.test_data.get_rates_from_pos(symbol, timeframe, start_pos, count)
|
||||
|
||||
@error_handler(msg='test data not available', exe=AttributeError)
|
||||
async def copy_rates_range(self, symbol: str, timeframe: TimeFrame, date_from: datetime | float,
|
||||
date_to: datetime | float) -> ndarray | None:
|
||||
return self.test_data.get_rates_range(symbol, timeframe, date_from, date_to)
|
||||
|
||||
@error_handler(msg='test data not available', exe=AttributeError)
|
||||
async def copy_ticks_from(self, symbol: str, date_from: datetime | float, count: int,
|
||||
flags: CopyTicks) -> ndarray | None:
|
||||
return self.test_data.get_ticks_from(symbol, date_from, count, flags)
|
||||
|
||||
@error_handler(msg='test data not available', exe=AttributeError)
|
||||
async def copy_ticks_range(self, symbol: str, date_from: datetime | float, date_to: datetime | float,
|
||||
flags: CopyTicks) -> ndarray | None:
|
||||
return self.test_data.get_ticks_range(symbol, date_from, date_to, flags)
|
||||
|
||||
@error_handler(msg='test data not available', exe=AttributeError)
|
||||
async def orders_total(self) -> int:
|
||||
return self.test_data.get_orders_total()
|
||||
|
||||
@error_handler(msg='test data not available', exe=AttributeError)
|
||||
async def orders_get(self, group: str = "", ticket: int = 0, symbol: str = "") -> tuple[TradeOrder, ...] | None:
|
||||
kwargs = {key: value for key, value in (('group', group), ('ticket', ticket), ('symbol', symbol)) if value}
|
||||
return self.test_data.get_orders(**kwargs)
|
||||
|
||||
@error_handler(msg='test data not available', exe=AttributeError)
|
||||
async def order_calc_margin(self, action: OrderType, symbol: str, volume: float,
|
||||
price: float, use_terminal: bool = True) -> float | None:
|
||||
res = await self.test_data.order_calc_margin(action, symbol, volume, price, use_terminal=use_terminal)
|
||||
return res
|
||||
|
||||
@error_handler(msg='test data not available', exe=AttributeError)
|
||||
async def order_calc_profit(self, action: OrderType, symbol: str, volume: float, price_open: float,
|
||||
price_close: float, use_terminal: bool = True) -> float | None:
|
||||
return await self.test_data.order_calc_profit(action, symbol, volume,
|
||||
price_open, price_close, use_terminal=use_terminal)
|
||||
|
||||
@error_handler(msg='test data not available', exe=AttributeError)
|
||||
async def order_check(self, request: dict, use_terminal: bool = True) -> OrderCheckResult:
|
||||
return await self.test_data.order_check(request, use_terminal=use_terminal)
|
||||
|
||||
async def order_send(self, request: dict, use_terminal: bool = True) -> OrderSendResult:
|
||||
return await self.test_data.order_send(request, use_terminal=use_terminal)
|
||||
|
||||
@error_handler(msg='test data not available', exe=AttributeError)
|
||||
async def positions_total(self) -> int:
|
||||
return self.test_data.get_positions_total()
|
||||
|
||||
@error_handler(msg='test data not available', exe=AttributeError)
|
||||
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}
|
||||
return self.test_data.get_positions(**kwargs)
|
||||
|
||||
@error_handler(msg='test data not available', exe=AttributeError)
|
||||
async def history_orders_total(self, date_from: datetime | float, date_to: datetime | float) -> int:
|
||||
return self.test_data.get_history_orders_total(date_from, date_to)
|
||||
|
||||
@error_handler(msg='test data not available', exe=AttributeError)
|
||||
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)
|
||||
return self.test_data.get_history_orders(*args, **kwargs)
|
||||
|
||||
@error_handler(msg='test data not available', exe=AttributeError)
|
||||
async def history_deals_total(self, date_from: datetime | float, date_to: datetime | float) -> int:
|
||||
return self.test_data.get_history_deals_total(date_from, date_to)
|
||||
|
||||
@error_handler(msg='test data not available', exe=AttributeError)
|
||||
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)
|
||||
return self.test_data.get_history_deals(*args, **kwargs)
|
||||
@@ -0,0 +1,35 @@
|
||||
import asyncio
|
||||
|
||||
from .event_manager import EventManager
|
||||
from .get_data import GetData
|
||||
from .test_data import TestData
|
||||
from .meta_tester import MetaTester
|
||||
|
||||
from ...core import Config
|
||||
|
||||
class StrategyTester:
|
||||
def __init__(self, *, strategies: list = None, test_data: TestData = None, test_data_file: str = ''):
|
||||
self.config = Config()
|
||||
self.mt5 = MetaTester()
|
||||
self.strategies = strategies or []
|
||||
self.test_data = test_data or self.get_test_data(name=test_data_file)
|
||||
self.config.test_data = self.test_data
|
||||
self.event_manager = EventManager(num_tasks=len(self.strategies))
|
||||
|
||||
def get_test_data(self, name: str) -> TestData | None:
|
||||
name = f"{self.config.test_data_dir_name}/{name or self.config.test_data_file}"
|
||||
data = GetData.load_data(name=name, compressed=self.config.compress_test_data)
|
||||
return TestData(data) if data is not None else None
|
||||
|
||||
async def start(self):
|
||||
acc = self.config.account_info()
|
||||
await self.mt5.initialize(**acc)
|
||||
await self.mt5.login(**acc)
|
||||
|
||||
async def run(self):
|
||||
await self.start()
|
||||
tasks = [*[asyncio.create_task(strategy.test()) for strategy in self.strategies],
|
||||
asyncio.create_task(self.event_manager.event_monitor())]
|
||||
self.event_manager.add_task(*tasks)
|
||||
await asyncio.gather(*tasks)
|
||||
await self.mt5.shutdown()
|
||||
@@ -0,0 +1,37 @@
|
||||
from dataclasses import dataclass, asdict
|
||||
from ...core.constants import AccountTradeMode, AccountMarginMode, AccountStopOutMode
|
||||
|
||||
|
||||
@dataclass
|
||||
class AccountInfo:
|
||||
login: int = 0
|
||||
server: str = ''
|
||||
trade_mode: AccountTradeMode = AccountTradeMode.DEMO
|
||||
balance: float = 0
|
||||
leverage: float = 0
|
||||
profit: float = 0
|
||||
equity: float = 0
|
||||
credit: float = 0
|
||||
margin: float = 0
|
||||
margin_level: float = 0
|
||||
margin_free: float = 0
|
||||
margin_mode: AccountMarginMode = AccountMarginMode.EXCHANGE
|
||||
margin_so_mode: AccountStopOutMode = AccountStopOutMode.PERCENT
|
||||
margin_so_call: float = 0
|
||||
margin_so_so: float = 0
|
||||
margin_initial: float = 0
|
||||
margin_maintenance: float = 0
|
||||
fifo_close: bool = False
|
||||
limit_orders: float = 0
|
||||
currency: str = "USD"
|
||||
trade_allowed: bool = True
|
||||
trade_expert: bool = True
|
||||
currency_digits: int = 2
|
||||
assets: float = 0
|
||||
liabilities: float = 0
|
||||
commission_blocked: float = 0
|
||||
name: str = ''
|
||||
company: str = ''
|
||||
|
||||
def asdict(self):
|
||||
return asdict(self)
|
||||
+281
-177
@@ -1,20 +1,23 @@
|
||||
import asyncio
|
||||
from collections import namedtuple
|
||||
from datetime import datetime
|
||||
from typing import Literal
|
||||
from itertools import zip_longest
|
||||
import random
|
||||
|
||||
import pandas as pd
|
||||
import pytz
|
||||
import numpy as np
|
||||
from pandas import DataFrame
|
||||
from MetaTrader5 import (Tick, SymbolInfo, AccountInfo, TradeOrder, TradePosition, TradeDeal,
|
||||
ORDER_TYPE_BUY, ORDER_TYPE_SELL, TradeRequest, OrderCheckResult, OrderSendResult,
|
||||
ACCOUNT_STOPOUT_MODE_PERCENT)
|
||||
from ..meta_trader import MetaTrader
|
||||
from ..constants import TimeFrame, CopyTicks, OrderType, TradeAction
|
||||
TradeRequest, OrderCheckResult, OrderSendResult, TerminalInfo)
|
||||
|
||||
from ...core.meta_trader import MetaTrader
|
||||
from ...core.constants import TimeFrame, CopyTicks, OrderType, TradeAction, AccountStopOutMode
|
||||
from .get_data import Data
|
||||
from ...account import Account
|
||||
from ...utils import round_down, round_up
|
||||
from .test_account import AccountInfo as Account
|
||||
from ...utils import round_down, round_up, error_handler, error_handler_sync
|
||||
# from .event_manager import EventManager
|
||||
|
||||
tz = pytz.timezone('Etc/UTC')
|
||||
Cursor = namedtuple('Cursor', ['index', 'time'])
|
||||
@@ -26,16 +29,17 @@ class TestData:
|
||||
|
||||
def __init__(self, data: Data):
|
||||
self._data = data
|
||||
self.account = Account(**data.account)
|
||||
self.symbols = {symbol: SymbolInfo(**info) for symbol, info in data.symbols.items()}
|
||||
self.prices = data.prices
|
||||
self.ticks = data.ticks
|
||||
self.rates = data.rates
|
||||
self.span = data.span
|
||||
self.range = data.range
|
||||
self.cursor = Cursor(index=self.range[0], time=self.span[0])
|
||||
self.iter = zip_longest(self.range, self.span)
|
||||
self.version: tuple[int, int, str] = data.version
|
||||
self.terminal_info = TerminalInfo(data.terminal)
|
||||
self.account: Account = Account(**data.account)
|
||||
self.symbols: dict[str, SymbolInfo] = {symbol: SymbolInfo(info) for symbol, info in data.symbols.items()}
|
||||
self.prices: dict[str, DataFrame] = data.prices
|
||||
self.ticks: dict[str, DataFrame] = data.ticks
|
||||
self.rates: dict[str, dict[str, DataFrame]] = data.rates
|
||||
self.span: range = data.span
|
||||
self.range: range = data.range
|
||||
self.orders: dict[str, dict[int, TradeOrder]] = {}
|
||||
self.deals: dict[str, dict[int, TradeDeal]] = {}
|
||||
self.open_orders: dict[int, TradeOrder] = {}
|
||||
self.positions: dict[str, dict[int, TradePosition]] = {}
|
||||
self.open_positions: dict[int, TradePosition] = {}
|
||||
@@ -43,38 +47,261 @@ class TestData:
|
||||
self.history_deals = data.history_deals
|
||||
self.margins: dict[int, float] = {}
|
||||
self.mt5 = MetaTrader()
|
||||
self.iter = zip_longest(self.range, self.span)
|
||||
self.cursor = next(self)
|
||||
# self.event_manager = EventManager()
|
||||
|
||||
def __next__(self):
|
||||
def __next__(self) -> Cursor:
|
||||
index, time = next(self.iter)
|
||||
self.cursor = Cursor(index=index, time=time)
|
||||
return self.cursor
|
||||
|
||||
def next(self) -> Cursor:
|
||||
return next(self)
|
||||
|
||||
@property
|
||||
def data(self):
|
||||
return self._data
|
||||
|
||||
def reset(self):
|
||||
self.iter = zip_longest(self.range, self.span)
|
||||
self.cursor = Cursor(index=self.range[0], time=self.span[0])
|
||||
return self.cursor
|
||||
|
||||
def go_to(self, index: int, time: int):
|
||||
range_ = range(time, self.range.stop, self.range.step)
|
||||
span = range(index, self.span.stop, self.span.step)
|
||||
self.iter = zip_longest(range_, span)
|
||||
self.cursor = next(self)
|
||||
|
||||
def get_dtype(self, df: DataFrame) -> list[tuple[str, str]]:
|
||||
return [(c, t) for c, t in zip(df.columns, df.dtypes)]
|
||||
|
||||
async def tracker(self):
|
||||
pos_tasks = [self.check_position(ticket) for ticket in self.open_positions]
|
||||
await asyncio.gather(*pos_tasks)
|
||||
order_tasks = [self.check_order(ticket) for ticket in self.open_orders]
|
||||
await asyncio.gather(*order_tasks)
|
||||
|
||||
def save(self):
|
||||
self._data.history_deals = self.history_deals
|
||||
self._data.history_orders = self.history_orders
|
||||
for symbol in self.orders:
|
||||
self.history_orders = pd.concat([DataFrame(self.orders[symbol].values()), self.history_orders])
|
||||
self._data.history_orders = self.history_orders
|
||||
for symbol in self.deals:
|
||||
self.history_deals = pd.concat([DataFrame(self.deals[symbol].values()), self.history_deals])
|
||||
self._data.history_deals = self.history_deals
|
||||
|
||||
@error_handler
|
||||
async def check_order(self, ticket: int):
|
||||
order = self.open_orders[ticket]
|
||||
order_type, symbol = order.type, order.symbol
|
||||
tick = self.prices[symbol].loc[self.cursor.time]
|
||||
tp, sl = order.tp, order.sl
|
||||
|
||||
match order_type:
|
||||
case OrderType.BUY:
|
||||
if tp >= tick.bid or sl <= tick.bid:
|
||||
self.close_position(ticket)
|
||||
|
||||
case OrderType.SELL:
|
||||
if tp <= tick.ask or sl >= tick.ask:
|
||||
self.close_position(ticket)
|
||||
case _:
|
||||
...
|
||||
|
||||
@error_handler
|
||||
async def check_position(self, ticket: int, use_terminal=True):
|
||||
pos = self.open_positions[ticket]
|
||||
order_type, symbol, volume, price_open, prev_profit = pos.type, pos.symbol, pos.volume, pos.price_open, pos.profit
|
||||
tick = self.prices[symbol].loc[self.cursor.time]
|
||||
price_current = tick.bid if order_type == OrderType.BUY else tick.ask
|
||||
profit = await self.order_calc_profit(order_type, symbol, volume, price_open, price_current, use_terminal)
|
||||
self.update_account(equity=profit - prev_profit)
|
||||
pos = pos._asdict()
|
||||
pos.update(profit=profit, price_current=price_current, time_update=self.cursor.time)
|
||||
pos = TradePosition(pos)
|
||||
self.open_positions[ticket] = pos
|
||||
self.positions[symbol][ticket] = pos
|
||||
|
||||
def close_position(self, ticket: int):
|
||||
position = self.open_positions.pop(ticket)
|
||||
margin = self.margins.pop(position.ticket)
|
||||
order = self.open_orders.pop(ticket)
|
||||
order = order._asdict()
|
||||
order.update(time_done=self.cursor.time)
|
||||
self.orders[order['symbol']][ticket] = TradeOrder(order)
|
||||
self.update_account(profit=position.profit, margin=-margin)
|
||||
|
||||
def modify_stops(self, ticket: int, sl: int = None, tp: int = None):
|
||||
pos = self.open_positions.pop(ticket)
|
||||
order = self.open_orders.pop(ticket)
|
||||
sl = sl or pos.sl
|
||||
tp = tp or pos.tp
|
||||
pos = pos._asdict()
|
||||
pos.update(tp=tp, sl=sl, time_update=self.cursor.time)
|
||||
sl = sl or order.sl
|
||||
tp = tp or order.tp
|
||||
order = order._asdict()
|
||||
order.update(tp=tp, sl=sl)
|
||||
pos = TradePosition(pos)
|
||||
order = TradeOrder(order)
|
||||
self.open_positions[ticket] = pos
|
||||
self.open_orders[ticket] = order
|
||||
self.positions[pos.symbol][ticket] = pos
|
||||
self.orders[order.symbol][ticket] = order
|
||||
|
||||
def update_account(self, *, profit: float = 0, margin: float = 0, equity: float = 0):
|
||||
self.account.balance += profit
|
||||
self.account.equity += equity
|
||||
self.account.margin += margin
|
||||
self.account.margin_free = self.account.equity - self.account.margin
|
||||
self.account.margin_level = (self.account.equity / (self.account.margin or 1)) * 100 \
|
||||
if self.account.margin_mode == AccountStopOutMode.PERCENT else self.account.margin_free
|
||||
|
||||
@error_handler
|
||||
async def order_send(self, request: dict, use_terminal: bool = True) -> OrderSendResult:
|
||||
osr = {'retcode': 10009, 'comment': 'Request completed', 'request': TradeRequest(request)}
|
||||
|
||||
if (position := request.get('position')) in self.open_positions:
|
||||
pos = self.open_positions[position]
|
||||
order_type = OrderType(request['type'])
|
||||
pos_type = OrderType(pos.type)
|
||||
if order_type.opposite == pos_type: # ToDo: is there another way to check if the order is a close order?
|
||||
# close position
|
||||
self.close_position(pos.ticket)
|
||||
return OrderSendResult(osr) # ToDo: Create a deal object here
|
||||
action = request['action']
|
||||
if action == TradeAction.SLTP:
|
||||
self.modify_stops(position, request['sl'], request['tp'])
|
||||
return OrderSendResult(osr)
|
||||
|
||||
if (action := request.get('action')) == TradeAction.DEAL:
|
||||
ocr = await self.order_check(request, use_terminal=use_terminal)
|
||||
if ocr.retcode != 0:
|
||||
osr.update({'comment': ocr.comment, 'retcode': ocr.retcode})
|
||||
return OrderSendResult(osr)
|
||||
|
||||
ticket = random.randint(100_000_000, 999_999_999)
|
||||
deal_ticket = random.randint(100_000_000, 999_999_999)
|
||||
tick = self.get_symbol_info_tick(request['symbol'])
|
||||
order_type = request['type']
|
||||
price = tick.ask if request['type'] == OrderType.BUY else tick.bid
|
||||
volume = request['volume']
|
||||
sl, tp = request.get('sl', 0), request.get('tp', 0)
|
||||
symbol = request['symbol']
|
||||
|
||||
pos = {'comment': 'open position', 'ticket': ticket, 'symbol': symbol, 'volume': volume,
|
||||
'price_open': price, 'price_current': price, 'type': order_type, 'profit': 0,
|
||||
'sl': sl, 'tp': tp, 'time': tick.time,
|
||||
'time_msc': tick.time_msc}
|
||||
|
||||
order = {'ticket': ticket, 'symbol': symbol, 'volume': volume, 'price': price, 'price_current': price,
|
||||
'price_open': price, 'type': order_type, 'time_setup': tick.time,
|
||||
'time_setup_msc': tick.time_msc, 'volume_current': volume, 'sl': sl, 'tp': tp, }
|
||||
|
||||
pos = TradePosition(pos)
|
||||
order = TradeOrder(order)
|
||||
# ToDo: Create a deal object here
|
||||
self.open_positions[pos.ticket] = pos
|
||||
self.open_orders[order.ticket] = order
|
||||
self.orders.setdefault(order.symbol, {})[order.ticket] = order
|
||||
self.positions.setdefault(pos.symbol, {})[pos.ticket] = pos
|
||||
osr.update({'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_terminal=use_terminal)
|
||||
self.margins[ticket] = margin
|
||||
self.update_account(margin=margin)
|
||||
return OrderSendResult(osr)
|
||||
|
||||
@error_handler
|
||||
async def order_check(self, request: dict, use_terminal=True) -> OrderCheckResult:
|
||||
action, symbol, volume = request.get('action'), request.get('symbol'), request.get('volume')
|
||||
price = request.get('price')
|
||||
ocr = {'retcode': 0, 'balance': 0, 'profit': 0, 'margin': 0, 'equity': 0, 'margin_free': 0,
|
||||
'margin_level': 0, 'comment': 'Done', request: TradeRequest(request)}
|
||||
|
||||
margin = 0
|
||||
if all([action, symbol, volume, price]):
|
||||
margin = await self.order_calc_margin(action, symbol, volume, price, use_terminal=use_terminal)
|
||||
|
||||
acc = self.get_account_info()
|
||||
equity = acc.equity
|
||||
used_margin = acc.margin + margin
|
||||
free_margin = acc.margin_free - margin
|
||||
margin_level = (equity / used_margin) * 100 if (
|
||||
acc.margin_mode == AccountStopOutMode.PERCENT and used_margin > 0) else free_margin
|
||||
|
||||
if use_terminal and self.mt5.config.use_terminal_for_backtesting:
|
||||
ocr_t = await self.mt5.order_check(request)
|
||||
# return order check result if invalid stops level are detected or bad request
|
||||
if ocr_t.retcode in (10016, 10013, 10014):
|
||||
return ocr_t
|
||||
|
||||
sym = self.symbols[symbol]
|
||||
tsl = sym.trade_stops_level
|
||||
sl, tp = request.get('sl', 0), request.get('tp', 0)
|
||||
|
||||
# check if the stops level is valid
|
||||
if tp or sl:
|
||||
min_sl = min(sl, tp)
|
||||
dsl = abs(price - min_sl) / sym.point
|
||||
if dsl < tsl:
|
||||
ocr['retcode'] = 10016
|
||||
ocr['comment'] = 'Invalid stops'
|
||||
return OrderCheckResult(ocr)
|
||||
|
||||
# check if the account has enough money
|
||||
if margin_level < acc.margin_so_call:
|
||||
ocr['retcode'] = 10019
|
||||
ocr['comment'] = 'No money'
|
||||
|
||||
# check volume
|
||||
if volume < sym.volume_min or volume > sym.volume_max:
|
||||
ocr['retcode'] = 10014
|
||||
ocr['comment'] = 'Invalid volume'
|
||||
|
||||
ocr.update({'balance': acc.balance, 'profit': acc.profit, 'margin': used_margin, 'equity': equity,
|
||||
'margin_free': free_margin, 'margin_level': margin_level})
|
||||
|
||||
return OrderCheckResult(ocr)
|
||||
|
||||
@error_handler_sync
|
||||
def get_terminal_info(self) -> TerminalInfo:
|
||||
return self.terminal_info
|
||||
|
||||
@error_handler_sync
|
||||
def get_version(self) -> tuple[int, int, str]:
|
||||
return self.version
|
||||
|
||||
@error_handler_sync
|
||||
def get_symbols_total(self) -> int:
|
||||
return len(self.symbols)
|
||||
|
||||
def get_symbols(self) -> list:
|
||||
return list(self.symbols.keys())
|
||||
@error_handler_sync
|
||||
def get_symbols(self, group: str = '') -> tuple[SymbolInfo, ...]:
|
||||
return tuple(list(self.symbols.values()))
|
||||
|
||||
@error_handler_sync
|
||||
def get_account_info(self) -> AccountInfo:
|
||||
return AccountInfo(**self.account._asdict())
|
||||
return AccountInfo(self.account.asdict())
|
||||
|
||||
@error_handler_sync
|
||||
def get_symbol_info_tick(self, symbol: str) -> Tick:
|
||||
tick = self.prices[symbol].iloc[self.cursor.index]
|
||||
return Tick(**tick)
|
||||
|
||||
return Tick(tick)
|
||||
|
||||
@error_handler_sync
|
||||
def get_symbol_info(self, symbol: str) -> SymbolInfo:
|
||||
info = self.symbols[symbol]
|
||||
tick = self.get_symbol_info_tick(symbol)
|
||||
info = info._asdict()
|
||||
info |= {'bid': tick.bid, 'bidhigh': tick.bid, 'bidlow': tick.bid, 'ask': tick.ask,
|
||||
'askhigh': tick.ask, 'asklow': tick.bid, 'last': tick.last, 'volume_real': tick.volume_real}
|
||||
return SymbolInfo(**info)
|
||||
return SymbolInfo(info)
|
||||
|
||||
@error_handler_sync
|
||||
def get_rates_from(self, symbol: str, timeframe: TimeFrame, date_from: datetime | float, count: int) -> np.ndarray:
|
||||
rates = self.rates[symbol][timeframe.name]
|
||||
start = int(datetime.timestamp(date_from)) if isinstance(date_from, datetime) else int(date_from)
|
||||
@@ -82,14 +309,16 @@ class TestData:
|
||||
start = rates[rates.index <= start].iloc[-1].name
|
||||
start = rates.index.get_loc(start)
|
||||
end = start + count
|
||||
return rates.iloc[start:end].to_numpy()
|
||||
return np.fromiter((tuple(i) for i in rates.iloc[start:end].iloc), dtype=self.get_dtype(rates))
|
||||
|
||||
@error_handler_sync
|
||||
def get_rates_from_pos(self, symbol: str, timeframe: TimeFrame, start_pos: int, count: int) -> np.ndarray:
|
||||
rates = self.rates[symbol][timeframe.name]
|
||||
end = -start_pos + count
|
||||
end = end or None
|
||||
return rates.iloc[-start_pos:end].to_numpy()
|
||||
return np.fromiter((tuple(i) for i in rates.iloc[-start_pos:end].iloc), dtype=self.get_dtype(rates))
|
||||
|
||||
@error_handler_sync
|
||||
def get_rates_range(self, symbol: str, timeframe: TimeFrame, date_from: datetime | float, date_to: datetime | float) -> np.ndarray:
|
||||
rates = self.rates[symbol][timeframe.name]
|
||||
start = int(datetime.timestamp(date_from)) if isinstance(date_from, datetime) else int(date_from)
|
||||
@@ -98,180 +327,50 @@ class TestData:
|
||||
end = int(datetime.timestamp(date_to)) if isinstance(date_to, datetime) else int(date_to)
|
||||
end = round_up(end, timeframe.time)
|
||||
end = rates[rates.index >= end].iloc[-1].name
|
||||
return rates.loc[start:end].to_numpy()
|
||||
return np.fromiter((tuple(i) for i in rates.loc[start:end].iloc), dtype=self.get_dtype(rates))
|
||||
|
||||
@error_handler_sync
|
||||
def get_ticks_from(self, symbol: str, date_from: datetime | float, count: int, flags: CopyTicks) -> np.ndarray:
|
||||
ticks = self.ticks[symbol]
|
||||
start = int(datetime.timestamp(date_from)) if isinstance(date_from, datetime) else int(date_from)
|
||||
start = ticks[ticks.index <= start].iloc[-1].name
|
||||
start = ticks.index.get_loc(start)
|
||||
end = start + count
|
||||
return ticks.iloc[start:end]
|
||||
|
||||
return np.fromiter((tuple(i) for i in ticks.iloc[start:end].iloc), dtype=self.get_dtype(ticks))
|
||||
|
||||
@error_handler_sync
|
||||
def get_ticks_range(self, symbol: str, date_from: datetime | float, date_to: datetime | float, flags) -> np.ndarray:
|
||||
ticks = self.ticks[symbol]
|
||||
start = int(datetime.timestamp(date_from)) if isinstance(date_from, datetime) else int(date_from)
|
||||
start = ticks[ticks.index <= start].iloc[-1].index
|
||||
end = int(datetime.timestamp(date_to)) if isinstance(date_to, datetime) else int(date_to)
|
||||
end = ticks[ticks.index >= end].iloc[-1].index
|
||||
return ticks.loc[start:end].to_numpy()
|
||||
return np.fromiter((tuple(i) for i in ticks.loc[start:end].iloc), dtype=self.get_dtype(ticks))
|
||||
|
||||
@error_handler
|
||||
async def order_calc_margin(self, action: Literal[OrderType.BUY, OrderType.SELL], symbol: str, volume: float,
|
||||
price: float, use_terminal=False):
|
||||
if use_terminal and self.mt5.config.use_terminal_for_backtesting:
|
||||
return await self.mt5.order_calc_margin(OrderType(action), symbol, volume, price)
|
||||
return await self.mt5.order_calc_margin(action, symbol, volume, price)
|
||||
sym = self.symbols[symbol]
|
||||
margin = (volume * sym.trade_contract_size * price) / (self.account.leverage / (sym.margin_initial or 1))
|
||||
return margin
|
||||
return round(margin, self.account.currency_digits)
|
||||
|
||||
@error_handler
|
||||
async def order_calc_profit(self, action: Literal[OrderType.BUY, OrderType.SELL], symbol: str, volume: float,
|
||||
price_open: float, price_close: float, use_terminal=True):
|
||||
if use_terminal and self.mt5.config.use_terminal_for_backtesting:
|
||||
return await self.mt5.order_calc_profit(action, symbol, volume, price_open, price_close)
|
||||
sym = self.symbols[symbol]
|
||||
profit = volume * sym.trade_contract_size * (price_close - price_open)
|
||||
return profit
|
||||
profit = (volume * sym.trade_contract_size *
|
||||
((price_close - price_open) if action == OrderType.BUY else (price_open - price_close)))
|
||||
return round(profit, self.account.currency_digits)
|
||||
|
||||
def check_order(self, ticket: int) -> bool:
|
||||
order = self.open_orders[ticket]
|
||||
order_type, symbol = order.type, order.symbol
|
||||
tick = self.prices[symbol].loc[self.cursor.time]
|
||||
tp, sl = order.tp, order.sl
|
||||
|
||||
match order_type:
|
||||
case self.mt5._ORDER_TYPE_BUY:
|
||||
if tp >= tick.bid or sl <= tick.bid:
|
||||
self.close_position(ticket)
|
||||
|
||||
case self.mt5.ORDER_TYPE_SELL:
|
||||
if tp <= tick.ask or sl >= tick.ask:
|
||||
self.close_position(ticket)
|
||||
|
||||
case _:
|
||||
...
|
||||
|
||||
def check_position(self, ticket: int) -> bool:
|
||||
...
|
||||
|
||||
def close_position(self, ticket: int):
|
||||
position = self.open_positions.pop(ticket)
|
||||
margin = self.margins.pop(position.ticket)
|
||||
profit = position.profit
|
||||
self.update_account(profit, margin=margin)
|
||||
|
||||
async def modify_stops(self, ticket: int, sl: int = None, tp: int = None):
|
||||
pos = self.open_positions.pop(ticket)
|
||||
sl = sl or pos.sl
|
||||
tp = tp or pos.tp
|
||||
order_type, symbol, volume, price_open = pos.order_type, pos.symbol, pos.volume
|
||||
pos = pos._asdict()
|
||||
pos.update(tp=tp, sl=sl, time_update=self.cursor.time)
|
||||
profit = await self.mt5.order_calc_profit(order_type, symbol, volume, price_open, sl)
|
||||
self.open_positions[ticket] = TradePosition(**pos)
|
||||
|
||||
def update_account(self, profit: float, margin: float = 0):
|
||||
self.account.balance += profit
|
||||
self.account.equity += profit
|
||||
self.account.margin -= margin
|
||||
self.account.margin_free = self.account.equity - self.account.margin
|
||||
self.account.margin_level = (self.account.equity / self.account.margin) * 100 if self.account.margin_mode
|
||||
|
||||
async def order_send(self, request: dict, use_terminal: bool = True) -> OrderSendResult:
|
||||
osr = {'retcode': 10009, 'comment': 'Request completed', 'request': TradeRequest(**request)}
|
||||
|
||||
if (position := request.get('position')) in self.open_positions:
|
||||
pos = self.open_positions[position]
|
||||
order_type = OrderType(request['type'])
|
||||
pos_type = OrderType(pos.type)
|
||||
if order_type.opposite == pos_type: # ToDo: is there another way to check if the order is a close order?
|
||||
# close position
|
||||
self.close_position(pos)
|
||||
return OrderSendResult(**osr) # ToDo: Create a deal object here
|
||||
action = request['action']
|
||||
if action == TradeAction.SLTP:
|
||||
self.modify_stops(position, request['sl'], request['tp'])
|
||||
return OrderSendResult(**osr)
|
||||
|
||||
if (action := request.get('action')) == TradeAction.DEAL:
|
||||
ocr = await self.order_check(request, use_terminal=use_terminal)
|
||||
if ocr.retcode != 0:
|
||||
osr.update({'comment': ocr.comment, 'retcode': ocr.retcode})
|
||||
return OrderSendResult(**osr)
|
||||
|
||||
ticket = random.randint(100_000_000, 999_999_999)
|
||||
deal_ticket = random.randint(100_000_000, 999_999_999)
|
||||
tick = self.get_symbol_info_tick(request['symbol'])
|
||||
order_type = request['type']
|
||||
price = tick.ask if request['type'] == ORDER_TYPE_BUY else tick.bid
|
||||
volume = request['volume']
|
||||
sl, tp = request.get('sl', 0), request.get('tp', 0)
|
||||
symbol = request['symbol']
|
||||
pos = {'comment': 'open position', 'ticket': ticket, 'symbol': symbol, 'volume': volume,
|
||||
'price_open': price, 'price_current': price, 'type': order_type, 'profit': 0,
|
||||
'sl': sl, 'tp': tp, 'time': tick.time,
|
||||
'time_msc': tick.time_msc}
|
||||
order = {'ticket': ticket, 'symbol': symbol, 'volume': volume, 'price': price, 'price_current': price,
|
||||
'price_open': price, 'type': order_type, 'time_setup': tick.time,
|
||||
'time_setup_msc': tick.time_msc, 'volume_current': volume, 'sl': sl, 'tp': tp,}
|
||||
pos = TradePosition(**pos)
|
||||
order = TradeOrder(**order)
|
||||
# ToDo: Create a deal object here
|
||||
self.open_positions[pos.ticket] = pos
|
||||
self.open_orders[order.ticket] = order
|
||||
self.orders.setdefault(order.symbol, {})[order.ticket] = order
|
||||
self.positions.setdefault(pos.symbol, {})[pos.ticket] = pos
|
||||
osr.update({'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)
|
||||
self.margins[ticket] = margin
|
||||
return OrderSendResult(**osr)
|
||||
|
||||
async def order_check(self, request: dict, use_terminal=True) -> OrderCheckResult:
|
||||
action, symbol, volume = request.get('action'), request.get('symbol'), request.get('volume')
|
||||
price = request.get('price')
|
||||
ocr = {'retcode': 0, 'balance': 0, 'profit': 0, 'margin': 0, 'equity': 0, 'margin_free': 0,
|
||||
'margin_level': 0, 'comment': 'Done', request: TradeRequest(**request)}
|
||||
|
||||
margin = 0
|
||||
if all([action, symbol, volume, price]):
|
||||
margin = await self.order_calc_margin(action, symbol, volume, price)
|
||||
|
||||
acc = self.get_account_info()
|
||||
equity = acc.equity
|
||||
used_margin = acc.margin + margin
|
||||
free_margin = acc.margin_free - margin
|
||||
margin_level = (equity / used_margin) * 100 if (acc.margin_mode == ACCOUNT_STOPOUT_MODE_PERCENT and used_margin > 0) else free_margin
|
||||
|
||||
if use_terminal and self.mt5.config.use_terminal_for_backtesting:
|
||||
ocr_t = await self.mt5.order_check(request)
|
||||
# return order check result if invalid stops level are detected or bad request
|
||||
if ocr_t.retcode in (10016, 10013, 10014):
|
||||
return ocr_t
|
||||
else:
|
||||
sym = self.symbols[symbol]
|
||||
tsl = sym.trade_stops_level
|
||||
sl, tp = request.get('sl', 0), request.get('tp', 0)
|
||||
|
||||
if tp or sl:
|
||||
min_sl = min(sl, tp)
|
||||
dsl = abs(price - min_sl) / sym.point
|
||||
if dsl < tsl:
|
||||
ocr['retcode'] = 10016
|
||||
ocr['comment'] = 'Invalid stops'
|
||||
return OrderCheckResult(**ocr)
|
||||
|
||||
if margin_level < acc.margin_so_call:
|
||||
ocr['retcode'] = 10019
|
||||
ocr['comment'] = 'No money'
|
||||
|
||||
ocr.update({'balance': acc.balance, 'profit': acc.profit, 'margin': used_margin, 'equity': equity,
|
||||
'margin_free': free_margin, 'margin_level': margin_level})
|
||||
|
||||
return OrderCheckResult(**ocr)
|
||||
|
||||
@error_handler_sync
|
||||
def get_orders_total(self) -> int:
|
||||
return len(self.open_orders)
|
||||
|
||||
|
||||
@error_handler_sync
|
||||
def get_orders(self, symbol: str = '', group: str = '', ticket: int = None) -> tuple[TradeOrder, ...]:
|
||||
if ticket:
|
||||
order = self.open_orders.get(ticket)
|
||||
@@ -286,9 +385,11 @@ class TestData:
|
||||
else:
|
||||
return tuple(order for order in self.open_orders.values())
|
||||
|
||||
def get_positions_total(self):
|
||||
@error_handler_sync
|
||||
def get_positions_total(self) -> int:
|
||||
return len(self.open_positions)
|
||||
|
||||
@error_handler_sync
|
||||
def get_positions(self, symbol: str = '', group: str = '', ticket: int = None) -> tuple[TradePosition, ...]:
|
||||
if ticket:
|
||||
position = self.open_positions.get(ticket)
|
||||
@@ -302,15 +403,17 @@ class TestData:
|
||||
|
||||
else:
|
||||
return tuple(position for position in self.open_positions.values())
|
||||
|
||||
def history_orders_total(self, date_from: datetime | float, date_to: datetime | float) -> int:
|
||||
|
||||
@error_handler_sync
|
||||
def get_history_orders_total(self, date_from: datetime | float, date_to: datetime | float) -> int:
|
||||
start = int(date_from.timestamp()) if isinstance(date_from, datetime) else int(date_from)
|
||||
end = int(date_to.timestamp()) if isinstance(date_to, datetime) else int(date_to)
|
||||
start = self.history_orders[self.history_orders.index >= start].iloc[0].name
|
||||
end = self.history_orders[self.history_orders.index <= end].iloc[-1].name
|
||||
return self.history_orders.loc[start:end].shape[0]
|
||||
|
||||
def history_orders_get(self, date_from: datetime | float, date_to: datetime | float, group: str = '',
|
||||
@error_handler_sync
|
||||
def get_history_orders(self, date_from: datetime | float, date_to: datetime | float, group: str = '',
|
||||
ticket: int = None, position: int = None) -> tuple[TradeOrder, ...]:
|
||||
start = int(date_from.timestamp()) if isinstance(date_from, datetime) else int(date_from)
|
||||
end = int(date_to.timestamp()) if isinstance(date_to, datetime) else int(date_to)
|
||||
@@ -328,8 +431,9 @@ class TestData:
|
||||
...
|
||||
|
||||
orders.drop(columns=['symbol'], inplace=True)
|
||||
return tuple(TradeOrder(**order) for order in orders.to_dict(orient='records'))
|
||||
return tuple(TradeOrder(order) for order in orders.iloc)
|
||||
|
||||
@error_handler_sync
|
||||
def get_history_deals_total(self, date_from: datetime | float, date_to: datetime | float) -> int:
|
||||
start = int(date_from.timestamp()) if isinstance(date_from, datetime) else int(date_from)
|
||||
end = int(date_to.timestamp()) if isinstance(date_to, datetime) else int(date_to)
|
||||
@@ -337,6 +441,7 @@ class TestData:
|
||||
end = self.history_deals[self.history_deals.index <= end].iloc[-1].name
|
||||
return self.history_deals.loc[start:end].shape[0]
|
||||
|
||||
@error_handler_sync
|
||||
def get_history_deals(self, date_from: datetime | float, date_to: datetime | float, group: str = '',
|
||||
position: int = None, ticket: int = None) -> tuple[TradeDeal, ...]:
|
||||
start = int(date_from.timestamp()) if isinstance(date_from, datetime) else int(date_from)
|
||||
@@ -354,5 +459,4 @@ class TestData:
|
||||
elif group:
|
||||
...
|
||||
|
||||
deals.drop(columns=['symbol'], inplace=True)
|
||||
return tuple(TradeDeal(**deal) for deal in deals.to_dict(orient='records'))
|
||||
return tuple(TradeDeal(deal) for deal in deals.iloc)
|
||||
@@ -0,0 +1,2 @@
|
||||
class FingerTrapTest:
|
||||
...
|
||||
@@ -0,0 +1,20 @@
|
||||
from .event_manager import EventManager
|
||||
|
||||
from ...core.config import Config
|
||||
|
||||
|
||||
class TestStrategy:
|
||||
event_manager: EventManager
|
||||
config: Config
|
||||
|
||||
def set_up(self):
|
||||
self.config = Config()
|
||||
self.event_manager = EventManager()
|
||||
|
||||
async def sleep(self, secs: float):
|
||||
time = self.config.test_data.cursor.time
|
||||
mod = time % secs
|
||||
secs = secs - mod if mod != 0 else mod
|
||||
time = self.config.test_data.cursor.time + secs
|
||||
while time > self.config.test_data.cursor.time:
|
||||
await self.event_manager.wait()
|
||||
@@ -1,3 +0,0 @@
|
||||
from .meta_tester import MetaTester
|
||||
from .test_data import TestData
|
||||
from .get_data import GetData
|
||||
@@ -1,23 +0,0 @@
|
||||
import socket
|
||||
|
||||
|
||||
class socketserver:
|
||||
def __init__(self, address = '192.168.1.15', port = 9090):
|
||||
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
self.address = address
|
||||
self.port = port
|
||||
self.sock.bind((self.address, self.port))
|
||||
self.cummdata = ''
|
||||
|
||||
|
||||
def recvmsg(self):
|
||||
g=self.sock.listen(1)
|
||||
print(g)
|
||||
self.conn, self.addr = self.sock.accept()
|
||||
print('connected to', self.addr)
|
||||
data = self.conn.recv(10)
|
||||
self.cummdata += data.decode("utf-8")
|
||||
|
||||
|
||||
so = socketserver()
|
||||
so.recvmsg()
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
@@ -1,207 +0,0 @@
|
||||
import pickle
|
||||
from datetime import datetime
|
||||
from logging import getLogger
|
||||
import asyncio
|
||||
import re
|
||||
|
||||
import pytz
|
||||
from MetaTrader5 import Tick, SymbolInfo
|
||||
import pandas as pd
|
||||
|
||||
from ... import TestData
|
||||
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
|
||||
from .test_data import TestData
|
||||
from .get_data import GetData
|
||||
|
||||
logger = getLogger(__name__)
|
||||
|
||||
|
||||
class MetaTester(MetaTrader):
|
||||
"""A class for testing trading strategies in the MetaTrader 5 terminal. A subclass of MetaTrader."""
|
||||
data: TestData
|
||||
|
||||
def __init__(self, data: TestData = None):
|
||||
super().__init__()
|
||||
self.data = data
|
||||
|
||||
async def initialize(self, path: str = "", login: int = 0, password: str = "", server: str = "",
|
||||
timeout: int | None = None, portable=False, compressed: bool = False) -> bool:
|
||||
self.data = await GetData.load_data(name=path, compressed=compressed)
|
||||
return True
|
||||
|
||||
async def account_info(self) -> AccountInfo:
|
||||
""""""
|
||||
res = self.data.account
|
||||
return res
|
||||
|
||||
async def symbols_total(self) -> int:
|
||||
return len(self.data.symbols)
|
||||
|
||||
async def symbols_get(self, group: str = "") -> tuple[SymbolInfo]:
|
||||
""""""
|
||||
symbols = self.data.symbols.values()
|
||||
return tuple(symbols)
|
||||
|
||||
async def symbol_info(self, symbol: str) -> SymbolInfo | None:
|
||||
return self.data.symbols.get(symbol)
|
||||
|
||||
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
|
||||
@@ -4,7 +4,7 @@ from logging import getLogger
|
||||
|
||||
from .config import Config
|
||||
from .meta_trader import MetaTrader
|
||||
|
||||
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()
|
||||
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)
|
||||
|
||||
@@ -8,6 +8,7 @@ from .task_queue import TaskQueue
|
||||
|
||||
logger = getLogger(__name__)
|
||||
Bot = TypeVar("Bot")
|
||||
TestData = TypeVar("TestData")
|
||||
|
||||
|
||||
class Config:
|
||||
@@ -44,16 +45,20 @@ class Config:
|
||||
record_trades: bool
|
||||
records_dir: Path
|
||||
records_dir_name: str
|
||||
compress_test_data: bool
|
||||
test_data_dir: Path
|
||||
test_data_dir_name: str
|
||||
task_queue: TaskQueue
|
||||
_test_data: TestData
|
||||
bot: Bot
|
||||
_instance: 'Config'
|
||||
mode: Literal['backtest', 'live']
|
||||
use_terminal_for_backtesting: bool
|
||||
test_data_file: str
|
||||
_defaults = {"timeout": 60000, "record_trades": True, "trade_record_mode": "csv", "mode": "live",
|
||||
'filename': "aiomql.json", "records_dir_name": "trade_records", "test_data_dir_name": "test_data",
|
||||
"use_terminal_for_backtesting": True, 'path': '', 'login': 0, 'password': '', 'server': ''}
|
||||
"use_terminal_for_backtesting": True, 'path': '', 'login': 0, 'password': '', 'server': '',
|
||||
"compress_test_data": False, 'test_data_file': ''}
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
if not hasattr(cls, "_instance"):
|
||||
@@ -61,12 +66,21 @@ class Config:
|
||||
cls._instance.state = {}
|
||||
cls._instance.task_queue = TaskQueue()
|
||||
cls._instance.set_attributes(**cls._defaults)
|
||||
cls._instance._test_data = None
|
||||
cls._instance.load_config(**kwargs)
|
||||
return cls._instance
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.set_attributes(**kwargs)
|
||||
|
||||
@property
|
||||
def test_data(self):
|
||||
return self._test_data
|
||||
|
||||
@test_data.setter
|
||||
def test_data(self, value: TestData):
|
||||
self._test_data = value
|
||||
|
||||
def set_attributes(self, **kwargs):
|
||||
"""Set keyword arguments as object attributes
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ class Error:
|
||||
|
||||
def __init__(self, code: int, description: str = ''):
|
||||
self.code = code
|
||||
self.description = description or self.descriptions.get(code, 'Unknown Error')
|
||||
self.description = description or self.descriptions.get(code, 'unknown error')
|
||||
|
||||
def is_connection_error(self):
|
||||
return self.code in self.conn_errors
|
||||
|
||||
@@ -334,9 +334,9 @@ class SymbolInfo(Base):
|
||||
path: str
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
if (name := kwargs.pop('name', None)) is None:
|
||||
if (name := kwargs.pop('name', '')) == '':
|
||||
raise AttributeError('Symbol Object Must be initialized with a name')
|
||||
self.name = name # type: str
|
||||
self.name = name
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def __repr__(self):
|
||||
@@ -351,28 +351,6 @@ class SymbolInfo(Base):
|
||||
def __hash__(self):
|
||||
return hash(self.name)
|
||||
|
||||
class TickInfo(Base):
|
||||
"""Price Tick of a Financial Instrument.
|
||||
|
||||
Attributes:
|
||||
time (int): Time of the last prices update for the symbol
|
||||
bid (float): Current Bid price
|
||||
ask (float): Current Ask price
|
||||
last (float): Price of the last deal (Last)
|
||||
volume (float): Volume for the current Last price
|
||||
time_msc (int): Time of the last prices update for the symbol in milliseconds
|
||||
flags (TickFlag): Tick flags
|
||||
volume_real (float): Volume for the current Last price
|
||||
Index (int): Custom attribute representing the position of the tick in a sequence.
|
||||
"""
|
||||
time: float
|
||||
bid: float
|
||||
ask: float
|
||||
last: float
|
||||
volume: float
|
||||
time_msc: float
|
||||
flags: TickFlag
|
||||
volume_real: float
|
||||
|
||||
class BookInfo(Base):
|
||||
"""Book Information Class.
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from .strategies import *
|
||||
from .traders import *
|
||||
from .symbols import *
|
||||
from .candle_patterns import *
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
from .fractals import *
|
||||
@@ -0,0 +1,13 @@
|
||||
from ...candle import Candle, Candles
|
||||
|
||||
|
||||
def find_bearish_fractal(candles: Candles) -> Candle | None:
|
||||
for i in range(len(candles) - 3, 1, -1):
|
||||
if candles[i].high > max(candles[i - 1].high, candles[i + 1].high, candles[i - 2].high, candles[i + 2].high):
|
||||
return candles[i]
|
||||
|
||||
|
||||
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]
|
||||
@@ -1,2 +1,3 @@
|
||||
from .finger_trap import FingerTrap
|
||||
from .tracker import Tracker
|
||||
from .finger_trap_back_test import FingerTrapTest
|
||||
|
||||
@@ -9,7 +9,7 @@ from ...candle import Candles
|
||||
from ...strategy import Strategy
|
||||
from ...core import TimeFrame, OrderType
|
||||
from ...sessions import Sessions
|
||||
from ...utils import find_bearish_fractal, find_bullish_fractal
|
||||
from ..candle_patterns import find_bearish_fractal, find_bullish_fractal
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -36,6 +36,7 @@ class FingerTrap(Strategy):
|
||||
|
||||
async def check_trend(self):
|
||||
try:
|
||||
|
||||
candles: Candles = await self.symbol.copy_rates_from_pos(timeframe=self.ttf, count=self.tcc)
|
||||
if not ((current := candles[-1].time) >= self.tracker.trend_time):
|
||||
self.tracker.update(new=False, order_type=None)
|
||||
@@ -106,6 +107,7 @@ class FingerTrap(Strategy):
|
||||
await self.trader.place_trade(order_type=self.tracker.order_type, parameters=self.parameters,
|
||||
sl=self.tracker.sl)
|
||||
await self.sleep(self.tracker.snooze)
|
||||
|
||||
except Exception as err:
|
||||
logger.error(f"{err} For {self.symbol} in {self.__class__.__name__}.trade")
|
||||
await self.sleep(self.ttf.time)
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
from .finger_trap import FingerTrap
|
||||
from ...contrib.backtester.test_strategy import TestStrategy
|
||||
|
||||
|
||||
class FingerTrapTest(TestStrategy, FingerTrap):
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.set_up()
|
||||
|
||||
async def test(self):
|
||||
print(f"Backtesting {self.symbol}")
|
||||
while True:
|
||||
await self.event_manager.acquire()
|
||||
try:
|
||||
await self.event_manager.wait()
|
||||
await self.watch_market()
|
||||
|
||||
if not self.tracker.new:
|
||||
continue
|
||||
|
||||
if self.tracker.order_type is not None:
|
||||
await self.trader.place_trade(order_type=self.tracker.order_type, parameters=self.parameters,
|
||||
sl=self.tracker.sl)
|
||||
await self.sleep(self.tracker.snooze)
|
||||
except Exception as err:
|
||||
print(f"{err} For {self.symbol} in {self.__class__.__name__}.trade")
|
||||
await self.sleep(self.ttf.time)
|
||||
|
||||
finally:
|
||||
self.event_manager.release()
|
||||
+38
-21
@@ -1,11 +1,11 @@
|
||||
"""Utility functions for aiomql."""
|
||||
|
||||
import decimal
|
||||
import random
|
||||
from functools import wraps, partial
|
||||
import asyncio
|
||||
from logging import getLogger
|
||||
|
||||
from .candle import Candles, Candle
|
||||
logger = getLogger(__name__)
|
||||
|
||||
|
||||
def dict_to_string(data: dict, multi=False) -> str:
|
||||
@@ -22,25 +22,6 @@ 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 = 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
|
||||
return float(decimal.Decimal(str(value)).quantize(decimal.Decimal(str(step))))
|
||||
|
||||
|
||||
def find_bearish_fractal(candles: Candles) -> Candle | None:
|
||||
for i in range(len(candles) - 3, 1, -1):
|
||||
if candles[i].high > max(candles[i - 1].high, candles[i + 1].high, candles[i - 2].high, candles[i + 2].high):
|
||||
return candles[i]
|
||||
|
||||
|
||||
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)
|
||||
@@ -64,8 +45,44 @@ def backoff_decorator(func=None, *, max_retries: int = 3, retries: int = 0, dela
|
||||
return wrapper
|
||||
|
||||
|
||||
def error_handler(func=None, *, msg='', exe = Exception):
|
||||
if func is None:
|
||||
return partial(error_handler, msg=msg, exe=exe)
|
||||
|
||||
@wraps(func)
|
||||
async def wrapper(*args, **kwargs):
|
||||
try:
|
||||
res = await func(*args, **kwargs)
|
||||
return res
|
||||
except exe as err:
|
||||
logger.error(f'Error in {func.__name__}: {msg or err}')
|
||||
|
||||
return wrapper
|
||||
|
||||
def error_handler_sync(func=None, *, msg='', exe=Exception):
|
||||
if func is None:
|
||||
return partial(error_handler, msg=msg, exe=exe)
|
||||
|
||||
@wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
try:
|
||||
res = func(*args, **kwargs)
|
||||
return res
|
||||
except exe as err:
|
||||
logger.error(f'Error in {func.__name__}: {msg or err}')
|
||||
|
||||
return wrapper
|
||||
|
||||
def round_down(value: int, base: int) -> int:
|
||||
return value if value % base == 0 else value - (value % base)
|
||||
|
||||
|
||||
def round_up(value: int, base: int) -> int:
|
||||
return value if value % base == 0 else value + base - (value % base)
|
||||
|
||||
|
||||
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
|
||||
return float(decimal.Decimal(str(value)).quantize(decimal.Decimal(str(step))))
|
||||
|
||||
Reference in New Issue
Block a user