From e34d96f064e5ce4cc8479d87dee4ae0c932f3b2d Mon Sep 17 00:00:00 2001 From: Ichinga Samuel Date: Wed, 2 Oct 2024 00:25:24 +0100 Subject: [PATCH] testdata --- src/aiomql/account.py | 80 +------- src/aiomql/bot_builder.py | 75 ++++---- .../contrib/backtester/backtest_engine.py | 2 +- src/aiomql/contrib/backtester/backtester.py | 2 - src/aiomql/contrib/backtester/check1.py | 24 +-- src/aiomql/core/_core.py | 2 +- src/aiomql/core/config.py | 4 +- src/aiomql/core/models.py | 6 +- src/aiomql/executor.py | 4 - src/aiomql/history.py | 137 ++++---------- src/aiomql/order.py | 63 +++---- src/aiomql/positions.py | 105 ++++------- src/aiomql/ram.py | 37 ++-- src/aiomql/result.py | 26 +-- src/aiomql/sessions.py | 175 +++++++++++------- src/aiomql/strategy.py | 2 +- src/aiomql/symbol.py | 12 +- tests/__init__.py | 6 +- tests/test.json | 10 + tests/test_meta_trader.py | 15 +- 20 files changed, 312 insertions(+), 475 deletions(-) diff --git a/src/aiomql/account.py b/src/aiomql/account.py index c8d8a4d..a76a263 100644 --- a/src/aiomql/account.py +++ b/src/aiomql/account.py @@ -1,7 +1,7 @@ -import asyncio from logging import getLogger +from typing import Self -from .core.models import AccountInfo, SymbolInfo +from .core.models import AccountInfo from .core.exceptions import LoginError logger = getLogger(__name__) @@ -13,33 +13,26 @@ class Account(AccountInfo): Attributes: connected (bool): Status of connection to MetaTrader 5 Terminal - symbols (set[SymbolInfo]): A set of available symbols for the financial market. Notes: Other Account properties are defined in the AccountInfo class. """ - _instance: 'Account' + _instance: Self connected: bool - symbols = set() def __new__(cls, *args, **kwargs): if not hasattr(cls, '_instance'): cls._instance = super().__new__(cls) + cls._instance.exclude = cls._instance.exclude | {'_instance'} return cls._instance - def __init__(self, **kwargs): - super().__init__(**kwargs) - self.exclude = self.exclude | {'_instance', 'symbols'} - acc = {k: (self.dict[k] or v) for k, v in self.config.account_info().items()} - self.set_attributes(**acc) - async def refresh(self): """Refreshes the account instance with the latest account details from the MetaTrader 5 terminal""" account_info = await self.mt5.account_info() acc = account_info._asdict() self.set_attributes(**acc) - async def __aenter__(self) -> 'Account': + async def __aenter__(self) -> Self: """Connect to a trading account and return the account instance. Async context manager for the Account class. @@ -49,70 +42,11 @@ class Account(AccountInfo): Raises: LoginError: If login fails """ - res = await self.sign_in() - if not res: + self.connected = await self.mt5.login() + if not self: raise LoginError('Login failed') return self async def __aexit__(self, exc_type, exc_val, exc_tb): await self.mt5.shutdown() self.connected = False - - 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, **kwargs) - if self.connected: - await self.refresh() - self.symbols = await self.symbols_get() - return self.connected - await self.mt5.shutdown() - return False - - async def _login(self, *, acc: dict, tries=3, **kwargs) -> bool: - res = False - - if tries == 0: - return False - - init_args = {**acc} | {'path': self.config.path} | {**kwargs} - ini = await self.mt5.initialize(**init_args) - - if ini: - res = await self.mt5.login(**acc) - if not res: - await self.mt5.shutdown() - - if ini and res: - return True - else: - await asyncio.sleep(5+tries) - 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. - - Args: - symbol (str | SymbolInfo): - - Returns: - bool: True if symbol is present otherwise False - """ - try: - return str(symbol) in {s.name for s in self.symbols} - except Exception as err: - logger.warning(f'Error: {err}; {symbol} not available in this market') - return False - - async def symbols_get(self) -> set[SymbolInfo]: - """Get all financial instruments from the MetaTrader 5 terminal available for the current account. - - Returns: - set[Symbol]: A set of available symbols. - """ - syms = await self.mt5.symbols_get() - return {SymbolInfo(name=sym.name) for sym in syms} diff --git a/src/aiomql/bot_builder.py b/src/aiomql/bot_builder.py index f2313e8..d17ab36 100644 --- a/src/aiomql/bot_builder.py +++ b/src/aiomql/bot_builder.py @@ -1,38 +1,34 @@ import asyncio from concurrent.futures import ProcessPoolExecutor -from typing import Type, Iterable, TypeVar, Callable, Coroutine +from typing import Type, Iterable, Callable, Coroutine import logging from .executor import Executor -from .account import Account from .core.config import Config +from .core.meta_trader import MetaTrader from .symbol import Symbol as Symbol from .strategy import Strategy as Strategy logger = logging.getLogger(__name__) - class Bot: """The bot class. Create a bot instance to run your strategies. Attributes: - account (Account): Account Object. executor: The default thread executor. - symbols (list[Symbols]): A set of symbols for the trading session config (Config): Config instance + mt (MetaTrader): MetaTrader instance """ config: Config - account: Account - symbols: set executor: Executor + mt: MetaTrader def __init__(self): - self.config = Config() - self.account = Account() - self.symbols = set() + self.config = Config(bot=self) self.executor = Executor() + self.mt = MetaTrader() @classmethod def run_bots(cls, funcs: dict[Callable: dict] = None, num_workers: int = None): @@ -55,20 +51,18 @@ class Bot: SystemExit if sign in was not successful """ try: - init = await self.account.sign_in() - if not init: + login = await self.mt.login() + if not login: logger.warning(f"Unable to sign in to MetaTrder 5 Terminal") raise SystemExit logger.info("Login Successful") - await self.init_symbols() - self.executor.remove_workers(symbols=self.symbols) + await self.init_strategies() self.add_coroutine(self.config.task_queue.start) - self.config.bot = self except Exception as err: logger.error(f"{err}. Bot initialization failed") raise SystemExit - def add_function(self, func: Callable, **kwargs: dict): + def add_function(self, func: Callable[..., ...], **kwargs: dict): """Add a function to the executor. Args: @@ -77,7 +71,7 @@ class Bot: """ self.executor.add_function(func, kwargs) - def add_coroutine(self, coro: Coroutine | Callable, **kwargs): + def add_coroutine(self, coro: Coroutine[..., ...], **kwargs): """Add a coroutine to the executor. Args: @@ -117,39 +111,32 @@ class Bot: """ [self.add_strategy(strategy) for strategy in strategies] - def add_strategy_all(self, *, strategy: Type[Strategy], params: dict | None = None): - """Use this to run a single strategy on all available instruments in the market using the default parameters - i.e. one set of parameters for all trading symbols + def add_strategy_all(self, *, strategy: Type[Strategy], params: dict | None = None, + symbols: list[Symbol] = None, **kwargs): + """Use this to run a single strategy on multiple symbols with the same parameters and keyword arguments. Keyword Args: strategy (Strategy): Strategy class params (dict): A dictionary of parameters for the strategy + symbols (list): A list of symbols to run the strategy on + **kwargs: Additional keyword arguments for the strategy """ [ - self.add_strategy(strategy(symbol=symbol, params=params)) - for symbol in self.symbols + self.add_strategy(strategy(symbol=symbol, params=params, **kwargs)) + for symbol in symbols ] - async def init_symbols(self): + @staticmethod + async def init_strategy(strategy: Strategy) -> tuple[bool, Strategy]: + """Initialize a single strategy. This method is called internally by the bot.""" + res = await strategy.symbol.init() + return res, strategy + + async def init_strategies(self): """Initialize the symbols for the current trading session. This method is called internally by the bot.""" - syms = [self.init_symbol(strategy.symbol) for strategy in self.executor.workers] - await asyncio.gather(*syms, return_exceptions=True) - - async def init_symbol(self, symbol: Symbol) -> Symbol: - """Initialize a symbol before the beginning of a trading sessions. - Removes it from the list of symbols if it was not successfully initialized or not available - for the account. - - Args: - symbol (Symbol): Symbol object to be initialized - - Returns: - Symbol: if successfully initialized - """ - if self.account.has_symbol(symbol): - init = await symbol.init() - if init: - self.symbols.add(symbol) - return symbol - logger.warning(f"Unable to initialize symbol {symbol}") - logger.warning(f"{symbol} not a available for this market") + tasks = [self.init_strategy(strategy) for strategy in self.executor.workers] + for task in asyncio.as_completed(tasks): + res = await task + if not res[0]: + logger.warning(f"Failed to initialize symbol {res[1].symbol}") + self.executor.workers.remove(res[1]) diff --git a/src/aiomql/contrib/backtester/backtest_engine.py b/src/aiomql/contrib/backtester/backtest_engine.py index 699020e..affc1b1 100644 --- a/src/aiomql/contrib/backtester/backtest_engine.py +++ b/src/aiomql/contrib/backtester/backtest_engine.py @@ -391,7 +391,7 @@ class BackTestEngine: 'sl': sl, 'tp': tp, 'time': current_tick.time, 'time_msc': current_tick.time_msc, 'time_update': current_tick.time, 'time_update_msc': current_tick.time_msc} - deal = {'ticket': deal_ticket, 'position': order_ticket, 'symbol': symbol, 'commission': 0, 'swap': 0, + deal = {'ticket': deal_ticket, 'order': order_ticket, 'symbol': symbol, 'commission': 0, 'swap': 0, 'position_id': order_ticket, 'fee': 0, 'time': current_tick.time, 'time_msc': current_tick.time_msc, 'volume': volume, 'price': price, 'type': DealType(order_type), 'reason': DealReason.EXPERT, 'entry': DealEntry.IN} diff --git a/src/aiomql/contrib/backtester/backtester.py b/src/aiomql/contrib/backtester/backtester.py index eb01db0..c16efc3 100644 --- a/src/aiomql/contrib/backtester/backtester.py +++ b/src/aiomql/contrib/backtester/backtester.py @@ -6,7 +6,6 @@ from .event_manager import EventManager from .meta_tester import MetaTester from .backtest_engine import BackTestEngine from .strategy_tester import StrategyTester -from ...core import Config logger = getLogger(__name__) @@ -22,7 +21,6 @@ class BackTester: try: await self.mt5.initialize() strategies = [strategy for strategy in self.strategies if await strategy.symbol.init()] - print(f"Number of strategies: {len(strategies)}") self.event_manager.num_main_tasks = len(strategies) tasks = [*[asyncio.create_task(strategy.test()) for strategy in strategies], asyncio.create_task(self.event_manager.event_monitor())] diff --git a/src/aiomql/contrib/backtester/check1.py b/src/aiomql/contrib/backtester/check1.py index 29313c5..0fdd368 100644 --- a/src/aiomql/contrib/backtester/check1.py +++ b/src/aiomql/contrib/backtester/check1.py @@ -1,21 +1,5 @@ -glob = dict() -class Check: - def __init__(self, ty): - self.r = ty - - @property - def r(self): - print('getting value') - return glob.get('r') - - @r.setter - def r(self, value): - print('setting value') - glob['r'] = value - - -f = Check(465) -print(f.r) -f.r = 56 -print(f.r) +class Check1: + f: str + t: str + v: float diff --git a/src/aiomql/core/_core.py b/src/aiomql/core/_core.py index 9ae9992..342f48c 100644 --- a/src/aiomql/core/_core.py +++ b/src/aiomql/core/_core.py @@ -1,10 +1,10 @@ from typing import Callable import MetaTrader5 - from MetaTrader5 import (Tick, SymbolInfo, AccountInfo, TerminalInfo, TradeOrder, TradePosition, TradeDeal, OrderCheckResult, OrderSendResult, BookInfo, TradeRequest) +from .config import Config constants = ('TIMEFRAME_M1', 'TIMEFRAME_M2', 'TIMEFRAME_M3', 'TIMEFRAME_M4', 'TIMEFRAME_M5', 'TIMEFRAME_M6', 'TIMEFRAME_M10', 'TIMEFRAME_M12', 'TIMEFRAME_M15', 'TIMEFRAME_M20', 'TIMEFRAME_M30', 'TIMEFRAME_H1', diff --git a/src/aiomql/core/config.py b/src/aiomql/core/config.py index 7637c2b..f159244 100644 --- a/src/aiomql/core/config.py +++ b/src/aiomql/core/config.py @@ -1,6 +1,6 @@ import os from pathlib import Path -from typing import Iterator, Literal, TypeVar +from typing import Iterator, Literal, TypeVar, Self import json from logging import getLogger @@ -51,7 +51,7 @@ class Config: task_queue: TaskQueue _backtest_engine: BackTestEngine bot: Bot - _instance: 'Config' + _instance: Self mode: Literal['backtest', 'live'] use_terminal_for_backtesting: bool _defaults = {"timeout": 60000, "record_trades": True, "trade_record_mode": "csv", "mode": "live", diff --git a/src/aiomql/core/models.py b/src/aiomql/core/models.py index fdbd6ec..35a4546 100644 --- a/src/aiomql/core/models.py +++ b/src/aiomql/core/models.py @@ -19,7 +19,6 @@ class AccountInfo(Base): Attributes: login: int - password: str server: str trade_mode: AccountTradeMode balance: float @@ -51,7 +50,6 @@ class AccountInfo(Base): company: str """ login: int = 0 - password: str = '' server: str = '' trade_mode: AccountTradeMode balance: float @@ -334,9 +332,7 @@ class SymbolInfo(Base): path: str def __init__(self, **kwargs): - if (name := kwargs.pop('name', '')) == '': - raise AttributeError('Symbol Object Must be initialized with a name') - self.name = name + assert 'name' in kwargs, "Symbol Object Must be initialized with a name" super().__init__(**kwargs) def __repr__(self): diff --git a/src/aiomql/executor.py b/src/aiomql/executor.py index c0fc133..52fee19 100644 --- a/src/aiomql/executor.py +++ b/src/aiomql/executor.py @@ -38,10 +38,6 @@ class Executor: """ self.workers.extend(strategies) - def remove_workers(self, *, symbols: set): - """Removes any worker running on a symbol not successfully initialized.""" - self.workers = [worker for worker in self.workers if worker.symbol in symbols] - def add_worker(self, strategy: Strategy): """Add a strategy instance to the list of workers diff --git a/src/aiomql/history.py b/src/aiomql/history.py index a6d5583..b91417e 100644 --- a/src/aiomql/history.py +++ b/src/aiomql/history.py @@ -1,15 +1,15 @@ +import asyncio from datetime import datetime from logging import getLogger +import pytz from pandas import DataFrame import pandas as pd from .core.config import Config from .core.meta_trader import MetaTrader, CopyTicks, OrderType from .core.models import TradeDeal, TradeOrder - from .contrib.backtester.meta_tester import MetaTester - from .utils import backoff_decorator logger = getLogger(__name__) @@ -24,71 +24,56 @@ class History: total_deals: Total number of deals total_orders (int): Total number orders group (str): Filter for selecting history by symbols. - ticket (int): Filter for selecting history by ticket number - position (int): Filter for selecting history deals by position mt5 (MetaTrader): MetaTrader instance config (Config): Config instance """ mt5: MetaTrader | MetaTester config: Config - def __init__(self, *, date_from: datetime | int = None, date_to: datetime | int = None, - group: str = "", ticket: int = None, position: int = None): + def __init__(self, *, date_from: datetime | int, date_to: datetime | int, group: str = '', use_utc: bool = True): """ Args: - date_from (datetime, float): Date the orders are requested from. Set by the 'datetime' object or as a + date_from (datetime, int): Date the orders are requested from. Set by the 'datetime' object or as a number of seconds elapsed since 1970.01.01. Defaults to twenty-four hours from the current time in 'utc' - date_to (datetime, float): Date up to which the orders are requested. Set by the 'datetime' object or as a + date_to (datetime, int): Date up to which the orders are requested. Set by the 'datetime' object or as a number of seconds elapsed since 1970.01.01. Defaults to the current time in "utc" - group (str): Filter for selecting history by symbols. - ticket (int): Filter for selecting history by ticket number - position (int): Filter for selecting history deals by position + group (str): Filter for selecting history by symbols. Defaults to an empty string """ self.config = Config() self.mt5 = MetaTrader() if self.config.mode == 'live' else MetaTester() - self.date_from = date_from - self.date_to = date_to + date_from = date_from if isinstance(date_from, datetime) else datetime.fromtimestamp(date_from) + date_to = date_to if isinstance(date_to, datetime) else datetime.fromtimestamp(date_to) + self.date_from = date_from.astimezone(pytz.utc) if use_utc else date_from + self.date_to = date_to.astimezone(pytz.utc) if use_utc else date_to self.group = group - self.ticket = ticket - self.position = position - self.deals: list[TradeDeal] = [] - self.orders: list[TradeOrder] = [] + self.deals: tuple[TradeDeal, ...] = () + self.orders: tuple[TradeOrder, ...] = () self.total_deals: int = 0 self.total_orders: int = 0 - async def init(self, deals=True, orders=True): - """Get history deals and orders - - Keyword Args: - deals (bool): If true get history deals during initial request to terminal - orders (bool): If true get history orders during initial request to terminal - """ - self.deals = await self.get_deals() if deals else tuple() - self.orders = await self.get_orders() if orders else tuple() + async def init(self): + """Get history deals and orders""" + deals, orders = await asyncio.gather(self.get_deals(), self.get_orders(), return_exceptions=True) + self.deals = deals if isinstance(deals, tuple) else () + self.orders = orders if isinstance(orders, tuple) else () self.total_deals = len(self.deals) self.total_orders = len(self.orders) @backoff_decorator - async def get_deals(self, *, date_from: datetime | int = None, date_to: datetime | int = None, group: str = '')\ - -> tuple[TradeDeal, ...]: + async def get_deals(self) -> tuple[TradeDeal, ...]: """Get deals from trading history using the parameters set in the constructor. - Returns: tuple[TradeDeal]: A list of trade deals """ - date_from, date_to, group = date_from or self.date_from, date_to or self.date_to, group or self.group - deals = await self.mt5.history_deals_get(date_from=date_from, date_to=date_to, group=group) - + deals = await self.mt5.history_deals_get(date_from=self.date_from, date_to=self.date_to, group=self.group) if deals is not None: return tuple(TradeDeal(**deal._asdict()) for deal in deals) - logger.warning(f'Failed to get deals') return tuple() - @backoff_decorator - async def get_deals_ticket(self, *, ticket: int = None) -> tuple[TradeDeal, ...]: + def get_deals_by_ticket(self, *, ticket: int) -> tuple[TradeDeal, ...]: """Call specifying the order ticket. Return all deals having the specified order ticket in the DEAL_ORDER property. @@ -98,13 +83,9 @@ class History: Returns: tuple[TradeDeal]: A tuple of all deals with the order ticket """ - ticket = ticket or self.ticket - assert ticket is not None, 'ticket not provided' - deals = await self.mt5.history_deals_get(ticket=ticket) - return tuple(sorted([TradeDeal(**deal._asdict()) for deal in deals or []], key=lambda x: x.time_msc)) + return tuple(sorted((deal for deal in self.deals if deal.order == ticket), key=lambda x: x.time_msc)) - @backoff_decorator - async def get_deals_position(self, *, position: int = None) -> tuple[TradeDeal, ...]: + def get_deals_by_position(self, *, position: int = None) -> tuple[TradeDeal, ...]: """ Get all deals with the specified position ticket in the DEAL_POSITION_ID property Args: @@ -113,36 +94,16 @@ class History: Returns: tuple[TradeDeal]: A tuple of all deals with the position ticket """ - position = position or self.position - assert position is not None, 'position not provided' - deals = await self.mt5.history_deals_get(position=position) - return tuple(sorted([TradeDeal(**deal._asdict()) for deal in deals or []], key=lambda x: x.time_msc)) - - async def deals_total(self, *, date_from: int | datetime = None, date_to: int | datetime = None) -> int: - """Get total number of deals within the specified period in the constructor. - Args: - date_from (int|datetime): Date the orders are requested from. Set by the 'datetime' object or as a number of - seconds elapsed since 1970.01.01. - date_to (int|datetime): Date up to which the orders are requested. Set by the 'datetime' object or as a - number of seconds elapsed since 1970.01.01. - Returns: - int: Total number of Deals - """ - date_from, date_to = date_from or self.date_from, date_to or self.date_to - assert date_from is not None and date_to is not None, 'date_from and/or date_to not provided' - total_deals = await self.mt5.history_deals_total(date_from, date_to) - return total_deals + return tuple(sorted((deal for deal in self.deals if deal.position_id == position), key=lambda x: x.time_msc)) @backoff_decorator - async def get_orders(self, *, date_from: datetime | int = None, date_to: datetime | int = None, group: str = '', - retries: int = 3) -> tuple[TradeOrder, ...]: + async def get_orders(self) -> tuple[TradeOrder, ...]: """Get orders from trading history using the parameters set in the constructor or the method arguments. Returns: list[TradeOrder]: A list of trade orders """ - date_from, date_to, group = date_from or self.date_from, date_to or self.date_to, group or self.group - orders = await self.mt5.history_orders_get(date_from=date_from, date_to=date_to, group=group) + orders = await self.mt5.history_orders_get(date_from=self.date_from, date_to=self.date_to, group=self.group) if orders is not None: return tuple(TradeOrder(**order._asdict()) for order in orders) @@ -150,45 +111,15 @@ class History: logger.warning(f'Failed to get orders') return tuple() - @backoff_decorator - async def get_order_ticket(self, ticket: int | None = None) -> TradeOrder | None: - ticket = ticket or self.ticket - assert isinstance(ticket, int), 'ticket not provided' - orders = await self.mt5.history_orders_get(ticket=ticket) + def get_orders_by_ticket(self, ticket: int) -> tuple[TradeOrder, ...]: + """filter orders by ticket""" + return tuple(sorted((order for order in self.orders if order.ticket == ticket), key=lambda x: x.time_done_msc)) - if orders and (order := orders[0]).ticket == ticket: - return TradeOrder(**order._asdict()) - return None - - @backoff_decorator - async def get_orders_position(self, position: int = None) -> tuple[TradeOrder, ...]: - """ - Call specifying the position ticket. Return all orders with a position ticket specified in the - ORDER_POSITION_ID property - - Args: - position: The position ticket - - Returns: - tuple[TradeOrder]: A tuple of all orders with the position ticket - """ - position = position or self.position - assert isinstance(position, int), 'position not provided' - orders = await self.mt5.history_orders_get(position=position) - return tuple(sorted([TradeOrder(**order._asdict()) for order in orders or []], key=lambda x: x.time_done_msc)) - - @backoff_decorator - async def orders_total(self, date_from: int | datetime = None, date_to: int | datetime = None) -> int: - """Get total number of orders within the specified period in the constructor. - - Returns: - int: Total number of orders - """ - date_from, date_to = date_from or self.date_from, date_to or self.date_to - assert date_from is not None and date_to is not None, 'date_from and/or date_to not provided' - total_orders = await self.mt5.history_orders_total(date_from, date_to) - return total_orders + def get_orders_by_position(self, position: int) -> tuple[TradeOrder, ...]: + """ filter orders by position""" + return tuple(sorted((order for order in self.orders if order.position_id == position), + key=lambda x: x.time_done_msc)) async def track_order(self, *, position: int = None, end_time: datetime = None) -> DataFrame: """ @@ -206,8 +137,8 @@ class History: Returns: DataFrame: A pandas DataFrame of the ticks and profit for the order. """ - orders = await self.get_orders_position(position=position) - deals = await self.get_deals_position(position=position) + orders = self.get_orders_by_position(position=position) + deals = self.get_deals_by_position(position=position) open_order = orders[0] open_deal = deals[0] close_deal = deals[-1] diff --git a/src/aiomql/order.py b/src/aiomql/order.py index 1edc5df..83ce8bc 100644 --- a/src/aiomql/order.py +++ b/src/aiomql/order.py @@ -1,9 +1,10 @@ from logging import getLogger -from .core.models import TradeRequest, OrderSendResult, OrderCheckResult, TradeOrder +from .core.models import TradeRequest, TradeOrder from .core.constants import TradeAction, OrderTime, OrderFilling from .core.exceptions import OrderError -from .utils import backoff_decorator +from .utils import backoff_decorator, error_handler +from MetaTrader5 import OrderCheckResult, OrderSendResult logger = getLogger(__name__) @@ -24,25 +25,20 @@ class Order(TradeRequest): type_time (OrderTime.DAY): Order time type_filling (OrderFilling.FOK): Order filling """ - if 'symbol' in kwargs: - kwargs['symbol'] = str(kwargs['symbol']) - self.action = kwargs.pop('action', TradeAction.DEAL) - self.type_time = kwargs.pop('type_time', OrderTime.DAY) - self.type_filling = kwargs.pop('type_filling', OrderFilling.FOK) + kwargs = {'action': TradeAction.DEAL, OrderTime.DAY: self.type_time, 'type_filling': OrderFilling.FOK, **kwargs} super().__init__(**kwargs) async def orders_total(self): - """Get the number of active orders. + """Get the number of active pending orders. Returns: (int): total number of active orders """ return await self.mt5.orders_total() - @backoff_decorator async def get_order(self, *, ticket: int) -> TradeOrder | None: """ - Get the order by ticket number. + Get a pending order by ticket number. Args: ticket (int): Order ticket number @@ -50,15 +46,15 @@ class Order(TradeRequest): Returns: """ orders = await self.mt5.orders_get(ticket=ticket) + order = None + for order_ in orders: + if order_.ticket == ticket: + return TradeOrder(**order_._asdict()) + return order - if orders and (order := orders[0]).ticket == ticket: - return TradeOrder(**order._asdict()) - - return None - - @backoff_decorator async def get_orders(self, *, ticket: int = 0, symbol: str = '', group: str = '') -> tuple[TradeOrder, ...]: - """Get the list of active orders for the current symbol. + """Get the list of active pending orders for the current symbol. + Keyword Args: ticket (int): Order ticket number symbol (str): Symbol name @@ -66,15 +62,12 @@ class Order(TradeRequest): Returns: tuple[TradeOrder]: A Tuple of active trade orders as TradeOrder objects """ - symbol = getattr(self, 'symbol', symbol) orders = await self.mt5.orders_get(symbol=symbol, ticket=ticket, group=group) - if orders is not None: - orders = (TradeOrder(**order._asdict()) for order in orders) - return tuple(orders) - + return tuple(TradeOrder(**order._asdict()) for order in orders) return tuple() + @backoff_decorator async def check(self, **kwargs) -> OrderCheckResult: """Check funds sufficiency for performing a required trading operation and the possibility of executing it. @@ -88,8 +81,9 @@ class Order(TradeRequest): res = await self.mt5.order_check(req) if res is None: raise OrderError(f'Order check failed for {self.symbol}') - return OrderCheckResult(**res._asdict()) + return res + @backoff_decorator async def send(self) -> OrderSendResult: """Send a request to perform a trading operation from the terminal to the trade server. @@ -102,17 +96,9 @@ class Order(TradeRequest): res = await self.mt5.order_send(self.dict) if res is None: raise OrderError(f'Failed to send order {self.symbol}') - res = OrderSendResult(**res._asdict()) - try: - profit = await self.calc_profit() - loss = await self.calc_profit(tp=self.sl) - res.loss = loss - res.profit = profit - except Exception as _: - pass return res - async def calc_margin(self) -> float: + async def calc_margin(self) -> float | None: """Return the required margin in the account currency to perform a specified trading operation. Returns: @@ -122,21 +108,16 @@ class Order(TradeRequest): OrderError: If not successful """ res = await self.mt5.order_calc_margin(self.type, self.symbol, self.volume, self.price) - if res is None: - raise OrderError(f'Failed to calculate margin for {self.symbol}') return res - async def calc_profit(self, **kwargs) -> float | None: + @error_handler(response=0) + async def calc_profit(self) -> float: """Return profit in the account currency for a specified trading operation. Returns: float: Returns float value if successful None: If not successful """ - include = {'tp', 'price', 'symbol', 'volume', 'type'} - args = self.get_dict(include=include) - args |= kwargs - if len(include.intersection(args.keys())) < len(include): - return None - res = await self.mt5.order_calc_profit(args['type'], args['symbol'], args['volume'], args['price'], args['tp']) + action, symbol, volume, price_open, price_close = self.type, self.symbol, self.volume, self.price, self.tp + res = await self.mt5.order_calc_profit(action, symbol, volume, price_open, price_close) return res diff --git a/src/aiomql/positions.py b/src/aiomql/positions.py index 0d8df5b..bdf222e 100644 --- a/src/aiomql/positions.py +++ b/src/aiomql/positions.py @@ -3,12 +3,10 @@ import asyncio from logging import getLogger from .core.meta_trader import MetaTrader -from .core.models import TradePosition, TradeAction -from .core.constants import OrderType +from .core.models import TradePosition, OrderSendResult +from .core.constants import OrderType, TradeAction 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 @@ -19,60 +17,32 @@ class Positions: """Get Open Positions. Attributes: - symbol (str): Financial instrument name. - group (str): The filter for arranging a group of necessary symbols. Optional named parameter. - If the group is specified, the function returns only positions meeting a specified criteria for a symbol. - ticket (int): Position ticket. mt5 (MetaTrader): MetaTrader instance. """ - mt5: MetaTrader #| MetaTester + mt5: MetaTrader | MetaTester + positions: tuple[TradePosition, ...] - def __init__(self, *, symbol: str = "", group: str = "", ticket: int = 0): - """Get Open Positions. - - Keyword Args: - symbol (str): Financial instrument name. - group (str): The filter for arranging a group of necessary symbols. Optional named parameter. If the group - is specified, the function returns only positions meeting a specified criteria for a symbol name. - ticket (int): Position ticket - - """ + def __init__(self): + """Get Open Positions""" self.config = Config() - self.mt5 = MetaTrader() # if self.config.mode == 'live' else MetaTester() - self.symbol = symbol - self.group = group - self.ticket = ticket - - async def positions_total(self) -> int: - """Get the number of open positions. - - Returns: - int: Return total number of open positions - """ - return await self.mt5.positions_total() + self.mt5 = MetaTrader() if self.config.mode == 'live' else MetaTester() + self.positions = () @backoff_decorator - async def positions_get(self, symbol: str = '', group: str = '', ticket: int = 0) -> list[TradePosition]: + async def get_positions(self) -> tuple[TradePosition, ...]: """Get open positions with the ability to filter by symbol or ticket. - Keyword Args: - symbol (str): Financial instrument name. - group (str): The filter for arranging a group of necessary symbols. Optional named parameter. If the group - is specified, the function returns only positions meeting a specified criteria for a symbol name. - ticket (int): Position ticket - Returns: - list[TradePosition]: A list of open trade positions + tuple[TradePosition, ...]: A tuple of open trade positions """ - positions = await self.mt5.positions_get(group=group or self.group, symbol=symbol or self.symbol, - ticket=ticket or self.ticket) + positions = await self.mt5.positions_get() if positions is not None: - return [TradePosition(**pos._asdict()) for pos in positions] + self.positions = tuple(TradePosition(**pos._asdict()) for pos in positions) + return self.positions + logger.warning('Failed to get open positions for') + return () - logger.warning(f'Failed to get positions for {symbol or self.symbol}') - return [] - - async def position_get(self, *, ticket: int) -> TradePosition | None: + async def get_position_by_ticket(self, *, ticket: int) -> TradePosition | None: """Get an open position by ticket. Args: ticket (int): Position ticket. @@ -80,16 +50,27 @@ class Positions: Returns: TradePosition: Return an open position """ - positions = await self.positions_get(ticket=ticket) + positions = await self.mt5.positions_get(ticket=ticket) position = positions[0] if positions else None - if position is None or position.ticket != ticket: return None + return TradePosition(**position._asdict()) - return position + async def get_position_by_symbol(self, *, symbol: str) -> tuple[TradePosition, ...]: + """Get open positions by symbol. + Args: + symbol (str): Financial instrument name. - async def close(self, *, ticket: int, symbol: str, price: float, volume: float, order_type: OrderType): + Returns: + tuple[TradePosition, ...]: A tuple of open trade positions + """ + positions = await self.mt5.positions_get(symbol=symbol) + return tuple(TradePosition(**pos._asdict()) for pos in (positions or ())) + + @staticmethod + async def close(ticket: int, symbol: str, price: float, volume: float, order_type: OrderType) -> OrderSendResult: """Close an open position for the trading account using the ticket and other parameters. + Args: ticket (int): Position ticket. symbol (str): Financial instrument name. @@ -101,31 +82,27 @@ class Positions: type=order_type.opposite) return await order.send() - async def close_by(self, pos: TradePosition): + @staticmethod + async def close_by(pos: TradePosition) -> OrderSendResult: """Close an open position for the trading account.""" order = Order(position=pos.ticket, symbol=pos.symbol, volume=pos.volume, type=pos.type.opposite, price=pos.price_current, action=TradeAction.DEAL) return await order.send() - async def close_position(self, *, position: TradePosition): + @staticmethod + async def close_position(position: TradePosition): """Close an open position for the trading account. Using a position object.""" order = Order(position=position.ticket, symbol=position.symbol, volume=position.volume, type=position.type.opposite, price=position.price_current, action=TradeAction.DEAL) return await order.send() - async def close_all(self, symbol: str = '', group: str = '') -> int: + async def close_all(self) -> int: """Close all open positions for the trading account. Specify a symbol or group to filter positions. - Keyword Args: - symbol (str): Financial instrument name. - group (str): The filter for specifying a group of symbols. - Returns: int: Return number of positions closed. """ - symbol = symbol or self.symbol - group = group or self.group - positions = [pos for pos in await self.positions_get(symbol=symbol, group=group)] - orders = [self.close_position(position=pos) for pos in positions] - results = await asyncio.gather(*[order for order in orders], return_exceptions=True) - return len([res for res in results if (res and res.retcode) == 10009]) + positions = self.positions or await self.get_positions() + results = await asyncio.gather(*(self.close_position(position) for position in positions), + return_exceptions=True) + return len([res for res in results if (isinstance(res, OrderSendResult) and res.retcode == 10009)]) diff --git a/src/aiomql/ram.py b/src/aiomql/ram.py index 354a496..a768ebb 100644 --- a/src/aiomql/ram.py +++ b/src/aiomql/ram.py @@ -7,13 +7,10 @@ class RAM: account: Account risk_to_reward: float risk: float - points: float - pips: float min_amount: float = 0 max_amount: float = 0 - risk_level: float = 50 loss_limit: int = 3 - open_limit: int = 6 + open_limit: int = 5 def __init__(self, *, risk_to_reward: float = 1, risk: float = 0.01, **kwargs): """Initialize Risk Assessment and Management with the provided keyword arguments. @@ -29,7 +26,7 @@ class RAM: [setattr(self, key, value) for key, value in kwargs.items()] async def get_amount(self) -> float: - """Calculate the amount to risk per trade as a percentage of equity. + """Calculate the amount to risk per trade as a percentage of margin_free. Returns: float: Amount to risk per trade @@ -40,27 +37,21 @@ class RAM: return max(self.min_amount, min(self.max_amount, amount)) return amount - async def check_losing_positions(self, *, symbol: str = '') -> bool: - """Check if the number of losing positions is greater than or equal the loss limit. + async def check_losing_positions(self) -> bool: + """Check if the number of losing positions is greater than or equal the loss limit - Args: - symbol (str): Symbol to check. Defaults to ''. + Returns: + bool: True if the number of losing positions is less than the loss limit """ - positions = await Positions().positions_get(symbol=symbol) - loosing = [trade for trade in positions if trade.profit <= 0] - return len(loosing) >= self.loss_limit + positions = await Positions().get_positions() + loosing = [position for position in positions if position.profit < 0] + return len(loosing) < self.loss_limit - async def check_open_positions(self, *, symbol: str = '') -> bool: + async def check_open_positions(self) -> bool: """Check if the number of open positions is greater than or equal the loss limit. - Args: - symbol (str): Symbol to check. Defaults to ''. + Returns: + bool: True if the number of open positions is less than the open limit """ - positions = await Positions().positions_get(symbol=symbol) - return len(positions) >= self.open_limit - - async def check_risk_level(self) -> bool: - """Check the risk level.""" - await self.account.refresh() - risk_level = (1 - (self.account.margin_free / self.account.equity)) * 100 - return risk_level >= self.risk_level + positions = await Positions().get_positions() + return len(positions) < self.open_limit diff --git a/src/aiomql/result.py b/src/aiomql/result.py index 37d37a5..da2a70d 100644 --- a/src/aiomql/result.py +++ b/src/aiomql/result.py @@ -4,6 +4,8 @@ from logging import getLogger from typing import Iterable, Literal from asyncio import Lock +from _typeshed import SupportsWrite, SupportsRead + from .core.config import Config from .core.models import OrderSendResult @@ -57,16 +59,18 @@ class Result: data = self.get_data() file = self.config.records_dir / f"{self.name}.csv" file.touch(exist_ok=True) if not file.exists() else ... - reader: Iterable[dict] = csv.DictReader(file.open('r', newline='')) + read_file = file.open('r', newline='') + reader: Iterable[dict] = csv.DictReader(read_file) + read_file.close() rows: list[dict] = [] headers = set() [(rows.append(row), headers.update(row.keys())) for row in reader] rows.append(data) headers.update(data.keys()) - writer = csv.DictWriter(file.open('w', newline=''), fieldnames=headers, restval=None, - extrasaction='ignore') - writer.writeheader() - writer.writerows(rows) + with file.open('w', newline='') as write_file: # type: SupportsWrite[str] + writer = csv.DictWriter(write_file, fieldnames=headers, restval=None, extrasaction='ignore') + writer.writeheader() + writer.writerows(rows) except Exception as err: logger.error(f'Unable to save to csv: {err}') @@ -89,14 +93,14 @@ class Result: try: file = self.config.records_dir / f"{self.name}.json" data = self.get_data() - exists = file.touch(exist_ok=True) if not file.exists() else True - if not exists: - json.dump([], file.open('w')) - with file.open('r') as fh: + file.touch(exist_ok=True) if not file.exists() else ... + with file.open('r') as fh: # type: SupportsRead[str] rows = json.load(fh) - rows.append(data) - with file.open('w') as fh: + rows.append(data) + + with file.open('w') as fh: # type: SupportsWrite[str] json.dump(rows, fh, indent=2, skipkeys=True, default=self.serialize) + except Exception as err: logger.error(f"Unable to save as json file: {err}") diff --git a/src/aiomql/sessions.py b/src/aiomql/sessions.py index fa905f2..ccc5c0a 100644 --- a/src/aiomql/sessions.py +++ b/src/aiomql/sessions.py @@ -1,12 +1,15 @@ import asyncio from datetime import time, timedelta, datetime -from asyncio import sleep, iscoroutinefunction -from typing import Literal, Callable +from typing import Literal, Callable, Iterable from logging import getLogger +import pytz + +from . import TradePosition +from .core.models import OrderSendResult 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 +23,14 @@ 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() + config = Config() + sleep = config.backtest_engine.cursor.time + secs + async with em.condition: + while sleep > config.backtest_engine.cursor.time: + await em.wait() class Session: @@ -39,7 +43,7 @@ class Session: on_end (str): The action to take when the session ends. Default is None. custom_start (Callable): A custom function to call when the session starts. Default is None. custom_end (Callable): A custom function to call when the session ends. Default is None. - name (str): A name for the session. Default is a combination of start and en + name (str): A name for the session. Default is a combination of start and end. """ def __init__(self, *, start: int | time, end: int | time, on_start: Literal['close_all', 'close_win', 'close_loss', 'custom_start'] = None, @@ -58,76 +62,103 @@ class Session: custom_end (Callable): A custom function to call when the session ends. Default is None. name (str): A name for the session. Default is a combination of start and end. """ - self.start = start if isinstance(start, time) else time(hour=start) - self.end = end if isinstance(end, time) else time(hour=end) + self.start = start if isinstance(start, time) else time(hour=start, tzinfo=pytz.UTC) + self.end = end if isinstance(end, time) else time(hour=end, tzinfo=pytz.UTC) self.on_start = on_start self.on_end = on_end self.custom_start = custom_start self.custom_end = custom_end - self.name = name or f'{self.start} - {self.end}' + self.name = name or f'{self.start}<-->{self.end}' + self.positions_manager = Positions() + self.config = Config() def __contains__(self, item: time): if self.start > self.end: - m1 = time(hour=23, minute=59, second=59, microsecond=9999) - m2 = time(hour=0) - return self.start <= item <= m1 or m2 <= item < self.end - return self.start <= item < self.end + end = timedelta(days=1, hours=self.start.hour, minutes=self.start.minute, seconds=self.start.second, + microseconds=self.start.microsecond) + start = delta(self.start) + if item < self.start and item < self.end: + item = timedelta(days=1, hours=item.hour, minutes=item.minute, seconds=item.second, + microseconds=item.microsecond) + else: + item = delta(item) + else: + start = delta(self.start) + end = delta(self.end) + return start <= item < end def __str__(self): - return f'{self.start}-->{self.name}-->{self.end}' if self.name else f'{self.start}-->{self.end}' + return f'{self.start}<-->{self.end}' def __repr__(self): - return f'{self.start}-->{self.end}' + return f'{self.start}<-->{self.end}' def __len__(self): return (delta(self.start) - delta(self.end)).seconds - + + def in_session(self) -> bool: + """Check if the current time is within the session.""" + now = datetime.now(tz=pytz.UTC).time() if self.config.mode == 'live'\ + else datetime.fromtimestamp(self.config.backtest_engine.cursor.time, tz=pytz.UTC).time() + return now in self + async def begin(self): """Call the action specified in on_start or custom_start.""" - await self.action(self.on_start) + await self.action(action=self.on_start) async def close(self): """Call the action specified in on_end or custom_end.""" - await self.action(self.on_end) + await self.action(action=self.on_end) - async def action(self, action): + async def close_positions(self, *, positions: tuple[TradePosition, ...]): + + results = asyncio.gather(*(self.positions_manager.close_position(pos) for pos in positions), + return_exceptions=True) + closed = pending = 0 + for result in results: + if isinstance(result, OrderSendResult) and result.retcode == 10009: + closed += 1 + continue + pending += 1 + logger.info(f'Closed {closed} positions') + logger.warning(f"{pending} positions still pending") if pending else ... + + async def close_all(self): + open_positions = await self.positions_manager.get_positions() + await self.close_positions(positions=open_positions) + + async def close_win(self): + open_positions = await self.positions_manager.get_positions() + positions = tuple(position for position in open_positions if position.profit >= 0) + await self.close_positions(positions=positions) + + async def close_loss(self): + open_positions = await self.positions_manager.get_positions() + positions = tuple(position for position in open_positions if position.profit < 0) + await self.close_positions(positions=positions) + + async def action(self, *, action): """Used by begin and close to call the action specified. Args: action (Literal['close_all', 'close_win', 'close_loss', 'custom_start', 'custom_end']): The action to take. """ try: - position = Positions() - positions = await position.positions_get() - match action: case 'close_all': - await asyncio.gather(*(position.close(price=pos.price_current, ticket=pos.ticket, - order_type=pos.type, volume=pos.volume, - symbol=pos.symbol) for pos in positions), - return_exceptions=True) + await self.close_all() case 'close_win': - await asyncio.gather( - *(position.close(price=pos.price_current, ticket=pos.ticket, order_type=pos.type, - volume=pos.volume, symbol=pos.symbol) for pos in positions if pos.profit > 0), - return_exceptions=True) + await self.close_win() case 'close_loss': - await asyncio.gather( - *(position.close(price=pos.price_current, ticket=pos.ticket, order_type=pos.type, - volume=pos.volume, symbol=pos.symbol) for pos in positions if - pos.profit < 0), return_exceptions=True) + await self.close_loss() case 'custom_end': - if iscoroutinefunction(self.custom_end): - await self.custom_end() - self.custom_end() + await self.custom_end() case 'custom_start': - if iscoroutinefunction(self.custom_start): - await self.custom_start() - self.custom_start() + await self.custom_start() case _: pass @@ -136,7 +167,12 @@ class Session: def until(self): """Get the seconds until the session starts from the current time in seconds.""" - return (delta(self.start) - delta(datetime.utcnow().time())).seconds + if self.config.mode == 'backtest': + now = datetime.fromtimestamp(self.config.backtest_engine.cursor.time, tz=pytz.UTC).time() + secs = delta(self.start) - delta(now) + else: + secs = (delta(self.start) - delta(datetime.now(tz=pytz.UTC).time())).seconds + return secs class Sessions: @@ -152,10 +188,13 @@ class Sessions: find_next: Find the next session that contains a datetime.time object. check: Check if the current session has started and if not, wait until it starts. """ - def __init__(self, *sessions: Session): + sessions: list[Session] + + def __init__(self, *, sessions: Iterable[Session]): self.sessions = list(sessions) - self.sessions.sort(key=lambda x: (x.start, x.end)) + self.sessions.sort(key=lambda x: (x.start.hour, x.end.hour)) self.current_session = None + self.config = Config() def find(self, obj: time) -> Session | None: """Find a session that contains a datetime.time object. @@ -181,7 +220,7 @@ class Sessions: Session: A Session object. """ for session in self.sessions: - if obj < session.start: + if delta(obj) < delta(session.start): return session return self.sessions[0] @@ -197,23 +236,33 @@ class Sessions: async def check(self): """Check if the current session has started and if not, wait until it starts.""" - now = datetime.utcnow().time() - current_session = self.find(now) - if current_session: - if self.current_session: - if self.current_session == current_session: - return - await self.current_session.close() + if self.current_session is not None and self.current_session.in_session(): + return + + if self.config.mode == 'backtest': + now = datetime.fromtimestamp(self.config.backtest_engine.cursor.time, tz=pytz.UTC).time() + else: + now = datetime.now().time() - self.current_session = current_session + next_session = self.find(now) + + if next_session and self.current_session is None: + self.current_session = next_session await self.current_session.begin() return - await self.current_session.close() if self.current_session else ... - 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 + if next_session and self.current_session is not None: + await self.current_session.close() + self.current_session = next_session + await self.current_session.begin() + + if next_session is None and self.current_session is not None: + await self.current_session.close() + + next_session = self.find_next(now) + secs = next_session.until() + 10 + logger.info(f'sleeping for {secs} seconds until next {next_session} session') + sleep_func = asyncio.sleep if self.config.mode == 'live' else backtest_sleep await sleep_func(secs) - self.current_session = current_session + self.current_session = next_session await self.current_session.begin() diff --git a/src/aiomql/strategy.py b/src/aiomql/strategy.py index b1becef..c9648a0 100644 --- a/src/aiomql/strategy.py +++ b/src/aiomql/strategy.py @@ -46,7 +46,7 @@ class Strategy(ABC): self.name = name or self.__class__.__name__ self.parameters["symbol"] = symbol.name self.parameters["name"] = self.name - self.sessions = sessions or Sessions(Session(start=0, end=dtime(hour=23, minute=59, second=59))) + self.sessions = sessions or Sessions(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() diff --git a/src/aiomql/symbol.py b/src/aiomql/symbol.py index 87dbeb8..ab195ba 100644 --- a/src/aiomql/symbol.py +++ b/src/aiomql/symbol.py @@ -107,16 +107,14 @@ class Symbol(SymbolInfo): bool: Returns True if symbol info was successful initialized """ try: - if await self.symbol_select(): - await self.book_add() - await self.info() - await self.info_tick() + res = await asyncio.gather(self.symbol_select(), self.info(), self.info_tick(), self.book_add(), + return_exceptions=True) + if all(res): return True - logger.warning(f'Unable to initialized symbol {self}') + logger.warning(f'Unable to initialized {self}') return False except Exception as err: - self.select = False - logger.warning(err) + logger.warning(f'{err}: Unable to initialized {self}') return False async def book_add(self) -> bool: diff --git a/tests/__init__.py b/tests/__init__.py index db802e0..67c5895 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,5 +1,5 @@ import pytest -from aiomql import MetaTester, TestData, MetaTrader, Config +from aiomql import Config import MetaTrader5 @@ -10,5 +10,5 @@ def config(): @pytest.fixture(scope='session', autouse=True) -def metatrader5(config): - return MetaTrader5 \ No newline at end of file +def metatrader5(): + return MetaTrader5 diff --git a/tests/test.json b/tests/test.json index e69de29..e322524 100644 --- a/tests/test.json +++ b/tests/test.json @@ -0,0 +1,10 @@ +{ + "login": 31288540, + "password": "nwa0#anaEze", + "server": "Deriv-Demo", + "demo": 5463204, + "fin": 24251812, + "deriv-demo": 5463204, + "deriv-real": 31288540, + "mode": "backtest" +} diff --git a/tests/test_meta_trader.py b/tests/test_meta_trader.py index 4ebd2e6..515f72f 100644 --- a/tests/test_meta_trader.py +++ b/tests/test_meta_trader.py @@ -9,16 +9,17 @@ from . import metatrader5 class TestMetaTrader: + @classmethod - def setup_class(self): + def setup_class(cls, metatrader5): tz = pytz.timezone('Etc/UTC') - self.mt = MetaTrader() - self.mt5 = metatrader5 - self.symbol = "Volatility 100 Index" + cls.mt = MetaTrader() + cls.mt5 = metatrader5 + cls.symbol = "Volatility 100 Index" now = datetime.now(tz=tz) - self.start = now - timedelta(hours=24) - self.end = now + timedelta(hours=2) - self.tf = self.mt.TIMEFRAME_H1 + cls.start = now - timedelta(hours=24) + cls.end = now + timedelta(hours=2) + cls.tf = cls.mt.TIMEFRAME_H1 @pytest.mark.asyncio async def test_initialize(self):