This commit is contained in:
Ichinga Samuel
2024-02-12 21:09:28 +01:00
parent 3f53df8375
commit b01e58ee08
68 changed files with 2986 additions and 6869 deletions
+1 -1
View File
@@ -16,4 +16,4 @@ from .trader import Trader
from .terminal import Terminal
from .sessions import Session, Sessions
from .utils import dict_to_string, round_off
from .lib import *
from .lib import *
+4 -2
View File
@@ -1,3 +1,4 @@
import asyncio
from logging import getLogger
from .core.models import AccountInfo, SymbolInfo
@@ -72,7 +73,7 @@ class Account(AccountInfo):
await self.mt5.shutdown()
return False
async def _login(self, *, acc:dict, tries=3):
async def _login(self, *, acc: dict, tries=3):
res = False
if tries == 0:
return False
@@ -82,6 +83,7 @@ class Account(AccountInfo):
if ini and res:
return True
else:
await asyncio.sleep(tries)
return await self._login(acc=acc, tries=tries-1)
def has_symbol(self, symbol: str | SymbolInfo):
@@ -106,4 +108,4 @@ class Account(AccountInfo):
set[Symbol]: A set of available symbols.
"""
syms = await self.mt5.symbols_get()
return {SymbolInfo(name=sym.name) for sym in syms}
return {SymbolInfo(name=sym.name) for sym in syms}
+5 -4
View File
@@ -34,7 +34,7 @@ class Bot:
self.config = Config()
self.account = Account()
self.symbols = set()
self.executor = Executor(bot=self)
self.executor = Executor()
@classmethod
def run_bots(cls, bots: dict[Callable: dict] = None, num_workers: int = None):
@@ -58,8 +58,9 @@ class Bot:
raise SystemExit
logger.info("Login Successful")
await self.init_symbols()
self.executor.remove_workers()
self.executor.remove_workers(symbols=self.symbols)
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
@@ -115,7 +116,7 @@ class Bot:
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
i.e. one set of parameters for all trading symbols
Keyword Args:
strategy (Strategy): Strategy class
@@ -148,4 +149,4 @@ class Bot:
self.symbols.add(symbol)
return symbol
logger.warning(f"Unable to initialize symbol {symbol}")
logger.warning(f"{symbol} not a available for this market")
logger.warning(f"{symbol} not a available for this market")
+34 -8
View File
@@ -12,8 +12,8 @@ logger = getLogger(__name__)
class Candle:
"""A class representing bars from the MetaTrader 5 terminal as a customized class analogous to Japanese Candlesticks.
You can subclass this class for added customization.
"""A class representing bars from the MetaTrader 5 terminal as a customized class analogous to Japanese
Candlesticks. You can subclass this class for added customization.
Attributes:
time (int): Period start time.
@@ -28,22 +28,25 @@ class Candle:
mid (float): The median of the high and low price.
"""
time: float
open: float
high: float
low: float
close: float
real_volume: float
spread: float
open: float
tick_volume: float
Index: int
mid: float
def __init__(self, **kwargs):
"""Create a Candle object from keyword arguments.
"""Create a Candle object from keyword arguments. This class must always be instantiated with open, high, low
and close prices.
Keyword Args:
**kwargs: Candle attributes and values as keyword arguments.
"""
if not all(i in kwargs for i in ['open', 'high', 'low', 'close']):
raise ValueError("Candle must be instantiated with open, high, low and close prices")
self.time = kwargs.pop('time', 0)
self.Index = kwargs.pop('Index', 0)
self.mid = kwargs.pop('mid', (kwargs['high'] + kwargs['low']) / 2)
@@ -55,6 +58,9 @@ class Candle:
"low": self.low, "close": self.close, "time": self.time, "mid": self.mid,
'Index': self.Index}
def __str__(self):
return self.dict()
def __eq__(self, other: "Candle"):
return self.time == other.time
@@ -94,6 +100,21 @@ class Candle:
"""
return self.open > self.close
def dict(self, exclude: set = None, include: set = None) -> dict:
"""
Returns a dictionary of the instance attributes.
Args:
exclude: A set of attributes to exclude from the dictionary. Defaults to None.
include: A set of attributes to include in the dictionary. Defaults to None.
Returns: dict
"""
exclude = exclude or set()
include = include or set()
keys = include or set(self.__dict__.keys()).difference(exclude)
return {k: v for k, v in self.__dict__.items() if k in keys}
_Candle = TypeVar("_Candle", bound=Candle)
_Candles = TypeVar("_Candles", bound="Candles")
@@ -119,8 +140,8 @@ class Candles(Generic[_Candle]):
data (DataFrame): A pandas DataFrame of all candles in the object.
Notes:
The candle class can be customized by subclassing the Candle class and passing the subclass as the candle keyword argument.
Or defining it on the class body as a class attribute.
The candle class can be customized by subclassing the Candle class and passing the subclass as the candle
keyword argument, or defining it on the class body as a class attribute.
"""
Index: Series
time: Series
@@ -134,6 +155,7 @@ class Candles(Generic[_Candle]):
mid: Series
Candle: Type[Candle]
timeframe: TimeFrame
_data: DataFrame
def __init__(self, *, data: DataFrame | _Candles | Iterable, flip=False, candle_class: Type[_Candle] = None):
"""A container class of Candle objects in chronological order.
@@ -193,7 +215,7 @@ class Candles(Generic[_Candle]):
raise TypeError(f"Expected Series got {type(value)}")
def __getattr__(self, item):
if item in list(self._data.columns.values):
if item in self._data.columns:
return self._data[item]
if item == 'Index':
return Series(self._data.index)
@@ -202,6 +224,10 @@ class Candles(Generic[_Candle]):
def __iter__(self):
return (self.Candle(**row._asdict()) for row in self._data.itertuples())
def __add__(self, other: _Candles | _Candle):
other = other.data if isinstance(other, type(self)) else other.dict()
return self.__class__(data=self._data.append(other.data, ignore_index=True))
@property
def timeframe(self):
tf = self.time[1] - self.time[0]
@@ -241,4 +267,4 @@ class Candles(Generic[_Candle]):
Candles: A new instance of the class with the renamed columns if inplace is False else the modified instance
"""
res = self._data.rename(columns=kwargs, inplace=inplace)
return self if inplace else self.__class__(data=res)
return self if inplace else self.__class__(data=res)
+1 -1
View File
@@ -5,4 +5,4 @@ from .constants import *
from .base import Base
from .errors import Error
from .exceptions import *
from .task_queue import TaskQueue
from .task_queue import TaskQueue
+4 -6
View File
@@ -9,10 +9,8 @@ logger = getLogger(__name__)
class Base:
"""A base class for all data model classes in the aiomql package.
This class provides a set of common methods and attributes for all data model classes.
For the data model classes attributes are annotated on the class body and are set as object attributes when the
class is instantiated.
"""A base class for all data structure classes in the aiomql package. This class provides a set of common methods
and attributes for handling data.
"""
mt5: MetaTrader
config: Config
@@ -21,7 +19,7 @@ class Base:
"""
Initialize a new instance of the Base class
Args:
**kwargs: Object attributes and values as keyword arguments. Only added if they are annotated on the class body.
**kwargs: Set instance attributes with keyword arguments. Only if they are annotated on the class body.
"""
self.config = Config()
self.mt5 = MetaTrader()
@@ -119,4 +117,4 @@ class Base:
return {key: value for key, value in (self.class_vars | self.__dict__).items() if
key not in _filter}
except Exception as err:
logger.warning(err)
logger.warning(err)
+28 -14
View File
@@ -16,7 +16,6 @@ class Config:
record_trades (bool): Whether to keep record of trades or not.
filename (str): Name of the config file
records_dir (str): Path to the directory where trade records are saved
win_percentage (float): Percentage of achieved target profit in a trade to be considered a win
login (int): Trading account number
password (str): Trading account password
server (str): Broker server
@@ -31,22 +30,21 @@ class Config:
or the load_config method.
By passing reload=True to the load_config method, you can reload and search again for the config file.
"""
login: int = 0
password: str = ""
server: str = ""
path: str = ""
path: str | Path = ""
timeout: int = 60000
record_trades: bool = True
filename: str = "aiomql.json"
win_percentage: float = 0.85
records_dir = Path(Path.home() / "Documents" / "Aiomql" / "Trade Records").mkdir(parents=True, exist_ok=True)
records_dir: str | Path = 'records'
config_dir: str = ''
_initialize = True
state: dict = {}
root_dir: Path = Path('.').absolute().resolve()
task_queue: TaskQueue = TaskQueue()
bot: 'Bot' = None
_instance: 'Config'
def __new__(cls, *args, **kwargs):
@@ -56,6 +54,8 @@ class Config:
def __init__(self, **kwargs):
reload = kwargs.pop('reload', False)
root_dir = kwargs.pop('root_dir', None)
setattr(self, 'root_dir', root_dir) if root_dir else ...
[setattr(self, key, value) for key, value in kwargs.items()]
self.load_config(reload=reload)
@@ -63,7 +63,10 @@ class Config:
if key == 'root_dir':
value = Path(value).absolute().resolve()
if key == 'records_dir':
value = self.create_records_dir(value)
self.create_records_dir(records_dir=value)
return
if key == 'path':
value = self.root_dir / Path(value) if not Path(value).exists() else value
super().__setattr__(key, value)
@staticmethod
@@ -92,17 +95,28 @@ class Config:
except Exception as _:
return
def create_records_dir(self, records_dir: str | Path):
"""Create records directory if it does not exist"""
def create_records_dir(self, *, records_dir: str | Path = 'records'):
"""Create records directory if it does not exist. Relative to the root directory of the project.
Keyword Args:
records_dir (str|Path): The name of the directory to create
"""
try:
records_dir = Path(records_dir).absolute().resolve() if isinstance(records_dir, str) else records_dir
records_dir = Path(records_dir) if isinstance(records_dir, str) else records_dir
records_dir = self.root_dir / records_dir
records_dir.mkdir(parents=True, exist_ok=True)
super().__setattr__('records_dir', records_dir)
return records_dir
except Exception as _:
logger.warning("Unable to create records directory")
except Exception as err:
logger.warning(f"{err}: Unable to create records directory")
def load_config(self, file: str = None, reload: bool = True, filename: str = None, config_dir: str = ''):
"""Load configuration settings from a file."""
def load_config(self, *, file: str = None, reload: bool = True, filename: str = None, config_dir: str = ''):
"""Load configuration settings from a file.
Keyword Args:
file (str): The path to the file to load. If not provided, the file is searched for
reload (bool): Whether to reload the config object. Default is True
filename (str): The name of the file to load. If not provided, the default filename is used
config_dir (str): The name of the directory to search for the file. Default is the root directory
"""
if not (self._initialize or reload):
return
self._initialize = False
@@ -123,4 +137,4 @@ class Config:
Returns:
dict: A dictionary of login details
"""
return {"login": self.login, "password": self.password, "server": self.server}
return {"login": self.login, "password": self.password, "server": self.server}
+17 -12
View File
@@ -15,6 +15,7 @@ Examples:
class Repr:
__enum_name__ = ""
name: str
def __repr__(self):
return f"{self.__enum_name__}_{self.name}"
@@ -211,7 +212,7 @@ class TimeFrame(Repr, IntEnum):
return times[self]
@classmethod
def get(cls, time: int):
def get(cls, time: int) -> 'TimeFrame':
times = {60: 1, 120: 2, 180: 3, 240: 4, 300: 5, 360: 6, 600: 10, 900: 15, 1200: 20, 1800: 30, 3600: 16385,
7200: 16386, 10800: 16387, 14400: 16388, 21600: 16390, 28800: 16392, 43200: 16396, 86400: 16408,
604800: 32769, 2592000: 49153}
@@ -340,7 +341,7 @@ class DealEntry(Repr, IntEnum):
class DealReason(Repr, IntEnum):
"""DEAL_REASON Enum. The reason for deal execution is contained in the DEAL_REASON property. A deal can be executed
as a result of triggering of an order placed from a mobile application or an MQL5 program, as well as as a result
as a result of triggering of an order placed from a mobile application or an MQL5 program, as well as a result
of the StopOut event, variation margin calculation, etc.
Attributes:
@@ -423,10 +424,11 @@ class SymbolCalcMode(Repr, IntEnum):
EXCH_OPTIONS (int): value is 34
EXCH_OPTIONS_MARGIN (int): value is 36
EXCH_BONDS (int): Exchange Bonds mode calculation of margin and profit for trading bonds on a stock exchange
STOCKS_MOEX (int): Exchange MOEX Stocks mode calculation of margin and profit for trading securities on MOEX
EXCH_STOCKS_MOEX (int): Exchange MOEX Stocks mode calculation of margin and profit for trading securities on
MOEX
EXCH_BONDS_MOEX (int): Exchange MOEX Bonds mode calculation of margin and profit for trading bonds on MOEX
SERV_COLLATERAL (int): Collateral mode - a symbol is used as a non-tradable asset on a trading account.
SERV_COLLATERAL (int): Collateral mode - a symbol is used as a non-tradeable asset on a trading account.
The market value of an open position is calculated based on the volume, current market price, contract size
and liquidity ratio. The value is included into Assets, which are added to Equity. Open positions of such
symbols increase the Free Margin amount and are used as additional margin (collateral) for open positions
@@ -483,7 +485,8 @@ class SymbolTradeExecution(Repr, IntEnum):
- If the broker does not accept the requested price, a "Requote" is sent — the broker returns prices,
at which this order can be executed.
MARKET (int): A broker makes a decision about the order execution price without any additional discussion with the trader.
MARKET (int): A broker makes a decision about the order execution price without any additional discussion with
the trader.
Sending the order in such a mode means advance consent to its execution at this price.
EXCHANGE (int): Trade operations are executed at the prices of the current market offers.
@@ -596,7 +599,8 @@ class SymbolOptionMode(Repr, IntEnum):
"""SYMBOL_OPTION_MODE Enum.
Attributes:
EUROPEAN (int): European option may only be exercised on a specified date (expiration, execution date, delivery date)
EUROPEAN (int): European option may only be exercised on a specified date
(expiration, execution date, delivery date)
AMERICAN (int): American option may be exercised on any trading day or before expiry. The period within which
a buyer can exercise the option is specified for it.
"""
@@ -622,7 +626,7 @@ class AccountTradeMode(Repr, IntEnum):
class TickFlag(Repr, IntFlag):
"""TICK_FLAG Enum. TICK_FLAG defines possible flags for ticks. These flags are used to describe ticks obtained by the
"""TICK_FLAG Enum. TICK_FLAG defines possible flags for ticks. The Flags are used to describe ticks obtained by the
copy_ticks_from() and copy_ticks_range() functions.
Attributes:
@@ -682,7 +686,8 @@ class TradeRetcode(Repr, IntEnum):
CLOSE_ORDER_EXIST (int): A close order already exists for a specified position. This may happen when working in
the hedging system:
· when attempting to close a position with an opposite one, while close orders for the position already exist
· when attempting to close a position with an opposite one, while close orders for the position already
exist
· when attempting to fully or partially close a position if the total volume of the already present close
orders and the newly placed one exceeds the current position volume
@@ -724,7 +729,7 @@ class TradeRetcode(Repr, IntEnum):
INVALID_STOPS = mt5.TRADE_RETCODE_INVALID_STOPS
TRADE_DISABLED = mt5.TRADE_RETCODE_TRADE_DISABLED
MARKET_CLOSED = mt5.TRADE_RETCODE_MARKET_CLOSED
NO_MONEY = mt5.TRADE_RETCODE_NO_MONEY
NO_MONEY = mt5.TRADE_RETCODE_NO_MONEY
PRICE_CHANGED = mt5.TRADE_RETCODE_PRICE_CHANGED
PRICE_OFF = mt5.TRADE_RETCODE_PRICE_OFF
INVALID_EXPIRATION = mt5.TRADE_RETCODE_INVALID_EXPIRATION
@@ -750,7 +755,7 @@ class TradeRetcode(Repr, IntEnum):
SHORT_ONLY = mt5.TRADE_RETCODE_SHORT_ONLY
CLOSE_ONLY = mt5.TRADE_RETCODE_CLOSE_ONLY
FIFO_CLOSE = mt5.TRADE_RETCODE_FIFO_CLOSE
class AccountStopOutMode(Repr, IntEnum):
"""ACCOUNT_STOPOUT_MODE Enum.
@@ -776,11 +781,11 @@ class AccountMarginMode(Repr, IntEnum):
EXCHANGE (int): Used for the exchange markets. Margin is calculated based on the discounts specified in
symbol settings. Discounts are set by the broker, but not less than the values set by the exchange.
HEDGING (int): Used for the exchange markets where individual positions are possible
RETAIL_HEDGING (int): Used for the exchange markets where individual positions are possible
(hedging, multiple positions can exist for one symbol). The margin is calculated based on the symbol
type (SYMBOL_TRADE_CALC_MODE) taking into account the hedged margin (SYMBOL_MARGIN_HEDGED).
"""
__enum_name__ = "ACCOUNT_MARGIN_MODE"
RETAIL_NETTING = mt5.ACCOUNT_MARGIN_MODE_RETAIL_NETTING
EXCHANGE = mt5.ACCOUNT_MARGIN_MODE_EXCHANGE
RETAIL_HEDGING = mt5.ACCOUNT_MARGIN_MODE_RETAIL_HEDGING
RETAIL_HEDGING = mt5.ACCOUNT_MARGIN_MODE_RETAIL_HEDGING
+6 -4
View File
@@ -20,12 +20,14 @@ class Error:
-10005: 'internal timeout',
}
conn_errors = (-10000, -10001, -10002, -10003, -10004, -10005)
def __init__(self, code: int, description: str = ''):
self.code = code
self.description = description or self.descriptions.get(code, 'Unknown Error')
def is_connection_error(self):
return self.code in self.conn_errors
def __repr__(self):
return f"""
Error Code: {self.code}
Error Description: {self.description}
"""
return f"{self.code}: {self.description}"
+8 -5
View File
@@ -5,7 +5,7 @@ from typing import Callable
import MetaTrader5
from MetaTrader5 import BookInfo, SymbolInfo, AccountInfo, Tick, TerminalInfo, TradeOrder, TradeDeal,\
from MetaTrader5 import BookInfo, SymbolInfo, AccountInfo, Tick, TerminalInfo, TradeOrder, TradeDeal, \
TradePosition, OrderSendResult, OrderCheckResult
from .constants import TimeFrame, CopyTicks, OrderType
@@ -61,6 +61,7 @@ class MetaTrader(metaclass=BaseMeta):
def __init__(self):
self.config = Config()
self.error = Error(1, 'Successful')
async def __aenter__(self) -> 'MetaTrader':
"""
@@ -110,7 +111,7 @@ class MetaTrader(metaclass=BaseMeta):
Returns:
bool: True if successful, False otherwise.
"""
args = (path,) if path else ()
args = (str(path),) if path else ()
kwargs = {key: value for key, value in (('login', login), ('password', password), ('server', server),
('timeout', timeout), ('portable', portable)) if value}
return await asyncio.to_thread(self._initialize, *args, **kwargs)
@@ -244,7 +245,8 @@ class MetaTrader(metaclass=BaseMeta):
return res
return res
async def copy_ticks_range(self, symbol: str, date_from: datetime | float, date_to: datetime | float, flags: CopyTicks):
async def copy_ticks_range(self, symbol: str, date_from: datetime | float, date_to: datetime | float,
flags: CopyTicks):
res = await asyncio.to_thread(self._copy_ticks_range, symbol, date_from, date_to, flags)
if res is None:
err = await self.last_error()
@@ -321,7 +323,8 @@ class MetaTrader(metaclass=BaseMeta):
async def history_orders_total(self, date_from: datetime | float, date_to: datetime | float) -> int:
return await asyncio.to_thread(self._history_orders_total, date_from, date_to)
async def history_orders_get(self, date_from: datetime | float = None, date_to: datetime | float = None, group: str = '',
async def history_orders_get(self, date_from: datetime | float = None, date_to: datetime | float = None,
group: str = '',
ticket: int = 0, position: int = 0) -> tuple[TradeOrder] | None:
kwargs = {key: value for key, value in (('date_from', date_from), ('date_to', date_to), ('group', group),
('ticket', ticket), ('position', position)) if value}
@@ -346,4 +349,4 @@ class MetaTrader(metaclass=BaseMeta):
self.error = Error(*err)
logger.warning(f'Error in getting deals.{self.error.description}')
return res
return res
return res
+6 -4
View File
@@ -1,8 +1,10 @@
import MetaTrader5 as mt5
from .constants import BookType, TradeAction, OrderType, OrderTime, OrderFilling, PositionReason, DealType, DealEntry,\
DealReason, SymbolChartMode, SymbolTradeMode, SymbolCalcMode, SymbolOptionMode, SymbolOrderGTCMode, SymbolOptionRight,\
SymbolTradeExecution, SymbolSwapMode, DayOfWeek, AccountTradeMode, AccountStopOutMode, AccountMarginMode, OrderReason
from .constants import BookType, TradeAction, OrderType, OrderTime, OrderFilling, PositionReason, DealType, DealEntry, \
DealReason, SymbolChartMode, SymbolTradeMode, SymbolCalcMode, SymbolOptionMode, SymbolOrderGTCMode, \
SymbolOptionRight, \
SymbolTradeExecution, SymbolSwapMode, DayOfWeek, AccountTradeMode, AccountStopOutMode, AccountMarginMode, \
OrderReason
from .base import Base
@@ -610,4 +612,4 @@ class TradeDeal(Base):
tp: float
symbol: str
comment: str
external_id: str
external_id: str
+1 -1
View File
@@ -45,4 +45,4 @@ class TaskQueue:
asyncio.create_task(self.worker())
async def start(self):
await self.queue.join()
await self.queue.join()
+17 -12
View File
@@ -1,9 +1,12 @@
import asyncio
from concurrent.futures import ThreadPoolExecutor
from typing import Sequence, Coroutine, Callable
from logging import getLogger
from .strategy import Strategy
logger = getLogger(__name__)
class Executor:
"""Executor class for running multiple strategies on multiple symbols concurrently.
@@ -15,18 +18,17 @@ class Executor:
functions (dict[Callable, dict]): A dictionary of functions to run in the executor
"""
def __init__(self, bot=None):
def __init__(self):
self.executor = ThreadPoolExecutor
self.workers: list[type(Strategy)] = []
self.coroutines: dict[Coroutine | Callable: dict] = {}
self.functions: dict[Callable: dict] = {}
self.bot: 'Bot' = bot
def add_function(self, func: Callable, kwargs: dict):
self.functions[func] = kwargs | {'bot': self.bot}
self.functions[func] = kwargs
def add_coroutine(self, coro: Coroutine, kwargs: dict):
self.coroutines[coro] = kwargs | {'bot': self.bot}
self.coroutines[coro] = kwargs
def add_workers(self, strategies: Sequence[type(Strategy)]):
"""Add multiple strategies at once
@@ -36,9 +38,9 @@ class Executor:
"""
self.workers.extend(strategies)
def remove_workers(self):
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 self.bot.symbols]
self.workers = [worker for worker in self.workers if worker.symbol in symbols]
def add_worker(self, strategy: type(Strategy)):
"""Add a strategy instance to the list of workers
@@ -65,21 +67,24 @@ class Executor:
func: The coroutine. A variadic function.
kwargs: A dictionary of keyword arguments for the function
"""
asyncio.run(func(**kwargs))
try:
asyncio.run(func(**kwargs))
except Exception as err:
logger.error(f'Error: {err}. Unable to run function')
async def execute(self, workers: int = 0):
async def execute(self, workers: int = 5):
"""Run the strategies with a threadpool executor.
Args:
workers: Number of workers to use in executor pool. Defaults to zero which uses all workers.
workers: Number of workers to use in executor pool. Defaults to 5.
Notes:
No matter the number specified, the executor will always use a minimum of 5 workers.
"""
workers = workers or sum([len(self.workers), len(self.functions), len(self.coroutines)])
workers = max(workers, 5)
workers_ = sum([len(self.workers), len(self.functions), len(self.coroutines)])
workers = max(workers, workers_)
loop = asyncio.get_running_loop()
with self.executor(max_workers=workers) as executor:
[loop.run_in_executor(executor, self.trade, worker) for worker in self.workers]
[loop.run_in_executor(executor, self.run, coro, kwargs) for coro, kwargs in self.coroutines.items()]
[loop.run_in_executor(executor, func, kwargs) for func, kwargs in self.functions.items()]
[loop.run_in_executor(executor, func, kwargs) for func, kwargs in self.functions.items()]
+27 -17
View File
@@ -71,21 +71,27 @@ class History:
self.initialized = all(res)
return self.initialized
async def get_deals(self) -> list[TradeDeal]:
async def get_deals(self, retries=3) -> list[TradeDeal]:
"""Get deals from trading history using the parameters set in the constructor.
Returns:
list[TradeDeal]: A list of trade deals
"""
if retries < 1:
logger.warning(f'Failed to get deals: {self.mt5.error}')
return []
deals = await self.mt5.history_deals_get(date_from=self.date_from, date_to=self.date_to, position=self.position,
group=self.group, ticket=self.ticket)
if deals is None:
logger.warning(f'Failed to get deals due to {self.mt5.error.description}')
deals = []
if deals is not None:
self.deals = [TradeDeal(**deal._asdict()) for deal in deals]
self.total_deals = len(self.deals)
return self.deals
if self.mt5.error.is_connection_error():
await asyncio.sleep(retries)
return await self.get_deals(retries=retries - 1)
self.deals = [TradeDeal(**deal._asdict()) for deal in deals]
self.total_deals = len(self.deals)
return self.deals
logger.warning(f'Failed to get deals: {self.mt5.error}')
return []
async def deals_total(self) -> int:
"""Get total number of deals within the specified period in the constructor.
@@ -96,22 +102,26 @@ class History:
self.total_deals = await self.mt5.history_deals_total(self.date_from, self.date_to)
return self.total_deals
async def get_orders(self) -> list[TradeOrder]:
async def get_orders(self, retries=3) -> list[TradeOrder]:
"""Get orders from trading history using the parameters set in the constructor.
Returns:
list[TradeOrder]: A list of trade orders
"""
if retries < 1:
logger.warning(f'Failed to get orders: {self.mt5.error}')
return []
orders = await self.mt5.history_orders_get(date_from=self.date_from, date_to=self.date_to, group=self.group,
position=self.position, ticket=self.ticket)
if orders is None:
logger.warning(f'Failed to get orders due to {self.mt5.error.description}')
orders = []
self.orders = [TradeOrder(**order._asdict()) for order in orders]
self.total_orders = len(self.orders)
return self.orders
if orders is not None:
self.orders = [TradeOrder(**order._asdict()) for order in orders]
self.total_orders = len(self.orders)
return self.orders
if self.mt5.error.is_connection_error():
await asyncio.sleep(retries)
return await self.get_orders(retries=retries - 1)
logger.warning(f'Failed to get orders: {self.mt5.error}')
return []
async def orders_total(self) -> int:
"""Get total number of orders within the specified period in the constructor.
@@ -120,4 +130,4 @@ class History:
int: Total number of orders
"""
self.total_orders = await self.mt5.history_orders_total(self.date_from, self.date_to)
return self.total_orders
return self.total_orders
+1 -1
View File
@@ -1,2 +1,2 @@
from .finger_trap import FingerTrap
from .tracker import Tracker
from .tracker import Tracker
+24 -18
View File
@@ -9,6 +9,7 @@ from ...candle import Candles
from ...strategy import Strategy
from ...core import TimeFrame, OrderType
from ...sessions import Sessions
from ...utils import find_bearish_fractal, find_bullish_fractal
logger = logging.getLogger(__name__)
@@ -16,7 +17,6 @@ logger = logging.getLogger(__name__)
class FingerTrap(Strategy):
ttf: TimeFrame
etf: TimeFrame
trend: int
fast_ema: int
slow_ema: int
entry_ema: int
@@ -25,8 +25,8 @@ class FingerTrap(Strategy):
tcc: int
trader: Trader
tracker: Tracker
parameters = {"trend": 3, "fast_ema": 8, "slow_ema": 20, "etf": TimeFrame.M5,
"ttf": TimeFrame.H1, "entry_ema": 5, "tcc": 50, "ecc": 600}
parameters = {"fast_ema": 8, "slow_ema": 20, "etf": TimeFrame.M5,
"ttf": TimeFrame.H1, "entry_ema": 5, "tcc": 672, "ecc": 3360}
def __init__(self, *, symbol: Symbol, params: dict | None = None, trader: Trader = None, sessions: Sessions = None,
name: str = 'FingerTrap'):
@@ -45,14 +45,13 @@ class FingerTrap(Strategy):
candles.ta.ema(length=self.fast_ema, append=True, fillna=0)
candles.rename(inplace=True, **{f"EMA_{self.fast_ema}": "fast", f"EMA_{self.slow_ema}": "slow"})
fas = candles.ta_lib.above(candles.fast, candles.slow) # fast above slow
fbs = candles.ta_lib.below(candles.fast, candles.slow) # fast below slow
caf = candles.ta_lib.above(candles.close, candles.fast) # close above fast
cbf = candles.ta_lib.below(candles.close, candles.fast) # close below fast
fas = candles.ta_lib.above(candles.fast, candles.slow)
fbs = candles.ta_lib.below(candles.fast, candles.slow)
caf = candles.ta_lib.above(candles.close, candles.fast)
cbf = candles.ta_lib.below(candles.close, candles.fast)
current = candles[-2]
if fas.iloc[-1] and caf.iloc[-1] and current.is_bullish():
self.tracker.update(trend="bullish")
elif fbs.iloc[-1] and cbf.iloc[-1] and current.is_bearish():
self.tracker.update(trend="bearish")
else:
@@ -67,20 +66,26 @@ class FingerTrap(Strategy):
if not ((current := candles[-1].time) >= self.tracker.entry_time):
self.tracker.update(new=False, order_type=None)
return
self.tracker.update(new=True, entry_time=current)
candles.ta.ema(length=self.entry_ema, append=True)
candles.rename(**{f"EMA_{self.entry_ema}": "ema"})
cae = candles.ta_lib.cross(candles.close, candles.ema)
cbe = candles.ta_lib.cross(candles.close, candles.ema, above=False)
if self.tracker.bullish and any([cae.iloc[-1], cae.iloc[-2]]):
self.tracker.update(snooze=self.ttf.time, order_type=OrderType.BUY)
elif self.tracker.bearish and any([cbe.iloc[-1], cbe.iloc[-2]]):
self.tracker.update(snooze=self.ttf.time, order_type=OrderType.SELL)
trend = self.ttf.time // self.etf.time
bull_trend = cae.iloc[-trend:]
bear_trend = cbe.iloc[-trend:]
count = 24 * 60 * 60 // self.etf.time
last_24 = candles[-count:]
if self.tracker.bullish and any(bull_trend):
sl = getattr(find_bullish_fractal(candles), 'low', last_24.low.min())
self.tracker.update(snooze=self.ttf.time, order_type=OrderType.BUY, sl=sl)
elif self.tracker.bearish and any(bear_trend):
sl = getattr(find_bearish_fractal(candles), 'high', last_24.high.max())
self.tracker.update(snooze=self.ttf.time, order_type=OrderType.SELL, sl=sl)
else:
self.tracker.update(snooze=self.etf.time, order_type=None)
except Exception as err:
logger.error(f"{err} for {self.symbol} in {self.__class__.__name__}.confirm_trend\n")
logger.error(f"{err} for {self.symbol} in {self.__class__.__name__}.confirm_trend")
self.tracker.update(snooze=self.etf.time, order_type=None)
async def watch_market(self):
@@ -91,7 +96,7 @@ class FingerTrap(Strategy):
async def trade(self):
logger.info(f"Trading {self.symbol}")
async with self.sessions as sess:
await self.sleep(self.ttf.time)
await self.sleep(self.etf.time)
while True:
await sess.check()
try:
@@ -102,8 +107,9 @@ class FingerTrap(Strategy):
if self.tracker.order_type is None:
await self.sleep(self.tracker.snooze)
continue
await self.trader.place_trade(order_type=self.tracker.order_type, parameters=self.parameters)
await self.trader.place_trade(order_type=self.tracker.order_type, parameters=self.parameters,
sl=self.tracker.sl)
await self.sleep(self.tracker.snooze)
except Exception as err:
logger.error(f"{err} For {self.symbol} in {self.__class__.__name__}.trade\n")
await self.sleep(self.ttf.time)
logger.error(f"{err} For {self.symbol} in {self.__class__.__name__}.trade")
await self.sleep(self.ttf.time)
+2
View File
@@ -16,6 +16,8 @@ class Tracker:
entry_time: float = 0
new: bool = True
order_type: OrderType = None
sl: float = 0
tp: float = 0
def update(self, **kwargs):
fields = self.__dict__
+2 -2
View File
@@ -1,6 +1,7 @@
from ...symbol import Symbol
from ...core.exceptions import VolumeError
class ForexSymbol(Symbol):
"""Subclass of Symbol for Forex Symbols. Handles the conversion of currency and the computation of stop loss,
take profit and volume.
@@ -32,7 +33,6 @@ class ForexSymbol(Symbol):
if adjust:
points = self.compute_points(amount=amount, volume=volume)
return volume, points
if use_limits:
vol = chk_vol[1]
if adjust:
@@ -80,4 +80,4 @@ class ForexSymbol(Symbol):
return volume
if use_limits:
return self.check_volume(volume)[1]
raise VolumeError(f"Incorrect Volume. Computed Volume outside the range of permitted volumes")
raise VolumeError(f"Incorrect Volume. Computed Volume outside the range of permitted volumes")
+1 -1
View File
@@ -1 +1 @@
from .simple_trader import SimpleTrader
from .simple_trader import SimpleTrader
+13 -15
View File
@@ -20,30 +20,28 @@ class SimpleTrader(Trader):
ram = ram or RAM(risk_to_reward=2)
super().__init__(symbol=symbol, ram=ram)
async def create_order(self, *, order_type: OrderType):
"""Complete the order object with the required values. Creates a simple order.
Args:
order_type (OrderType): Type of order
"""
losing = await self.ram.check_losing_positions()
if losing:
raise RuntimeError(f"More than {self.ram.loss_limit} losing positions")
async def create_order(self, *, order_type: OrderType, sl: float):
amount = await self.ram.get_amount()
points = self.symbol.compute_points(amount=amount, volume=self.symbol.volume_min)
self.order.volume = self.symbol.volume_min
await self.symbol.info()
tick = await self.symbol.info_tick()
min_points = self.symbol.trade_stops_level + (self.symbol.spread * 1.5)
points = (tick.ask - sl) / self.symbol.point if order_type == OrderType.BUY else\
(abs(tick.bid - sl) / self.symbol.point)
points = max(points, min_points)
self.order.type = order_type
self.order.comment = self.parameters.get('name', 'SimpleTrader')
volume, points = await self.symbol.compute_volume_points(amount=amount, points=points)
self.order.volume = volume
self.order.comment = self.parameters.get('name', self.__class__.__name__)
tick = await self.symbol.info_tick()
self.set_trade_stop_levels(points=points, tick=tick)
async def place_trade(self, order_type: OrderType, parameters: dict = None):
async def place_trade(self, order_type: OrderType, sl: float, parameters: dict = None):
"""Places a trade based on the order_type."""
try:
self.parameters |= parameters or {}
await self.create_order(order_type=order_type)
await self.create_order(order_type=order_type, sl=sl)
if not await self.check_order():
return
await self.send_order()
except Exception as err:
logger.error(f"{err} in {self.__class__.__name__}.place_trade for {self.symbol.name}")
logger.error(f"{err} in {self.__class__.__name__}.place_trade for {self.symbol.name}")
+22 -13
View File
@@ -1,10 +1,9 @@
"""Order Class"""
import asyncio
from logging import getLogger
from .core.models import TradeRequest, OrderSendResult, OrderCheckResult, TradeOrder, SymbolInfo
from .core.models import TradeRequest, OrderSendResult, OrderCheckResult, TradeOrder
from .core.constants import TradeAction, OrderTime, OrderFilling
from .core.exceptions import SymbolError, OrderError
from .symbol import Symbol
from .core.exceptions import OrderError
logger = getLogger(__name__)
@@ -39,20 +38,30 @@ class Order(TradeRequest):
"""
return await self.mt5.orders_total()
async def orders(self) -> tuple[TradeOrder]:
async def get_orders(self, *, ticket: int = 0, symbol: str = '', group: str = '', retries=3)\
-> tuple[TradeOrder, ...]:
"""Get the list of active orders for the current symbol.
Keyword Args:
ticket (int): Order ticket number
symbol (str): Symbol name
group (str): Group name
Returns:
tuple[TradeOrder]: A Tuple of active trade orders as TradeOrder objects
"""
orders = await self.mt5.orders_get(symbol=self.symbol)
if orders is None:
raise OrderError(f'Failed to get orders for {self.symbol} due to {self.mt5.error.description}')
orders = (TradeOrder(**order._asdict()) for order in orders)
return tuple(orders)
if retries < 1:
raise OrderError(f'Failed to get orders for {self.symbol}: {self.mt5.error}')
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)
if self.mt5.error.is_connection_error():
await asyncio.sleep(retries)
return await self.get_orders(ticket=ticket, symbol=symbol, group=group, retries=retries-1)
raise OrderError(f'Failed to get orders for {self.symbol}: {self.mt5.error}')
async def check(self) -> OrderCheckResult:
"""Check funds sufficiency for performing a required trading operation and the possibility to execute it at
"""Check funds sufficiency for performing a required trading operation and the possibility of executing it.
Returns:
OrderCheckResult: An OrderCheckResult object
@@ -105,4 +114,4 @@ class Order(TradeRequest):
res = await self.mt5.order_calc_profit(self.type, self.symbol, self.volume, self.price, self.tp)
if res is None:
raise OrderError(f'Failed to calculate profit for {self.symbol} due to {self.mt5.error.description}')
return res
return res
+15 -8
View File
@@ -14,7 +14,7 @@ class 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 name.
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.
"""
@@ -43,7 +43,7 @@ class Positions:
"""
return await self.mt5.positions_total()
async def positions_get(self, symbol: str = '', group: str = '', ticket: int = 0) -> list[TradePosition]:
async def positions_get(self, symbol: str = '', group: str = '', ticket: int = 0, retries=3) -> list[TradePosition]:
"""Get open positions with the ability to filter by symbol or ticket.
Keyword Args:
@@ -55,12 +55,18 @@ class Positions:
Returns:
list[TradePosition]: A list of open trade positions
"""
if retries < 1:
logger.warning(f'Failed to get positions for {symbol or self.symbol}. {self.mt5.error}')
return []
positions = await self.mt5.positions_get(group=group or self.group, symbol=symbol or self.symbol,
ticket=ticket or self.ticket)
if positions is None:
logger.warning(f'Failed to get positions for {symbol or self.symbol} due to {self.mt5.error.description}')
positions = []
return [TradePosition(**pos._asdict()) for pos in positions]
if positions is not None:
return [TradePosition(**pos._asdict()) for pos in positions]
if self.mt5.error.is_connection_error():
await asyncio.sleep(retries)
return await self.positions_get(symbol, group, ticket, retries - 1)
logger.warning(f'Failed to get positions for {symbol or self.symbol}. {self.mt5.error}')
return []
async def close(self, *, ticket: int, symbol: str, price: float, volume: float, order_type: OrderType):
"""Close an open position for the trading account."""
@@ -71,7 +77,8 @@ class Positions:
async def close_by(self, pos: TradePosition):
"""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)
order = Order(position=pos.ticket, symbol=pos.symbol, volume=pos.volume, type=pos.type.opposite,
price=pos.price_current)
return await order.send()
async def close_all(self, symbol: str = '', group: str = '') -> int:
@@ -89,4 +96,4 @@ class Positions:
positions = [pos for pos in await self.positions_get(symbol=symbol, group=group)]
orders = [self.close_by(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])
return len([res for res in results if (res and res.retcode) == 10009])
+5 -3
View File
@@ -11,7 +11,7 @@ class RAM:
pips: float
min_amount: float
max_amount: float
balance_level: float = 50
balance_level: float = 10
loss_limit: int = 3
def __init__(self, *, risk_to_reward: float = 1, risk: float = 0.01, **kwargs):
@@ -37,12 +37,14 @@ class RAM:
return self.account.balance * self.risk
async def check_losing_positions(self) -> bool:
"""Check if the number of losing positions is greater than or equal the loss limit."""
positions = await Positions().positions_get()
positions.sort(key=lambda pos: pos.time_msc)
loosing = [trade for trade in positions if trade.profit <= 0]
return len(loosing) > self.loss_limit
return len(loosing) >= self.loss_limit
async def check_balance_level(self) -> bool:
"""Check if the balance level is greater than or equal to the balance level."""
await self.account.refresh()
balance_level = (self.account.margin / self.account.balance) * 100
return balance_level >= self.balance_level
return balance_level >= self.balance_level
+5 -5
View File
@@ -15,18 +15,18 @@ class Records:
Attributes:
config: Config object
records_dir(Path): Path to directory containing record of placed trades, If not given takes the default
from the config
records_dir(Path): Absolute path to directory containing record of placed trades, If not given takes the default
from the config
"""
config: Config
mt5: MetaTrader
def __init__(self, records_dir: Path = ''):
def __init__(self, records_dir: Path | str = ''):
"""Initialize the Records class. The main method of this class is update_records which you should call to update
all the records specified in the records_dir.
Keyword Args:
records_dir (Path): Path to directory containing record of placed trades.
records_dir (Path): Absolute path to directory containing record of placed trades.
"""
self.config = Config()
self.mt5 = MetaTrader()
@@ -111,4 +111,4 @@ class Records:
async def update_record(self, file: Path | str):
"""Update a single trade record file."""
await self.read_update(file)
await self.read_update(file)
+6 -5
View File
@@ -1,4 +1,3 @@
import asyncio
import csv
from logging import getLogger
from threading import RLock
@@ -31,10 +30,11 @@ class Result:
self.parameters = parameters or {}
self.result = result
self.name = name or parameters.get('name', 'Trades')
self.config.create_records_dir()
def get_data(self) -> dict:
return (self.parameters | self.result.get_dict(exclude={'retcode', 'comment', 'retcode_external', 'request_id', 'request'})
| {'actual_profit': 0, 'closed': False, 'win': False})
res = self.result.get_dict(exclude={'retcode', 'comment', 'retcode_external', 'request_id', 'request'})
return self.parameters | res | {'actual_profit': 0, 'closed': False, 'win': False}
async def to_csv(self):
"""Record trade results and associated parameters as a csv file
@@ -45,9 +45,10 @@ class Result:
exists = file.exists()
with RLock():
with open(file, 'a', newline='') as fh:
writer = csv.DictWriter(fh, fieldnames=sorted(list(data.keys())), extrasaction='ignore', restval=None)
f_names = sorted(list(data.keys()))
writer = csv.DictWriter(fh, fieldnames=f_names, extrasaction='ignore', restval=None)
if not exists:
writer.writeheader()
writer.writerow(data)
except Exception as err:
logger.error(f'Error: {err}. Unable to save trade results')
logger.error(f'Error: {err}. Unable to save trade results')
+3 -11
View File
@@ -1,4 +1,3 @@
"""Sessions allow you to run code at specific times of the day."""
import asyncio
from datetime import time, timedelta, datetime
from asyncio import sleep, iscoroutinefunction
@@ -10,7 +9,7 @@ from .positions import Positions
logger = getLogger(__name__)
def delta(obj: time):
def delta(obj: time) -> timedelta:
"""Get the timedelta of a datetime.time object.
Args:
@@ -29,14 +28,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 end.
Methods:
begin: Call the action specified in on_start or custom_start.
close: Call the action specified in on_end or custom_end.
action: Used by begin and close to call the action specified.
delta: Get the timedelta of a datetime.time object.
until: Get the seconds until the session starts from the current time.
name (str): A name for the session. Default is a combination of start and en
"""
def __init__(self, *, start: int | time, end: int | time,
on_start: Literal['close_all', 'close_win', 'close_loss', 'custom_start'] = None,
@@ -212,4 +204,4 @@ class Sessions:
logger.info(f'sleeping for {secs} seconds until next {current_session} session')
await sleep(secs)
self.current_session = current_session
await self.current_session.begin()
await self.current_session.begin()
+1 -1
View File
@@ -73,7 +73,7 @@ class Strategy(ABC):
"""
mod = time() % secs
secs = secs - mod if mod != 0 else mod
await asyncio.sleep(secs + 0.1)
await asyncio.sleep(secs + 0.2)
@abstractmethod
async def trade(self):
+85 -36
View File
@@ -1,8 +1,7 @@
"""Symbol class for handling a financial instrument."""
import asyncio
from datetime import datetime
from logging import getLogger
from math import log10, ceil
import decimal
from .core.constants import TimeFrame, CopyTicks
from .core.models import SymbolInfo, BookInfo
@@ -17,7 +16,7 @@ logger = getLogger(__name__)
class Symbol(SymbolInfo):
"""Main class for handling a financial instrument. A subclass of SymbolInfo and Base it has attributes and methods
"""Main class for handling a financial instrument. A subclass of SymbolInfo it has attributes and methods
for working with a financial instrument.
Attributes:
@@ -49,7 +48,7 @@ class Symbol(SymbolInfo):
"""
return self.point * 10
async def info_tick(self, *, name: str = "") -> Tick:
async def info_tick(self, *, name: str = "", retries=3) -> Tick:
"""Get the current price tick of a financial instrument.
Args:
@@ -61,12 +60,17 @@ class Symbol(SymbolInfo):
Raises:
ValueError: If request was unsuccessful and None was returned
"""
if retries < 1:
raise ValueError(f'Could not get tick for {name or self.name}. {self.mt5.error}')
tick = await self.mt5.symbol_info_tick(name or self.name)
if tick is None:
raise ValueError(f'Could not get tick for {name or self.name}')
tick = Tick(**tick._asdict())
setattr(self, 'tick', tick) if not name else ...
return tick
if tick is not None:
tick = Tick(**tick._asdict())
setattr(self, 'tick', tick) if not name else ...
return tick
if self.mt5.error.is_connection_error():
await asyncio.sleep(retries)
return await self.info_tick(name=name, retries=retries - 1)
raise ValueError(f'Could not get tick for {name or self.name}. {self.mt5.error}')
async def symbol_select(self, *, enable: bool = True) -> bool:
"""Select a symbol in the MarketWatch window or remove a symbol from the window.
@@ -82,7 +86,7 @@ class Symbol(SymbolInfo):
self.select = await self.mt5.symbol_select(self.name, enable)
return self.select
async def info(self) -> SymbolInfo:
async def info(self, retries=3) -> SymbolInfo:
"""Get data on the specified financial instrument and update the symbol object properties
Returns:
@@ -91,13 +95,18 @@ class Symbol(SymbolInfo):
Raises:
ValueError: If request was unsuccessful and None was returned
"""
if retries < 1:
raise ValueError(f'Could not get info for {self.name}. {self.mt5.error}')
info = await self.mt5.symbol_info(self.name)
if info:
info = info._asdict()
info['swap_rollover3days'] = info.get('swap_rollover3days', 0) % 7
self.set_attributes(**info)
return SymbolInfo(**info)
raise ValueError(f'Could not get info for {self.name}')
if self.mt5.error.is_connection_error():
await asyncio.sleep(retries)
return await self.info(retries=retries - 1)
raise ValueError(f'Could not get info for {self.name}. {self.mt5.error}')
async def init(self) -> bool:
"""Initialized the symbol by pulling properties from the terminal
@@ -127,7 +136,7 @@ class Symbol(SymbolInfo):
"""
return await self.mt5.market_book_add(self.name)
async def book_get(self) -> tuple[BookInfo]:
async def book_get(self, retries=3) -> tuple[BookInfo, ...]:
"""Returns a tuple of BookInfo featuring Market Depth entries for the specified symbol.
Returns:
@@ -136,11 +145,16 @@ class Symbol(SymbolInfo):
Raises:
ValueError: If request was unsuccessful and None was returned
"""
if retries < 1:
raise ValueError(f'Could not get book info for {self.name}. {self.mt5.error}')
infos = await self.mt5.market_book_get(self.name)
if infos is None:
raise ValueError(f'Could not get book info for {self.name}')
book_infos = (BookInfo(**info._asdict()) for info in infos)
return tuple(book_infos)
if infos is not None:
book_infos = (BookInfo(**info._asdict()) for info in infos)
return tuple(book_infos)
if self.mt5.error.is_connection_error():
await asyncio.sleep(retries)
return await self.book_get(retries=retries - 1)
raise ValueError(f'Could not get book info for {self.name}. {self.mt5.error}')
async def book_release(self) -> bool:
"""Cancels subscription of the MetaTrader 5 terminal to the Market Depth change events for a specified symbol.
@@ -173,7 +187,7 @@ class Symbol(SymbolInfo):
Args:
volume (float): Volume to round off
down (bool): If True, round down. If False, round up. Optional unnamed parameter. Defaults to True.
round_down (bool): If True, round down. If False, round up. Optional unnamed parameter. Defaults to True.
Returns:
float: Rounded off volume
@@ -191,7 +205,7 @@ class Symbol(SymbolInfo):
that implements the computation of volume.
Keyword Args:
use_limits (bool): round up or round down the computed volume to the nearest volume limit i.e volume_min
use_limits (bool): round up or round down the computed volume to the nearest volume limit i.e. volume_min
or volume_max
Returns:
@@ -235,7 +249,8 @@ class Symbol(SymbolInfo):
else:
logger.warning(f'Currency conversion failed: Unable to convert {amount} in {quote} to {base}')
async def copy_rates_from(self, *, timeframe: TimeFrame, date_from: datetime | int, count: int = 500) -> Candles:
async def copy_rates_from(self, *, timeframe: TimeFrame,
date_from: datetime | int, count: int = 500, retries=3) -> Candles:
"""
Get bars from the MetaTrader 5 terminal starting from the specified date.
@@ -253,12 +268,19 @@ class Symbol(SymbolInfo):
Raises:
ValueError: If request was unsuccessful and None was returned
"""
if retries < 1:
raise ValueError(f'Could not get rates for {self.name}. {self.mt5.error}')
rates = await self.mt5.copy_rates_from(self.name, timeframe, date_from, count)
if rates is not None:
return Candles(data=rates)
raise ValueError(f'Could not get rates for {self.name}')
if self.mt5.error.is_connection_error():
await asyncio.sleep(retries)
return await self.copy_rates_from(timeframe=timeframe, date_from=date_from,
count=count, retries=retries - 1)
raise ValueError(f'Could not get rates for {self.name}. {self.mt5.error}')
async def copy_rates_from_pos(self, *, timeframe: TimeFrame, count: int = 500, start_position: int = 0) -> Candles:
async def copy_rates_from_pos(self, *, timeframe: TimeFrame, count: int = 500,
start_position: int = 0, retries=3) -> Candles:
"""Get bars from the MetaTrader 5 terminal starting from the specified index.
Args:
@@ -275,23 +297,31 @@ class Symbol(SymbolInfo):
Raises:
ValueError: If request was unsuccessful and None was returned
"""
if retries < 1:
raise ValueError(f'Could not get rates for {self.name}. {self.mt5.error}')
rates = await self.mt5.copy_rates_from_pos(self.name, timeframe, start_position, count)
if rates is not None:
return Candles(data=rates)
raise ValueError(f'Could not get rates for {self.name}')
if self.mt5.error.is_connection_error():
await asyncio.sleep(retries)
return await self.copy_rates_from_pos(timeframe=timeframe, count=count,
start_position=start_position, retries=retries - 1)
raise ValueError(f'Could not get rates for {self.name}. {self.mt5.error}')
async def copy_rates_range(self, *, timeframe: TimeFrame, date_from: datetime | int,
date_to: datetime | int) -> Candles:
date_to: datetime | int, retries=3) -> Candles:
"""Get bars in the specified date range from the MetaTrader 5 terminal.
Args:
timeframe (TimeFrame): Timeframe for the bars using the TimeFrame enumeration. Required unnamed parameter.
date_from (datetime | int): Date the bars are requested from. Set by the 'datetime' object or as a number of seconds
elapsed since 1970.01.01. Bars with the open time >= date_from are returned. Required unnamed parameter.
date_from (datetime | int): Date the bars are requested from. Set by the 'datetime' object or as a number
of seconds elapsed since 1970.01.01. Bars with the open time >= date_from are returned. Required unnamed
parameter.
date_to (datetime | int): Date, up to which the bars are requested. Set by the 'datetime' object or as a number of
seconds elapsed since 1970.01.01. Bars with the open time <= date_to are returned. Required unnamed parameter.
date_to (datetime | int): Date, up to which the bars are requested. Set by the 'datetime' object or as a
number of seconds elapsed since 1970.01.01. Bars with the open time <= date_to are returned.
Required unnamed parameter.
Returns:
Candles: Returns a Candles object as a collection of rates ordered chronologically.
@@ -299,14 +329,21 @@ class Symbol(SymbolInfo):
Raises:
ValueError: If request was unsuccessful and None was returned
"""
if retries < 1:
raise ValueError(f'Could not get rates for {self.name}. {self.mt5.error}')
rates = await self.mt5.copy_rates_range(symbol=self.name, timeframe=timeframe, date_from=date_from,
date_to=date_to)
if rates is not None:
return Candles(data=rates)
raise ValueError(f'Could not get rates for {self.name}')
if self.mt5.error.is_connection_error():
await asyncio.sleep(retries)
return await self.copy_rates_range(timeframe=timeframe, date_from=date_from,
date_to=date_to, retries=retries - 1)
raise ValueError(f'Could not get rates for {self.name}. {self.mt5.error}')
async def copy_ticks_from(self, *, date_from: datetime | int, count: int = 100,
flags: CopyTicks = CopyTicks.ALL) -> Ticks:
flags: CopyTicks = CopyTicks.ALL, retries=3) -> Ticks:
"""
Get ticks from the MetaTrader 5 terminal starting from the specified date.
@@ -323,21 +360,28 @@ class Symbol(SymbolInfo):
Raises:
ValueError: If request was unsuccessful and None was returned
"""
if retries < 1:
raise ValueError(f'Could not get ticks for {self.name}. {self.mt5.error}')
ticks = await self.mt5.copy_ticks_from(self.name, date_from, count, flags)
if ticks is not None:
return Ticks(data=ticks)
raise ValueError(f'Could not get ticks for {self.name}')
if self.mt5.error.is_connection_error():
await asyncio.sleep(retries)
return await self.copy_ticks_from(date_from=date_from, count=count, flags=flags, retries=retries - 1)
raise ValueError(f'Could not get ticks for {self.name}. {self.mt5.error}')
async def copy_ticks_range(self, *, date_from: datetime | int, date_to: datetime | int,
flags: CopyTicks = CopyTicks.ALL) -> Ticks:
flags: CopyTicks = CopyTicks.ALL, retries=3) -> Ticks:
"""Get ticks for the specified date range from the MetaTrader 5 terminal.
Args:
date_from: Date the bars are requested from. Set by the 'datetime' object or as a number of seconds elapsed since 1970.01.01. Bars with
the open time >= date_from are returned. Required unnamed parameter.
date_from: Date the bars are requested from. Set by the 'datetime' object or as a number of seconds elapsed
since 1970.01.01. Bars with the open time >= date_from are returned. Required unnamed parameter.
date_to: Date, up to which the bars are requested. Set by the 'datetime' object or as a number of seconds elapsed since 1970.01.01. Bars
with the open time <= date_to are returned. Required unnamed parameter.
date_to: Date, up to which the bars are requested. Set by the 'datetime' object or as a number of
seconds elapsed since 1970.01.01. Bars with the open time <= date_to are returned.
Required unnamed parameter.
flags (CopyTicks):
@@ -347,7 +391,12 @@ class Symbol(SymbolInfo):
Raises:
ValueError: If request was unsuccessful and None was returned.
"""
if retries < 1:
raise ValueError(f'Could not get ticks for {self.name}. {self.mt5.error}')
ticks = await self.mt5.copy_ticks_range(self.name, date_from, date_to, flags)
if ticks is not None:
return Ticks(data=ticks)
raise ValueError(f'Could not get ticks for {self.name}')
if self.mt5.error.is_connection_error():
await asyncio.sleep(retries)
return await self.copy_ticks_range(date_from=date_from, date_to=date_to, flags=flags, retries=retries - 1)
raise ValueError(f'Could not get ticks for {self.name}. {self.mt5.error}')
+2 -2
View File
@@ -21,7 +21,7 @@ class Terminal(TerminalInfo):
"""Establish a connection with the MetaTrader 5 terminal. There are three call options. Call without parameters.
The terminal for connection is found automatically. Call specifying the path to the MetaTrader 5 terminal we
want to connect to. word path as a keyword argument Call specifying the trading account path and parameters
i.e login, password, server, as keyword arguments, path can be omitted.
i.e. login, password, server, as keyword arguments, path can be omitted.
Returns:
bool: True if successful else False
@@ -67,4 +67,4 @@ class Terminal(TerminalInfo):
Returns:
int: Total number of available symbols
"""
return await self.mt5.symbols_total()
return await self.mt5.symbols_total()
+10 -19
View File
@@ -7,7 +7,6 @@ import pandas_ta as ta
from .core.constants import TickFlag
Self = TypeVar('Self', bound='Ticks')
@@ -36,14 +35,17 @@ class Tick:
Index: int
def __init__(self, **kwargs):
self.time = kwargs.pop('time', 0)
"""Initialize the Tick class. Set attributes from keyword arguments. bid, ask, last, time and volume must be
present"""
if not all(key in kwargs for key in ['bid', 'ask', 'last', 'volume', 'time']):
raise ValueError("bid, ask, last and volume, time must be present in the keyword arguments")
self.Index = kwargs.pop('Index', 0)
self.set_attributes(**kwargs)
def __repr__(self):
return ("%(class)s(Index=%(Index)s, time=%(time)s, bid=%(bid)s, ask=%(ask)s, last=%(last)s, volume=%(volume)s,"
" mid=%(mid)s)") % {"class": self.__class__.__name__, "time": self.time, "bid": self.bid,
"ask": self.ask, "last": self.last, "volume": self.volume, 'Index': self.Index}
return ("%(class)s(Index=%(Index)s, time=%(time)s, bid=%(bid)s, ask=%(ask)s, last=%(last)s, volume=%(volume)s)"
% {"class": self.__class__.__name__, "time": self.time, "bid": self.bid,
"ask": self.ask, "last": self.last, "volume": self.volume, 'Index': self.Index})
def set_attributes(self, **kwargs):
"""Set attributes from keyword arguments"""
@@ -55,18 +57,7 @@ _Ticks = TypeVar('_Ticks', bound='Ticks')
class Ticks:
"""Container data class for price ticks. Arrange in chronological order.
Supports iteration, slicing and assignment
Args:
data (DataFrame | tuple[tuple]): Dataframe of price ticks or a tuple of tuples
Keyword Args:
flip (bool): If flip is True reverse data chronological order.
Attributes:
data: Dataframe Object holding the ticks
"""
"""Container class for price ticks. Arrange in chronological order. Supports iteration, slicing and assignment"""
time: Series
bid: Series
ask: Series
@@ -154,7 +145,7 @@ class Ticks:
"""DataFrame of price ticks arranged in chronological order."""
return self._data
def rename(self, inplace=True, **kwargs) -> _Ticks | None :
def rename(self, inplace=True, **kwargs) -> _Ticks | None:
"""Rename columns of the candle class.
Keyword Args:
@@ -166,4 +157,4 @@ class Ticks:
None: If inplace is True
"""
res = self._data.rename(columns=kwargs, inplace=inplace)
return res if inplace else self.__class__(data=res)
return res if inplace else self.__class__(data=res)
+7 -7
View File
@@ -90,8 +90,8 @@ class Trader(ABC):
"""
check = await self.order.check()
if check.retcode != 0:
logger.warning(f"""Invalid order for {self.symbol}
\r\r{dict_to_string(check.request._asdict() | check.get_dict(include={'comment', 'retcode'}))}""")
req = check.request._asdict() | check.get_dict(include={'comment', 'retcode'})
logger.warning(f"Invalid order for {self.symbol}: {dict_to_string(req)}")
return False
return True
@@ -99,11 +99,11 @@ class Trader(ABC):
"""Send the order to the broker."""
result = await self.order.send()
if result.retcode != 10009:
logger.warning(f"""Unable to place order for {self.symbol}
\r\r{dict_to_string(result.request._asdict() | result.get_dict(include={'comment', 'retcode'}))}\n""")
req = result.request._asdict() | result.get_dict(include={'comment', 'retcode'})
logger.warning(f"Unable to place order for {self.symbol}: {dict_to_string(req)}")
return result
logger.info(f"""Placed Trade for {self.symbol}
\r\r{dict_to_string(result.get_dict(exclude={'request', 'retcode_external', 'retcode', 'request_id'}), multi=True)}\n""")
res = result.get_dict(exclude={'request', 'retcode_external', 'retcode', 'request_id'})
logger.info(f"Placed Trade for {self.symbol}: {dict_to_string(res)}")
await self.record_trade(result, parameters=self.parameters.copy())
return result
@@ -129,4 +129,4 @@ class Trader(ABC):
@abstractmethod
async def place_trade(self, *args, **kwargs):
"""Places a trade based on the order_type."""
"""Places a trade based on the order_type."""
+16 -3
View File
@@ -1,14 +1,15 @@
"""Utility functions for aiomql."""
import decimal
from .candle import Candles, Candle
def dict_to_string(data: dict, multi=True) -> str:
def dict_to_string(data: dict, multi=False) -> str:
"""Convert a dict to a string. Useful for logging.
Args:
data (dict): The dict to convert.
multi (bool, optional): If True, each key-value pair will be on a new line. Defaults to True.
multi (bool, optional): If True, each key-value pair will be on a new line. Defaults to False.
Returns:
str: The string representation of the dict.
@@ -21,4 +22,16 @@ def round_off(value: float, step: float, round_down: bool = True) -> float:
"""Round off a number to the nearest step."""
with decimal.localcontext() as ctx:
ctx.rounding = decimal.ROUND_DOWN if round_down else decimal.ROUND_UP
return float(decimal.Decimal(str(value)).quantize(decimal.Decimal(str(step))))
return float(decimal.Decimal(str(value)).quantize(decimal.Decimal(str(step))))
def find_bearish_fractal(candles: Candles) -> Candle | None:
for i in range(len(candles) - 3, 1, -1):
if candles[i].high > max(candles[i - 1].high, candles[i + 1].high, candles[i - 2].high, candles[i + 2].high):
return candles[i]
def find_bullish_fractal(candles: Candles) -> Candle | None:
for i in range(len(candles) - 3, 1, -1):
if candles[i].low < min(candles[i - 1].low, candles[i + 1].low, candles[i - 2].low, candles[i + 2].low):
return candles[i]