mirror of
https://github.com/Ichinga-Samuel/aiomql.git
synced 2026-08-23 00:38:07 +00:00
testdata
This commit is contained in:
+7
-73
@@ -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
@@ -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())]
|
||||||
|
|||||||
@@ -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,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',
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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()
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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:
|
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):
|
||||||
|
|||||||
Reference in New Issue
Block a user