mirror of
https://github.com/Ichinga-Samuel/aiomql.git
synced 2026-08-06 16:57:46 +00:00
testdata
This commit is contained in:
+7
-73
@@ -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}
|
||||
|
||||
+31
-44
@@ -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])
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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())]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
+34
-103
@@ -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]
|
||||
|
||||
+22
-41
@@ -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
|
||||
|
||||
+41
-64
@@ -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)])
|
||||
|
||||
+14
-23
@@ -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
|
||||
|
||||
+15
-11
@@ -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}")
|
||||
|
||||
|
||||
+112
-63
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
+3
-3
@@ -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
|
||||
def metatrader5():
|
||||
return MetaTrader5
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"login": 31288540,
|
||||
"password": "nwa0#anaEze",
|
||||
"server": "Deriv-Demo",
|
||||
"demo": 5463204,
|
||||
"fin": 24251812,
|
||||
"deriv-demo": 5463204,
|
||||
"deriv-real": 31288540,
|
||||
"mode": "backtest"
|
||||
}
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user