This commit is contained in:
Ichinga Samuel
2024-10-02 00:25:24 +01:00
parent 81bdedcdaa
commit e34d96f064
20 changed files with 312 additions and 475 deletions
+7 -73
View File
@@ -1,7 +1,7 @@
import asyncio
from logging import getLogger from logging import getLogger
from typing import Self
from .core.models import AccountInfo, SymbolInfo from .core.models import AccountInfo
from .core.exceptions import LoginError from .core.exceptions import LoginError
logger = getLogger(__name__) logger = getLogger(__name__)
@@ -13,33 +13,26 @@ class Account(AccountInfo):
Attributes: Attributes:
connected (bool): Status of connection to MetaTrader 5 Terminal connected (bool): Status of connection to MetaTrader 5 Terminal
symbols (set[SymbolInfo]): A set of available symbols for the financial market.
Notes: Notes:
Other Account properties are defined in the AccountInfo class. Other Account properties are defined in the AccountInfo class.
""" """
_instance: 'Account' _instance: Self
connected: bool connected: bool
symbols = set()
def __new__(cls, *args, **kwargs): def __new__(cls, *args, **kwargs):
if not hasattr(cls, '_instance'): if not hasattr(cls, '_instance'):
cls._instance = super().__new__(cls) cls._instance = super().__new__(cls)
cls._instance.exclude = cls._instance.exclude | {'_instance'}
return cls._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): async def refresh(self):
"""Refreshes the account instance with the latest account details from the MetaTrader 5 terminal""" """Refreshes the account instance with the latest account details from the MetaTrader 5 terminal"""
account_info = await self.mt5.account_info() account_info = await self.mt5.account_info()
acc = account_info._asdict() acc = account_info._asdict()
self.set_attributes(**acc) self.set_attributes(**acc)
async def __aenter__(self) -> 'Account': async def __aenter__(self) -> Self:
"""Connect to a trading account and return the account instance. """Connect to a trading account and return the account instance.
Async context manager for the Account class. Async context manager for the Account class.
@@ -49,70 +42,11 @@ class Account(AccountInfo):
Raises: Raises:
LoginError: If login fails LoginError: If login fails
""" """
res = await self.sign_in() self.connected = await self.mt5.login()
if not res: if not self:
raise LoginError('Login failed') raise LoginError('Login failed')
return self return self
async def __aexit__(self, exc_type, exc_val, exc_tb): async def __aexit__(self, exc_type, exc_val, exc_tb):
await self.mt5.shutdown() await self.mt5.shutdown()
self.connected = False 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}
+31 -44
View File
@@ -1,38 +1,34 @@
import asyncio import asyncio
from concurrent.futures import ProcessPoolExecutor from concurrent.futures import ProcessPoolExecutor
from typing import Type, Iterable, TypeVar, Callable, Coroutine from typing import Type, Iterable, Callable, Coroutine
import logging import logging
from .executor import Executor from .executor import Executor
from .account import Account
from .core.config import Config from .core.config import Config
from .core.meta_trader import MetaTrader
from .symbol import Symbol as Symbol from .symbol import Symbol as Symbol
from .strategy import Strategy as Strategy from .strategy import Strategy as Strategy
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class Bot: class Bot:
"""The bot class. Create a bot instance to run your strategies. """The bot class. Create a bot instance to run your strategies.
Attributes: Attributes:
account (Account): Account Object.
executor: The default thread executor. executor: The default thread executor.
symbols (list[Symbols]): A set of symbols for the trading session
config (Config): Config instance config (Config): Config instance
mt (MetaTrader): MetaTrader instance
""" """
config: Config config: Config
account: Account
symbols: set
executor: Executor executor: Executor
mt: MetaTrader
def __init__(self): def __init__(self):
self.config = Config() self.config = Config(bot=self)
self.account = Account()
self.symbols = set()
self.executor = Executor() self.executor = Executor()
self.mt = MetaTrader()
@classmethod @classmethod
def run_bots(cls, funcs: dict[Callable: dict] = None, num_workers: int = None): 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 SystemExit if sign in was not successful
""" """
try: try:
init = await self.account.sign_in() login = await self.mt.login()
if not init: if not login:
logger.warning(f"Unable to sign in to MetaTrder 5 Terminal") logger.warning(f"Unable to sign in to MetaTrder 5 Terminal")
raise SystemExit raise SystemExit
logger.info("Login Successful") logger.info("Login Successful")
await self.init_symbols() await self.init_strategies()
self.executor.remove_workers(symbols=self.symbols)
self.add_coroutine(self.config.task_queue.start) self.add_coroutine(self.config.task_queue.start)
self.config.bot = self
except Exception as err: except Exception as err:
logger.error(f"{err}. Bot initialization failed") logger.error(f"{err}. Bot initialization failed")
raise SystemExit raise SystemExit
def add_function(self, func: Callable, **kwargs: dict): def add_function(self, func: Callable[..., ...], **kwargs: dict):
"""Add a function to the executor. """Add a function to the executor.
Args: Args:
@@ -77,7 +71,7 @@ class Bot:
""" """
self.executor.add_function(func, kwargs) 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. """Add a coroutine to the executor.
Args: Args:
@@ -117,39 +111,32 @@ class Bot:
""" """
[self.add_strategy(strategy) for strategy in strategies] [self.add_strategy(strategy) for strategy in strategies]
def add_strategy_all(self, *, strategy: Type[Strategy], params: dict | None = None): 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 symbols: list[Symbol] = None, **kwargs):
i.e. one set of parameters for all trading symbols """Use this to run a single strategy on multiple symbols with the same parameters and keyword arguments.
Keyword Args: Keyword Args:
strategy (Strategy): Strategy class strategy (Strategy): Strategy class
params (dict): A dictionary of parameters for the strategy 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)) self.add_strategy(strategy(symbol=symbol, params=params, **kwargs))
for symbol in self.symbols 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.""" """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] tasks = [self.init_strategy(strategy) for strategy in self.executor.workers]
await asyncio.gather(*syms, return_exceptions=True) for task in asyncio.as_completed(tasks):
res = await task
async def init_symbol(self, symbol: Symbol) -> Symbol: if not res[0]:
"""Initialize a symbol before the beginning of a trading sessions. logger.warning(f"Failed to initialize symbol {res[1].symbol}")
Removes it from the list of symbols if it was not successfully initialized or not available self.executor.workers.remove(res[1])
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")
@@ -391,7 +391,7 @@ class BackTestEngine:
'sl': sl, 'tp': tp, 'time': current_tick.time, 'time_msc': current_tick.time_msc, '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} '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, '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, 'volume': volume, 'price': price, 'type': DealType(order_type), 'reason': DealReason.EXPERT,
'entry': DealEntry.IN} 'entry': DealEntry.IN}
@@ -6,7 +6,6 @@ from .event_manager import EventManager
from .meta_tester import MetaTester from .meta_tester import MetaTester
from .backtest_engine import BackTestEngine from .backtest_engine import BackTestEngine
from .strategy_tester import StrategyTester from .strategy_tester import StrategyTester
from ...core import Config
logger = getLogger(__name__) logger = getLogger(__name__)
@@ -22,7 +21,6 @@ class BackTester:
try: try:
await self.mt5.initialize() await self.mt5.initialize()
strategies = [strategy for strategy in self.strategies if await strategy.symbol.init()] 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) self.event_manager.num_main_tasks = len(strategies)
tasks = [*[asyncio.create_task(strategy.test()) for strategy in strategies], tasks = [*[asyncio.create_task(strategy.test()) for strategy in strategies],
asyncio.create_task(self.event_manager.event_monitor())] asyncio.create_task(self.event_manager.event_monitor())]
+4 -20
View File
@@ -1,21 +1,5 @@
glob = dict()
class Check: class Check1:
def __init__(self, ty): f: str
self.r = ty t: str
v: float
@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)
+1 -1
View File
@@ -1,10 +1,10 @@
from typing import Callable from typing import Callable
import MetaTrader5 import MetaTrader5
from MetaTrader5 import (Tick, SymbolInfo, AccountInfo, TerminalInfo, TradeOrder, TradePosition, TradeDeal, from MetaTrader5 import (Tick, SymbolInfo, AccountInfo, TerminalInfo, TradeOrder, TradePosition, TradeDeal,
OrderCheckResult, OrderSendResult, BookInfo, TradeRequest) OrderCheckResult, OrderSendResult, BookInfo, TradeRequest)
from .config import Config
constants = ('TIMEFRAME_M1', 'TIMEFRAME_M2', 'TIMEFRAME_M3', 'TIMEFRAME_M4', 'TIMEFRAME_M5', 'TIMEFRAME_M6', 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', 'TIMEFRAME_M10', 'TIMEFRAME_M12', 'TIMEFRAME_M15', 'TIMEFRAME_M20', 'TIMEFRAME_M30', 'TIMEFRAME_H1',
+2 -2
View File
@@ -1,6 +1,6 @@
import os import os
from pathlib import Path from pathlib import Path
from typing import Iterator, Literal, TypeVar from typing import Iterator, Literal, TypeVar, Self
import json import json
from logging import getLogger from logging import getLogger
@@ -51,7 +51,7 @@ class Config:
task_queue: TaskQueue task_queue: TaskQueue
_backtest_engine: BackTestEngine _backtest_engine: BackTestEngine
bot: Bot bot: Bot
_instance: 'Config' _instance: Self
mode: Literal['backtest', 'live'] mode: Literal['backtest', 'live']
use_terminal_for_backtesting: bool use_terminal_for_backtesting: bool
_defaults = {"timeout": 60000, "record_trades": True, "trade_record_mode": "csv", "mode": "live", _defaults = {"timeout": 60000, "record_trades": True, "trade_record_mode": "csv", "mode": "live",
+1 -5
View File
@@ -19,7 +19,6 @@ class AccountInfo(Base):
Attributes: Attributes:
login: int login: int
password: str
server: str server: str
trade_mode: AccountTradeMode trade_mode: AccountTradeMode
balance: float balance: float
@@ -51,7 +50,6 @@ class AccountInfo(Base):
company: str company: str
""" """
login: int = 0 login: int = 0
password: str = ''
server: str = '' server: str = ''
trade_mode: AccountTradeMode trade_mode: AccountTradeMode
balance: float balance: float
@@ -334,9 +332,7 @@ class SymbolInfo(Base):
path: str path: str
def __init__(self, **kwargs): def __init__(self, **kwargs):
if (name := kwargs.pop('name', '')) == '': assert 'name' in kwargs, "Symbol Object Must be initialized with a name"
raise AttributeError('Symbol Object Must be initialized with a name')
self.name = name
super().__init__(**kwargs) super().__init__(**kwargs)
def __repr__(self): def __repr__(self):
-4
View File
@@ -38,10 +38,6 @@ class Executor:
""" """
self.workers.extend(strategies) 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): def add_worker(self, strategy: Strategy):
"""Add a strategy instance to the list of workers """Add a strategy instance to the list of workers
+34 -103
View File
@@ -1,15 +1,15 @@
import asyncio
from datetime import datetime from datetime import datetime
from logging import getLogger from logging import getLogger
import pytz
from pandas import DataFrame from pandas import DataFrame
import pandas as pd import pandas as pd
from .core.config import Config from .core.config import Config
from .core.meta_trader import MetaTrader, CopyTicks, OrderType from .core.meta_trader import MetaTrader, CopyTicks, OrderType
from .core.models import TradeDeal, TradeOrder from .core.models import TradeDeal, TradeOrder
from .contrib.backtester.meta_tester import MetaTester from .contrib.backtester.meta_tester import MetaTester
from .utils import backoff_decorator from .utils import backoff_decorator
logger = getLogger(__name__) logger = getLogger(__name__)
@@ -24,71 +24,56 @@ class History:
total_deals: Total number of deals total_deals: Total number of deals
total_orders (int): Total number orders total_orders (int): Total number orders
group (str): Filter for selecting history by symbols. 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 mt5 (MetaTrader): MetaTrader instance
config (Config): Config instance config (Config): Config instance
""" """
mt5: MetaTrader | MetaTester mt5: MetaTrader | MetaTester
config: Config config: Config
def __init__(self, *, date_from: datetime | int = None, date_to: datetime | int = None, def __init__(self, *, date_from: datetime | int, date_to: datetime | int, group: str = '', use_utc: bool = True):
group: str = "", ticket: int = None, position: int = None):
""" """
Args: 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' 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" number of seconds elapsed since 1970.01.01. Defaults to the current time in "utc"
group (str): Filter for selecting history by symbols. group (str): Filter for selecting history by symbols. Defaults to an empty string
ticket (int): Filter for selecting history by ticket number
position (int): Filter for selecting history deals by position
""" """
self.config = Config() self.config = Config()
self.mt5 = MetaTrader() if self.config.mode == 'live' else MetaTester() self.mt5 = MetaTrader() if self.config.mode == 'live' else MetaTester()
self.date_from = date_from date_from = date_from if isinstance(date_from, datetime) else datetime.fromtimestamp(date_from)
self.date_to = date_to 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.group = group
self.ticket = ticket self.deals: tuple[TradeDeal, ...] = ()
self.position = position self.orders: tuple[TradeOrder, ...] = ()
self.deals: list[TradeDeal] = []
self.orders: list[TradeOrder] = []
self.total_deals: int = 0 self.total_deals: int = 0
self.total_orders: int = 0 self.total_orders: int = 0
async def init(self, deals=True, orders=True): async def init(self):
"""Get history deals and orders """Get history deals and orders"""
deals, orders = await asyncio.gather(self.get_deals(), self.get_orders(), return_exceptions=True)
Keyword Args: self.deals = deals if isinstance(deals, tuple) else ()
deals (bool): If true get history deals during initial request to terminal self.orders = orders if isinstance(orders, tuple) else ()
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()
self.total_deals = len(self.deals) self.total_deals = len(self.deals)
self.total_orders = len(self.orders) self.total_orders = len(self.orders)
@backoff_decorator @backoff_decorator
async def get_deals(self, *, date_from: datetime | int = None, date_to: datetime | int = None, group: str = '')\ async def get_deals(self) -> tuple[TradeDeal, ...]:
-> tuple[TradeDeal, ...]:
"""Get deals from trading history using the parameters set in the constructor. """Get deals from trading history using the parameters set in the constructor.
Returns: Returns:
tuple[TradeDeal]: A list of trade deals 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=self.date_from, date_to=self.date_to, group=self.group)
deals = await self.mt5.history_deals_get(date_from=date_from, date_to=date_to, group=group)
if deals is not None: if deals is not None:
return tuple(TradeDeal(**deal._asdict()) for deal in deals) return tuple(TradeDeal(**deal._asdict()) for deal in deals)
logger.warning(f'Failed to get deals') logger.warning(f'Failed to get deals')
return tuple() return tuple()
@backoff_decorator def get_deals_by_ticket(self, *, ticket: int) -> tuple[TradeDeal, ...]:
async def get_deals_ticket(self, *, ticket: int = None) -> tuple[TradeDeal, ...]:
"""Call specifying the order ticket. Return all deals having the specified order ticket in the DEAL_ORDER """Call specifying the order ticket. Return all deals having the specified order ticket in the DEAL_ORDER
property. property.
@@ -98,13 +83,9 @@ class History:
Returns: Returns:
tuple[TradeDeal]: A tuple of all deals with the order ticket tuple[TradeDeal]: A tuple of all deals with the order ticket
""" """
ticket = ticket or self.ticket return tuple(sorted((deal for deal in self.deals if deal.order == ticket), key=lambda x: x.time_msc))
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))
@backoff_decorator def get_deals_by_position(self, *, position: int = None) -> tuple[TradeDeal, ...]:
async def get_deals_position(self, *, position: int = None) -> tuple[TradeDeal, ...]:
""" """
Get all deals with the specified position ticket in the DEAL_POSITION_ID property Get all deals with the specified position ticket in the DEAL_POSITION_ID property
Args: Args:
@@ -113,36 +94,16 @@ class History:
Returns: Returns:
tuple[TradeDeal]: A tuple of all deals with the position ticket tuple[TradeDeal]: A tuple of all deals with the position ticket
""" """
position = position or self.position return tuple(sorted((deal for deal in self.deals if deal.position_id == position), key=lambda x: x.time_msc))
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
@backoff_decorator @backoff_decorator
async def get_orders(self, *, date_from: datetime | int = None, date_to: datetime | int = None, group: str = '', async def get_orders(self) -> tuple[TradeOrder, ...]:
retries: int = 3) -> tuple[TradeOrder, ...]:
"""Get orders from trading history using the parameters set in the constructor or the method arguments. """Get orders from trading history using the parameters set in the constructor or the method arguments.
Returns: Returns:
list[TradeOrder]: A list of trade orders 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=self.date_from, date_to=self.date_to, group=self.group)
orders = await self.mt5.history_orders_get(date_from=date_from, date_to=date_to, group=group)
if orders is not None: if orders is not None:
return tuple(TradeOrder(**order._asdict()) for order in orders) return tuple(TradeOrder(**order._asdict()) for order in orders)
@@ -150,45 +111,15 @@ class History:
logger.warning(f'Failed to get orders') logger.warning(f'Failed to get orders')
return tuple() return tuple()
@backoff_decorator def get_orders_by_ticket(self, ticket: int) -> tuple[TradeOrder, ...]:
async def get_order_ticket(self, ticket: int | None = None) -> TradeOrder | None: """filter orders by ticket"""
ticket = ticket or self.ticket return tuple(sorted((order for order in self.orders if order.ticket == ticket), key=lambda x: x.time_done_msc))
assert isinstance(ticket, int), 'ticket not provided'
orders = await self.mt5.history_orders_get(ticket=ticket)
if orders and (order := orders[0]).ticket == ticket:
return TradeOrder(**order._asdict())
return None def get_orders_by_position(self, position: int) -> tuple[TradeOrder, ...]:
""" filter orders by position"""
@backoff_decorator return tuple(sorted((order for order in self.orders if order.position_id == position),
async def get_orders_position(self, position: int = None) -> tuple[TradeOrder, ...]: key=lambda x: x.time_done_msc))
"""
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
async def track_order(self, *, position: int = None, end_time: datetime = None) -> DataFrame: async def track_order(self, *, position: int = None, end_time: datetime = None) -> DataFrame:
""" """
@@ -206,8 +137,8 @@ class History:
Returns: Returns:
DataFrame: A pandas DataFrame of the ticks and profit for the order. DataFrame: A pandas DataFrame of the ticks and profit for the order.
""" """
orders = await self.get_orders_position(position=position) orders = self.get_orders_by_position(position=position)
deals = await self.get_deals_position(position=position) deals = self.get_deals_by_position(position=position)
open_order = orders[0] open_order = orders[0]
open_deal = deals[0] open_deal = deals[0]
close_deal = deals[-1] close_deal = deals[-1]
+22 -41
View File
@@ -1,9 +1,10 @@
from logging import getLogger 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.constants import TradeAction, OrderTime, OrderFilling
from .core.exceptions import OrderError 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__) logger = getLogger(__name__)
@@ -24,25 +25,20 @@ class Order(TradeRequest):
type_time (OrderTime.DAY): Order time type_time (OrderTime.DAY): Order time
type_filling (OrderFilling.FOK): Order filling type_filling (OrderFilling.FOK): Order filling
""" """
if 'symbol' in kwargs: kwargs = {'action': TradeAction.DEAL, OrderTime.DAY: self.type_time, 'type_filling': OrderFilling.FOK, **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)
super().__init__(**kwargs) super().__init__(**kwargs)
async def orders_total(self): async def orders_total(self):
"""Get the number of active orders. """Get the number of active pending orders.
Returns: Returns:
(int): total number of active orders (int): total number of active orders
""" """
return await self.mt5.orders_total() return await self.mt5.orders_total()
@backoff_decorator
async def get_order(self, *, ticket: int) -> TradeOrder | None: async def get_order(self, *, ticket: int) -> TradeOrder | None:
""" """
Get the order by ticket number. Get a pending order by ticket number.
Args: Args:
ticket (int): Order ticket number ticket (int): Order ticket number
@@ -50,15 +46,15 @@ class Order(TradeRequest):
Returns: Returns:
""" """
orders = await self.mt5.orders_get(ticket=ticket) 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, ...]: 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: Keyword Args:
ticket (int): Order ticket number ticket (int): Order ticket number
symbol (str): Symbol name symbol (str): Symbol name
@@ -66,15 +62,12 @@ class Order(TradeRequest):
Returns: Returns:
tuple[TradeOrder]: A Tuple of active trade orders as TradeOrder objects 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) orders = await self.mt5.orders_get(symbol=symbol, ticket=ticket, group=group)
if orders is not None: if orders is not None:
orders = (TradeOrder(**order._asdict()) for order in orders) return tuple(TradeOrder(**order._asdict()) for order in orders)
return tuple(orders)
return tuple() return tuple()
@backoff_decorator
async def check(self, **kwargs) -> OrderCheckResult: async def check(self, **kwargs) -> OrderCheckResult:
"""Check funds sufficiency for performing a required trading operation and the possibility of executing it. """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) res = await self.mt5.order_check(req)
if res is None: if res is None:
raise OrderError(f'Order check failed for {self.symbol}') raise OrderError(f'Order check failed for {self.symbol}')
return OrderCheckResult(**res._asdict()) return res
@backoff_decorator
async def send(self) -> OrderSendResult: async def send(self) -> OrderSendResult:
"""Send a request to perform a trading operation from the terminal to the trade server. """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) res = await self.mt5.order_send(self.dict)
if res is None: if res is None:
raise OrderError(f'Failed to send order {self.symbol}') 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 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. """Return the required margin in the account currency to perform a specified trading operation.
Returns: Returns:
@@ -122,21 +108,16 @@ class Order(TradeRequest):
OrderError: If not successful OrderError: If not successful
""" """
res = await self.mt5.order_calc_margin(self.type, self.symbol, self.volume, self.price) 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 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. """Return profit in the account currency for a specified trading operation.
Returns: Returns:
float: Returns float value if successful float: Returns float value if successful
None: If not successful None: If not successful
""" """
include = {'tp', 'price', 'symbol', 'volume', 'type'} action, symbol, volume, price_open, price_close = self.type, self.symbol, self.volume, self.price, self.tp
args = self.get_dict(include=include) res = await self.mt5.order_calc_profit(action, symbol, volume, price_open, price_close)
args |= kwargs
if len(include.intersection(args.keys())) < len(include):
return None
res = await self.mt5.order_calc_profit(args['type'], args['symbol'], args['volume'], args['price'], args['tp'])
return res return res
+41 -64
View File
@@ -3,12 +3,10 @@ import asyncio
from logging import getLogger from logging import getLogger
from .core.meta_trader import MetaTrader from .core.meta_trader import MetaTrader
from .core.models import TradePosition, TradeAction from .core.models import TradePosition, OrderSendResult
from .core.constants import OrderType from .core.constants import OrderType, TradeAction
from .core.config import Config from .core.config import Config
from .contrib.backtester.meta_tester import MetaTester
# from .contrib.backtester.meta_tester import MetaTester
from .order import Order from .order import Order
from .utils import backoff_decorator from .utils import backoff_decorator
@@ -19,60 +17,32 @@ class Positions:
"""Get Open Positions. """Get Open Positions.
Attributes: 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): MetaTrader instance.
""" """
mt5: MetaTrader #| MetaTester mt5: MetaTrader | MetaTester
positions: tuple[TradePosition, ...]
def __init__(self, *, symbol: str = "", group: str = "", ticket: int = 0): def __init__(self):
"""Get Open Positions. """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
"""
self.config = Config() self.config = Config()
self.mt5 = MetaTrader() # if self.config.mode == 'live' else MetaTester() self.mt5 = MetaTrader() if self.config.mode == 'live' else MetaTester()
self.symbol = symbol self.positions = ()
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()
@backoff_decorator @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. """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: 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, positions = await self.mt5.positions_get()
ticket=ticket or self.ticket)
if positions is not None: 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}') async def get_position_by_ticket(self, *, ticket: int) -> TradePosition | None:
return []
async def position_get(self, *, ticket: int) -> TradePosition | None:
"""Get an open position by ticket. """Get an open position by ticket.
Args: Args:
ticket (int): Position ticket. ticket (int): Position ticket.
@@ -80,16 +50,27 @@ class Positions:
Returns: Returns:
TradePosition: Return an open position 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 position = positions[0] if positions else None
if position is None or position.ticket != ticket: if position is None or position.ticket != ticket:
return None 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. """Close an open position for the trading account using the ticket and other parameters.
Args: Args:
ticket (int): Position ticket. ticket (int): Position ticket.
symbol (str): Financial instrument name. symbol (str): Financial instrument name.
@@ -101,31 +82,27 @@ class Positions:
type=order_type.opposite) type=order_type.opposite)
return await order.send() 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.""" """Close an open position for the trading account."""
order = Order(position=pos.ticket, symbol=pos.symbol, volume=pos.volume, type=pos.type.opposite, order = Order(position=pos.ticket, symbol=pos.symbol, volume=pos.volume, type=pos.type.opposite,
price=pos.price_current, action=TradeAction.DEAL) price=pos.price_current, action=TradeAction.DEAL)
return await order.send() 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.""" """Close an open position for the trading account. Using a position object."""
order = Order(position=position.ticket, symbol=position.symbol, volume=position.volume, order = Order(position=position.ticket, symbol=position.symbol, volume=position.volume,
type=position.type.opposite, price=position.price_current, action=TradeAction.DEAL) type=position.type.opposite, price=position.price_current, action=TradeAction.DEAL)
return await order.send() 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. """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: Returns:
int: Return number of positions closed. int: Return number of positions closed.
""" """
symbol = symbol or self.symbol positions = self.positions or await self.get_positions()
group = group or self.group results = await asyncio.gather(*(self.close_position(position) for position in positions),
positions = [pos for pos in await self.positions_get(symbol=symbol, group=group)] return_exceptions=True)
orders = [self.close_position(position=pos) for pos in positions] return len([res for res in results if (isinstance(res, OrderSendResult) and res.retcode == 10009)])
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])
+14 -23
View File
@@ -7,13 +7,10 @@ class RAM:
account: Account account: Account
risk_to_reward: float risk_to_reward: float
risk: float risk: float
points: float
pips: float
min_amount: float = 0 min_amount: float = 0
max_amount: float = 0 max_amount: float = 0
risk_level: float = 50
loss_limit: int = 3 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): def __init__(self, *, risk_to_reward: float = 1, risk: float = 0.01, **kwargs):
"""Initialize Risk Assessment and Management with the provided keyword arguments. """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()] [setattr(self, key, value) for key, value in kwargs.items()]
async def get_amount(self) -> float: 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: Returns:
float: Amount to risk per trade float: Amount to risk per trade
@@ -40,27 +37,21 @@ class RAM:
return max(self.min_amount, min(self.max_amount, amount)) return max(self.min_amount, min(self.max_amount, amount))
return amount return amount
async def check_losing_positions(self, *, symbol: str = '') -> bool: async def check_losing_positions(self) -> bool:
"""Check if the number of losing positions is greater than or equal the loss limit. """Check if the number of losing positions is greater than or equal the loss limit
Args: Returns:
symbol (str): Symbol to check. Defaults to ''. bool: True if the number of losing positions is less than the loss limit
""" """
positions = await Positions().positions_get(symbol=symbol) positions = await Positions().get_positions()
loosing = [trade for trade in positions if trade.profit <= 0] loosing = [position for position in positions if position.profit < 0]
return len(loosing) >= self.loss_limit 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. """Check if the number of open positions is greater than or equal the loss limit.
Args: Returns:
symbol (str): Symbol to check. Defaults to ''. bool: True if the number of open positions is less than the open limit
""" """
positions = await Positions().positions_get(symbol=symbol) positions = await Positions().get_positions()
return len(positions) >= self.open_limit 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
+15 -11
View File
@@ -4,6 +4,8 @@ from logging import getLogger
from typing import Iterable, Literal from typing import Iterable, Literal
from asyncio import Lock from asyncio import Lock
from _typeshed import SupportsWrite, SupportsRead
from .core.config import Config from .core.config import Config
from .core.models import OrderSendResult from .core.models import OrderSendResult
@@ -57,16 +59,18 @@ class Result:
data = self.get_data() data = self.get_data()
file = self.config.records_dir / f"{self.name}.csv" file = self.config.records_dir / f"{self.name}.csv"
file.touch(exist_ok=True) if not file.exists() else ... 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] = [] rows: list[dict] = []
headers = set() headers = set()
[(rows.append(row), headers.update(row.keys())) for row in reader] [(rows.append(row), headers.update(row.keys())) for row in reader]
rows.append(data) rows.append(data)
headers.update(data.keys()) headers.update(data.keys())
writer = csv.DictWriter(file.open('w', newline=''), fieldnames=headers, restval=None, with file.open('w', newline='') as write_file: # type: SupportsWrite[str]
extrasaction='ignore') writer = csv.DictWriter(write_file, fieldnames=headers, restval=None, extrasaction='ignore')
writer.writeheader() writer.writeheader()
writer.writerows(rows) writer.writerows(rows)
except Exception as err: except Exception as err:
logger.error(f'Unable to save to csv: {err}') logger.error(f'Unable to save to csv: {err}')
@@ -89,14 +93,14 @@ class Result:
try: try:
file = self.config.records_dir / f"{self.name}.json" file = self.config.records_dir / f"{self.name}.json"
data = self.get_data() data = self.get_data()
exists = file.touch(exist_ok=True) if not file.exists() else True file.touch(exist_ok=True) if not file.exists() else ...
if not exists: with file.open('r') as fh: # type: SupportsRead[str]
json.dump([], file.open('w'))
with file.open('r') as fh:
rows = json.load(fh) rows = json.load(fh)
rows.append(data) rows.append(data)
with file.open('w') as fh:
with file.open('w') as fh: # type: SupportsWrite[str]
json.dump(rows, fh, indent=2, skipkeys=True, default=self.serialize) json.dump(rows, fh, indent=2, skipkeys=True, default=self.serialize)
except Exception as err: except Exception as err:
logger.error(f"Unable to save as json file: {err}") logger.error(f"Unable to save as json file: {err}")
+112 -63
View File
@@ -1,12 +1,15 @@
import asyncio import asyncio
from datetime import time, timedelta, datetime from datetime import time, timedelta, datetime
from asyncio import sleep, iscoroutinefunction from typing import Literal, Callable, Iterable
from typing import Literal, Callable
from logging import getLogger from logging import getLogger
import pytz
from . import TradePosition
from .core.models import OrderSendResult
from .positions import Positions from .positions import Positions
from .core.config import Config from .core.config import Config
# from.contrib.backtester.event_manager import EventManager from.contrib.backtester.event_manager import EventManager
logger = getLogger(__name__) 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) return timedelta(hours=obj.hour, minutes=obj.minute, seconds=obj.second, microseconds=obj.microsecond)
# async def backtest_sleep(secs): async def backtest_sleep(secs):
# """A custom function to call when the session starts.""" """A custom function to call when the session starts."""
# # em = EventManager() em = EventManager()
# config = Config()
# async with em.condition: sleep = config.backtest_engine.cursor.time + secs
# while em.config.test_data.cursor.time < (em.config.test_data.cursor.time + secs): async with em.condition:
# await em.condition.wait() while sleep > config.backtest_engine.cursor.time:
await em.wait()
class Session: class Session:
@@ -39,7 +43,7 @@ class Session:
on_end (str): The action to take when the session ends. Default is None. 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_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. 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, def __init__(self, *, start: int | time, end: int | time,
on_start: Literal['close_all', 'close_win', 'close_loss', 'custom_start'] = None, 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. 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. 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.start = start if isinstance(start, time) else time(hour=start, tzinfo=pytz.UTC)
self.end = end if isinstance(end, time) else time(hour=end) self.end = end if isinstance(end, time) else time(hour=end, tzinfo=pytz.UTC)
self.on_start = on_start self.on_start = on_start
self.on_end = on_end self.on_end = on_end
self.custom_start = custom_start self.custom_start = custom_start
self.custom_end = custom_end 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): def __contains__(self, item: time):
if self.start > self.end: if self.start > self.end:
m1 = time(hour=23, minute=59, second=59, microsecond=9999) end = timedelta(days=1, hours=self.start.hour, minutes=self.start.minute, seconds=self.start.second,
m2 = time(hour=0) microseconds=self.start.microsecond)
return self.start <= item <= m1 or m2 <= item < self.end start = delta(self.start)
return self.start <= item < self.end 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): 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): def __repr__(self):
return f'{self.start}-->{self.end}' return f'{self.start}<-->{self.end}'
def __len__(self): def __len__(self):
return (delta(self.start) - delta(self.end)).seconds 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): async def begin(self):
"""Call the action specified in on_start or custom_start.""" """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): async def close(self):
"""Call the action specified in on_end or custom_end.""" """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. """Used by begin and close to call the action specified.
Args: Args:
action (Literal['close_all', 'close_win', 'close_loss', 'custom_start', 'custom_end']): The action to take. action (Literal['close_all', 'close_win', 'close_loss', 'custom_start', 'custom_end']): The action to take.
""" """
try: try:
position = Positions()
positions = await position.positions_get()
match action: match action:
case 'close_all': case 'close_all':
await asyncio.gather(*(position.close(price=pos.price_current, ticket=pos.ticket, await self.close_all()
order_type=pos.type, volume=pos.volume,
symbol=pos.symbol) for pos in positions),
return_exceptions=True)
case 'close_win': case 'close_win':
await asyncio.gather( await self.close_win()
*(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)
case 'close_loss': case 'close_loss':
await asyncio.gather( await self.close_loss()
*(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)
case 'custom_end': case 'custom_end':
if iscoroutinefunction(self.custom_end): await self.custom_end()
await self.custom_end()
self.custom_end()
case 'custom_start': case 'custom_start':
if iscoroutinefunction(self.custom_start): await self.custom_start()
await self.custom_start()
self.custom_start()
case _: case _:
pass pass
@@ -136,7 +167,12 @@ class Session:
def until(self): def until(self):
"""Get the seconds until the session starts from the current time in seconds.""" """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: class Sessions:
@@ -152,10 +188,13 @@ class Sessions:
find_next: Find the next session that contains a datetime.time object. 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. 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 = 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.current_session = None
self.config = Config()
def find(self, obj: time) -> Session | None: def find(self, obj: time) -> Session | None:
"""Find a session that contains a datetime.time object. """Find a session that contains a datetime.time object.
@@ -181,7 +220,7 @@ class Sessions:
Session: A Session object. Session: A Session object.
""" """
for session in self.sessions: for session in self.sessions:
if obj < session.start: if delta(obj) < delta(session.start):
return session return session
return self.sessions[0] return self.sessions[0]
@@ -197,23 +236,33 @@ class Sessions:
async def check(self): async def check(self):
"""Check if the current session has started and if not, wait until it starts.""" """Check if the current session has started and if not, wait until it starts."""
now = datetime.utcnow().time() if self.current_session is not None and self.current_session.in_session():
current_session = self.find(now) return
if current_session:
if self.current_session: if self.config.mode == 'backtest':
if self.current_session == current_session: now = datetime.fromtimestamp(self.config.backtest_engine.cursor.time, tz=pytz.UTC).time()
return else:
await self.current_session.close() 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() await self.current_session.begin()
return return
await self.current_session.close() if self.current_session else ... if next_session and self.current_session is not None:
current_session = self.find_next(now) await self.current_session.close()
secs = current_session.until() + 10 self.current_session = next_session
logger.info(f'sleeping for {secs} seconds until next {current_session} session') await self.current_session.begin()
sleep_func = sleep # if Config().mode == 'live' else backtest_sleep
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) await sleep_func(secs)
self.current_session = current_session self.current_session = next_session
await self.current_session.begin() await self.current_session.begin()
+1 -1
View File
@@ -46,7 +46,7 @@ class Strategy(ABC):
self.name = name or self.__class__.__name__ self.name = name or self.__class__.__name__
self.parameters["symbol"] = symbol.name self.parameters["symbol"] = symbol.name
self.parameters["name"] = self.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.config = Config()
self.mt5 = MetaTrader() if self.config.mode == 'live' else MetaTester() self.mt5 = MetaTrader() if self.config.mode == 'live' else MetaTester()
+5 -7
View File
@@ -107,16 +107,14 @@ class Symbol(SymbolInfo):
bool: Returns True if symbol info was successful initialized bool: Returns True if symbol info was successful initialized
""" """
try: try:
if await self.symbol_select(): res = await asyncio.gather(self.symbol_select(), self.info(), self.info_tick(), self.book_add(),
await self.book_add() return_exceptions=True)
await self.info() if all(res):
await self.info_tick()
return True return True
logger.warning(f'Unable to initialized symbol {self}') logger.warning(f'Unable to initialized {self}')
return False return False
except Exception as err: except Exception as err:
self.select = False logger.warning(f'{err}: Unable to initialized {self}')
logger.warning(err)
return False return False
async def book_add(self) -> bool: async def book_add(self) -> bool:
+3 -3
View File
@@ -1,5 +1,5 @@
import pytest import pytest
from aiomql import MetaTester, TestData, MetaTrader, Config from aiomql import Config
import MetaTrader5 import MetaTrader5
@@ -10,5 +10,5 @@ def config():
@pytest.fixture(scope='session', autouse=True) @pytest.fixture(scope='session', autouse=True)
def metatrader5(config): def metatrader5():
return MetaTrader5 return MetaTrader5
+10
View File
@@ -0,0 +1,10 @@
{
"login": 31288540,
"password": "nwa0#anaEze",
"server": "Deriv-Demo",
"demo": 5463204,
"fin": 24251812,
"deriv-demo": 5463204,
"deriv-real": 31288540,
"mode": "backtest"
}
+8 -7
View File
@@ -9,16 +9,17 @@ from . import metatrader5
class TestMetaTrader: class TestMetaTrader:
@classmethod @classmethod
def setup_class(self): def setup_class(cls, metatrader5):
tz = pytz.timezone('Etc/UTC') tz = pytz.timezone('Etc/UTC')
self.mt = MetaTrader() cls.mt = MetaTrader()
self.mt5 = metatrader5 cls.mt5 = metatrader5
self.symbol = "Volatility 100 Index" cls.symbol = "Volatility 100 Index"
now = datetime.now(tz=tz) now = datetime.now(tz=tz)
self.start = now - timedelta(hours=24) cls.start = now - timedelta(hours=24)
self.end = now + timedelta(hours=2) cls.end = now + timedelta(hours=2)
self.tf = self.mt.TIMEFRAME_H1 cls.tf = cls.mt.TIMEFRAME_H1
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_initialize(self): async def test_initialize(self):