mirror of
https://github.com/Ichinga-Samuel/aiomql.git
synced 2026-08-16 13:28:08 +00:00
add docs
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
from .account import Account
|
||||
from .ram import RAM
|
||||
from .symbol import Symbol
|
||||
from .strategy import Strategy
|
||||
from .bot_builder import Bot
|
||||
from .result import Result
|
||||
from .records import Records
|
||||
from .candle import Candle, Candles
|
||||
from .positions import Positions
|
||||
from .executor import Executor
|
||||
from .order import Order
|
||||
from .ticks import Tick, Ticks
|
||||
from .history import History
|
||||
from .trader import Trader
|
||||
from .terminal import Terminal
|
||||
from .sessions import Session, Sessions
|
||||
|
||||
from .core.config import Config
|
||||
from .core.constants import *
|
||||
from .core.meta_trader import MetaTrader
|
||||
from .core.models import *
|
||||
from .core.exceptions import *
|
||||
from .lib import *
|
||||
@@ -0,0 +1,109 @@
|
||||
from logging import getLogger
|
||||
from typing import Type
|
||||
|
||||
from .core.models import AccountInfo, SymbolInfo
|
||||
from .core.exceptions import LoginError
|
||||
|
||||
logger = getLogger(__name__)
|
||||
|
||||
|
||||
class Account(AccountInfo):
|
||||
"""A class for managing a trading account. A singleton class.
|
||||
A subclass of AccountInfo. All AccountInfo attributes are available in this class.
|
||||
|
||||
Attributes:
|
||||
connected (bool): Status of connection to MetaTrader 5 Terminal
|
||||
symbols (set[SymbolInfo]): A set of available symbols for the financial market.
|
||||
|
||||
Notes:
|
||||
Other Account properties are defined in the AccountInfo class.
|
||||
"""
|
||||
connected: bool
|
||||
symbols = set()
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
if not hasattr(cls, '_instance'):
|
||||
cls._instance = super().__new__(cls)
|
||||
return cls._instance
|
||||
|
||||
async def refresh(self):
|
||||
"""
|
||||
Refreshes the account instance with the latest account details from the MetaTrader 5 terminal
|
||||
"""
|
||||
account_info = await self.mt5.account_info()
|
||||
acc = account_info._asdict()
|
||||
self.set_attributes(**acc)
|
||||
|
||||
@property
|
||||
def account_info(self) -> dict:
|
||||
"""Get account login, server and password details. If the login attribute of the account instance returns
|
||||
a falsy value, the config instance is used to get the account details.
|
||||
|
||||
Returns:
|
||||
dict: A dict of login, server and password details
|
||||
|
||||
Note:
|
||||
This method will only look for config details in the config instance if the login attribute of the
|
||||
account Instance returns a falsy value
|
||||
"""
|
||||
acc_info = self.get_dict(include={'login', 'server', 'password'})
|
||||
return acc_info if acc_info['login'] else self.config.account_info()
|
||||
|
||||
async def __aenter__(self) -> 'Account':
|
||||
"""Connect to a trading account and return the account instance.
|
||||
Async context manager for the Account class.
|
||||
|
||||
Returns:
|
||||
Account: An instance of the Account class
|
||||
|
||||
Raises:
|
||||
LoginError: If login fails
|
||||
"""
|
||||
res = await self.sign_in()
|
||||
if not res:
|
||||
raise LoginError('Login failed')
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
||||
await self.mt5.shutdown()
|
||||
self.connected = False
|
||||
|
||||
async def sign_in(self) -> bool:
|
||||
"""Connect to a trading account.
|
||||
|
||||
Returns:
|
||||
bool: True if login was successful else False
|
||||
"""
|
||||
await self.mt5.initialize(**self.account_info)
|
||||
self.connected = await self.mt5.login(**self.account_info)
|
||||
if self.connected:
|
||||
await self.refresh()
|
||||
self.symbols = await self.symbols_get()
|
||||
return self.connected
|
||||
await self.mt5.shutdown()
|
||||
return False
|
||||
|
||||
def has_symbol(self, symbol: str | Type[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:
|
||||
symbol = SymbolInfo(name=str(symbol)) if not isinstance(symbol, SymbolInfo) else symbol
|
||||
return symbol 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}
|
||||
@@ -0,0 +1,116 @@
|
||||
import asyncio
|
||||
from typing import Type, Iterable, TypeVar
|
||||
import logging
|
||||
|
||||
from .executor import Executor
|
||||
from .account import Account
|
||||
from .symbol import Symbol as _Symbol
|
||||
from .strategy import Strategy as _Strategy
|
||||
# from
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
Strategy = TypeVar('Strategy', bound=_Strategy)
|
||||
Symbol = TypeVar('Symbol', bound=_Symbol)
|
||||
|
||||
|
||||
class Bot:
|
||||
"""The bot class. Create a bot instance to run your strategies.
|
||||
|
||||
Attributes:
|
||||
account (Account): Account Object.
|
||||
executor: The default thread executor.
|
||||
symbols (set[Symbols]): A set of symbols for the trading session
|
||||
"""
|
||||
account: Account = Account()
|
||||
|
||||
def __init__(self):
|
||||
self.symbols = set()
|
||||
self.executor = Executor(bot=self)
|
||||
|
||||
async def initialize(self):
|
||||
"""Prepares the bot by signing in to the trading account and initializing the symbols for the trading session.
|
||||
|
||||
Raises:
|
||||
SystemExit if sign in was not successful
|
||||
"""
|
||||
init = await self.account.sign_in()
|
||||
logger.info("Login Successful")
|
||||
if not init:
|
||||
logger.warning('Unable to sign in to MetaTrder 5 Terminal')
|
||||
raise SystemExit
|
||||
await self.init_symbols()
|
||||
self.executor.remove_workers()
|
||||
|
||||
def add_func(self, func, kwargs):
|
||||
self.executor.add_func(func, kwargs)
|
||||
|
||||
def add_coro(self, coro, **kwargs):
|
||||
self.executor.add_coro(coro, kwargs)
|
||||
|
||||
def execute(self):
|
||||
"""Execute the bot.
|
||||
"""
|
||||
asyncio.run(self.start())
|
||||
|
||||
async def start(self):
|
||||
"""Starts the bot by calling the initialize method and running the strategies in the executor.
|
||||
"""
|
||||
await self.initialize()
|
||||
await self.executor.execute()
|
||||
|
||||
def add_strategy(self, strategy: Strategy):
|
||||
"""Add a strategy to the executor. An added strategy will only run if it's symbol was successfully initialized.
|
||||
|
||||
Args:
|
||||
strategy (Strategy): A Strategy instance to run on bot
|
||||
|
||||
Notes:
|
||||
Make sure the symbol has been added to the market
|
||||
"""
|
||||
self.symbols.add(strategy.symbol)
|
||||
self.executor.add_worker(strategy)
|
||||
|
||||
def add_strategies(self, strategies: Iterable[Strategy]):
|
||||
"""Add multiple strategies at the same time
|
||||
|
||||
Args:
|
||||
strategies: A list of strategies
|
||||
"""
|
||||
[self.add_strategy(strategy) for strategy in strategies]
|
||||
|
||||
def add_strategy_all(self, *, strategy: Type[Strategy], params: dict | None = None):
|
||||
"""Use this to run a single strategy on all available instruments in the market using the default parameters
|
||||
i.e one set of parameters for all trading symbols
|
||||
|
||||
Keyword Args:
|
||||
strategy (Strategy): Strategy class
|
||||
params (dict): A dictionary of parameters for the strategy
|
||||
"""
|
||||
[self.add_strategy(strategy(symbol=symbol, params=params)) for symbol in self.symbols]
|
||||
|
||||
async def init_symbols(self):
|
||||
"""Initialize the symbols for the current trading session. This method is called internally by the bot.
|
||||
"""
|
||||
syms = [self.init_symbol(symbol) for symbol in self.symbols]
|
||||
await asyncio.gather(*syms, return_exceptions=True)
|
||||
|
||||
async def init_symbol(self, symbol: Symbol) -> Symbol:
|
||||
"""Initialize a symbol before the beginning of a trading sessions.
|
||||
Removes it from the list of symbols if it was not successfully initialized or not available
|
||||
for the account.
|
||||
|
||||
Args:
|
||||
symbol (Symbol): Symbol object to be initialized
|
||||
|
||||
Returns:
|
||||
Symbol: if successfully initialized
|
||||
"""
|
||||
if self.account.has_symbol(symbol):
|
||||
init = await symbol.init()
|
||||
if init:
|
||||
return symbol
|
||||
self.symbols.discard(symbol)
|
||||
logger.warning(f'Unable to initialize symbol {symbol}')
|
||||
|
||||
self.symbols.remove(symbol)
|
||||
logger.warning(f'{symbol} not a available for this market')
|
||||
@@ -0,0 +1,234 @@
|
||||
"""Candle and Candles classes for handling bars from the MetaTrader 5 terminal."""
|
||||
|
||||
from typing import Type, TypeVar, Generic, Iterable
|
||||
from logging import getLogger
|
||||
import reprlib
|
||||
|
||||
from pandas import DataFrame, Series
|
||||
import pandas_ta as ta
|
||||
|
||||
from .core.constants import TimeFrame
|
||||
|
||||
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.
|
||||
|
||||
Attributes:
|
||||
time (int): Period start time.
|
||||
open (int): Open price
|
||||
high (float): The highest price of the period
|
||||
low (float): The lowest price of the period
|
||||
close (float): Close price
|
||||
tick_volume (float): Tick volume
|
||||
real_volume (float): Trade volume
|
||||
spread (float): Spread
|
||||
Index (int): Custom attribute representing the position of the candle in a sequence.
|
||||
"""
|
||||
time: float
|
||||
high: float
|
||||
low: float
|
||||
close: float
|
||||
real_volume: float
|
||||
spread: float
|
||||
open: float
|
||||
tick_volume: float
|
||||
Index: int
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""Create a Candle object from keyword arguments.
|
||||
|
||||
Keyword Args:
|
||||
**kwargs: Candle attributes and values as keyword arguments.
|
||||
"""
|
||||
self.time = kwargs.pop('time', 0)
|
||||
self.Index = kwargs.pop('Index', 0)
|
||||
self.set_attributes(**kwargs)
|
||||
def __repr__(self):
|
||||
keys = reprlib.repr(', '.join('%s=%s' % (i, j) for i, j in self.__dict__.items()))[1:-1]
|
||||
return '%(class)s(%(args)s)' % {'class': self.__class__.__name__, 'args': keys}
|
||||
|
||||
def __eq__(self, other: 'Candle'):
|
||||
return self.time == other.time
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.time)
|
||||
|
||||
def __lt__(self, other: 'Candle'):
|
||||
return self.time < other.time
|
||||
|
||||
def __gt__(self, other: 'Candle'):
|
||||
return self.time > other.time
|
||||
|
||||
def set_attributes(self, **kwargs):
|
||||
"""Set keyword arguments as instance attributes
|
||||
|
||||
Keyword Args:
|
||||
**kwargs: Instance attributes and values as keyword arguments
|
||||
"""
|
||||
[setattr(self, i, j) for i, j in kwargs.items()]
|
||||
|
||||
@property
|
||||
def mid(self) -> float:
|
||||
"""The median of open and close
|
||||
|
||||
Returns:
|
||||
float: The median of open and close
|
||||
"""
|
||||
return (self.open + self.close) / 2
|
||||
|
||||
def is_bullish(self) -> bool:
|
||||
""" A simple check to see if the candle is bullish.
|
||||
|
||||
Returns:
|
||||
bool: True or False
|
||||
"""
|
||||
return self.close >= self.open
|
||||
|
||||
def is_bearish(self) -> bool:
|
||||
"""A simple check to see if the candle is bearish.
|
||||
|
||||
Returns:
|
||||
bool: True or False
|
||||
"""
|
||||
return self.open > self.close
|
||||
|
||||
_Candle = TypeVar('_Candle', bound=Candle)
|
||||
_Candles = TypeVar('_Candles', bound='Candles')
|
||||
|
||||
|
||||
class Candles(Generic[_Candle]):
|
||||
"""An iterable container class of Candle objects in chronological order.
|
||||
|
||||
Attributes:
|
||||
Index (Series['int']): A pandas Series of the indexes of all candles in the object.
|
||||
time (Series['int']): A pandas Series of the time of all candles in the object.
|
||||
open (Series[float]): A pandas Series of the opening price of all candles in the object.
|
||||
high (Series[float]): A pandas Series of the high price of all candles in the object.
|
||||
low (Series[float]): A pandas Series of the low price of all candles in the object.
|
||||
close (Series[float]): A pandas Series of the closing price of all candles in the object.
|
||||
tick_volume (Series[float]): A pandas Series of the tick volume of all candles in the object.
|
||||
real_volume (Series[float]): A pandas Series of the real volume of all candles in the object.
|
||||
spread (Series[float]): A pandas Series of the spread of all candles in the object.
|
||||
timeframe (TimeFrame): The timeframe of the candles in the object.
|
||||
Candle (Type[Candle]): The Candle class for representing the candles in the object.
|
||||
|
||||
properties:
|
||||
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.
|
||||
"""
|
||||
Index: Series
|
||||
time: Series
|
||||
open: Series
|
||||
high: Series
|
||||
low: Series
|
||||
close: Series
|
||||
tick_volume: Series
|
||||
real_volume: Series
|
||||
spread: Series
|
||||
Candle: Type[Candle]
|
||||
timeframe: TimeFrame
|
||||
|
||||
def __init__(self, *, data: DataFrame | _Candles | Iterable, flip=False, candle_class: Type[_Candle] = None):
|
||||
"""A container class of Candle objects in chronological order.
|
||||
|
||||
Args:
|
||||
data (DataFrame|Candles|Iterable): A pandas dataframe, a Candles object or any suitable iterable
|
||||
|
||||
Keyword Args:
|
||||
flip (bool): Reverse the chronological order of the candles to the oldest first. Defaults to False.
|
||||
candle_class: A subclass of Candle to use as the candle class. Defaults to Candle.
|
||||
"""
|
||||
if isinstance(data, DataFrame):
|
||||
data = data
|
||||
elif isinstance(data, type(self)):
|
||||
data = DataFrame(data.data)
|
||||
elif isinstance(data, Iterable):
|
||||
data = DataFrame(data)
|
||||
else:
|
||||
raise ValueError(f'Cannot create DataFrame from object of {type(data)}')
|
||||
|
||||
self._data = data.iloc[::-1] if flip else data
|
||||
self.Candle = candle_class or Candle
|
||||
|
||||
def __repr__(self):
|
||||
return self._data.__repr__()
|
||||
|
||||
def __len__(self):
|
||||
return self._data.shape[0]
|
||||
|
||||
def __contains__(self, item: _Candle):
|
||||
return item.time == self[item.Index].time
|
||||
|
||||
def __getitem__(self, index) -> _Candle | _Candles:
|
||||
if isinstance(index, slice):
|
||||
cls = self.__class__
|
||||
data = self._data.iloc[index]
|
||||
data.reset_index(drop=True, inplace=True)
|
||||
return cls(data=data)
|
||||
|
||||
if isinstance(index, str):
|
||||
return self._data[index]
|
||||
|
||||
item = self._data.iloc[index]
|
||||
return self.Candle(Index=index, **item)
|
||||
|
||||
def __setitem__(self, index, value: Series):
|
||||
if isinstance(value, Series):
|
||||
self._data[index] = value
|
||||
return
|
||||
raise TypeError(f'Expected Series got {type(value)}')
|
||||
|
||||
def __getattr__(self, item):
|
||||
if item in list(self._data.columns.values):
|
||||
return self._data[item]
|
||||
raise AttributeError(f'Attribute {item} not defined on class {self.__class__.__name__}')
|
||||
|
||||
def __iter__(self):
|
||||
return (self.Candle(**row._asdict()) for row in self._data.itertuples())
|
||||
|
||||
@property
|
||||
def timeframe(self):
|
||||
tf = self.time[1] - self.time[0]
|
||||
return TimeFrame.get(abs(tf))
|
||||
|
||||
@property
|
||||
def ta(self):
|
||||
"""Access to the pandas_ta library for performing technical analysis on the underlying data attribute.
|
||||
|
||||
Returns:
|
||||
pandas_ta: The pandas_ta library
|
||||
"""
|
||||
return self._data.ta
|
||||
|
||||
@property
|
||||
def ta_lib(self):
|
||||
"""Access to the ta library for performing technical analysis. Not dependent on the underlying data attribute.
|
||||
|
||||
Returns:
|
||||
ta: The ta library
|
||||
"""
|
||||
return ta
|
||||
|
||||
@property
|
||||
def data(self) -> DataFrame:
|
||||
"""The original data passed to the class as a pandas DataFrame"""
|
||||
return self._data
|
||||
|
||||
def rename(self, inplace=True, **kwargs) -> _Candles | None :
|
||||
"""Rename columns of the candles class.
|
||||
|
||||
Keyword Args:
|
||||
inplace (bool): Rename the columns inplace or return a new instance of the class with the renamed columns
|
||||
**kwargs: The new names of the columns
|
||||
|
||||
Returns:
|
||||
Candles: A new instance of the class with the renamed columns if inplace is False.
|
||||
None: If inplace is True
|
||||
"""
|
||||
res = self._data.rename(columns=kwargs, inplace=inplace)
|
||||
return res if inplace else self.__class__(data=res)
|
||||
@@ -0,0 +1,7 @@
|
||||
from .meta_trader import MetaTrader
|
||||
from .config import Config
|
||||
from .models import *
|
||||
from .constants import *
|
||||
from .base import Base
|
||||
from .errors import Error
|
||||
from .exceptions import *
|
||||
@@ -0,0 +1,137 @@
|
||||
from functools import cache
|
||||
import reprlib
|
||||
from logging import getLogger
|
||||
|
||||
from .config import Config
|
||||
from .meta_trader import MetaTrader
|
||||
|
||||
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.
|
||||
|
||||
Keyword Args:
|
||||
**kwargs: Object attributes and values as keyword arguments. Only added if they are annotated on the class body.
|
||||
|
||||
Class Attributes:
|
||||
mt5 (MetaTrader): An instance of the MetaTrader class
|
||||
config (Config): An instance of the Config class
|
||||
Meta (Type[Meta]): The Meta class for configuration of the data model class
|
||||
"""
|
||||
mt5: MetaTrader = MetaTrader()
|
||||
config = Config()
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.set_attributes(**kwargs)
|
||||
|
||||
def __repr__(self):
|
||||
keys = reprlib.repr(', '.join('%s=%s' % (i, j) for i, j in self.__dict__.items()))[1:-1]
|
||||
return '%(class)s(%(args)s)' % {'class': self.__class__.__name__, 'args': keys}
|
||||
|
||||
def set_attributes(self, **kwargs):
|
||||
"""Set keyword arguments as object attributes
|
||||
|
||||
Keyword Args:
|
||||
**kwargs: Object attributes and values as keyword arguments
|
||||
|
||||
Raises:
|
||||
AttributeError: When assigning an attribute that does not belong to the class or any parent class
|
||||
|
||||
Notes:
|
||||
Only sets attributes that have been annotated on the class body.
|
||||
"""
|
||||
for i, j in kwargs.items():
|
||||
try:
|
||||
setattr(self, i, self.annotations[i](j))
|
||||
except KeyError:
|
||||
logger.warning(f"Attribute {i} does not belong to class {self.__class__.__name__}")
|
||||
continue
|
||||
|
||||
except ValueError:
|
||||
logger.warning(f'Cannot covert object of type {type(j)} to type {self.annotations[i]}')
|
||||
continue
|
||||
|
||||
except Exception as exe:
|
||||
logger.warning(f'Did not set attribute {i} on class {self.__class__.__name__} due to {exe}')
|
||||
continue
|
||||
|
||||
@property
|
||||
@cache
|
||||
def annotations(self) -> dict:
|
||||
"""Class annotations from all ancestor classes and the current class.
|
||||
|
||||
Returns:
|
||||
dict: A dictionary of class annotations
|
||||
"""
|
||||
annots = {}
|
||||
for base in self.__class__.__mro__[-3::-1]:
|
||||
annots |= getattr(base, '__annotations__', {})
|
||||
return annots
|
||||
|
||||
def get_dict(self, exclude: set = None, include: set = None) -> dict:
|
||||
"""Returns class attributes as a dict, with the ability to filter
|
||||
|
||||
Keyword Args:
|
||||
exclude: A set of attributes to be excluded
|
||||
include: Specific attributes to be returned
|
||||
|
||||
Returns:
|
||||
dict: A dictionary of specified class attributes
|
||||
|
||||
Notes:
|
||||
You can only set either of include or exclude. If you set both, include will take precedence
|
||||
"""
|
||||
exclude, include = exclude or set(), include or set()
|
||||
filter_ = include or set(self.dict.keys()).difference(exclude)
|
||||
return {key: value for key, value in self.dict.items() if key in filter_}
|
||||
|
||||
@property
|
||||
@cache
|
||||
def class_vars(self):
|
||||
"""Annotated class attributes
|
||||
|
||||
Returns:
|
||||
dict: A dictionary of available class attributes in all ancestor classes and the current class.
|
||||
"""
|
||||
clss = self.__class__.__mro__[-3::-1]
|
||||
cls_dict = {}
|
||||
for cls in clss:
|
||||
cls_dict |= cls.__dict__
|
||||
return {key: value for key, value in cls_dict.items() if key in self.annotations}
|
||||
|
||||
@property
|
||||
def dict(self) -> dict:
|
||||
"""All instance and class attributes as a dictionary, except those excluded in the Meta class.
|
||||
|
||||
Returns:
|
||||
dict: A dictionary of instance and class attributes
|
||||
"""
|
||||
try:
|
||||
return {key: value for key, value in (self.class_vars | self.__dict__).items() if key not in self.Meta.filter}
|
||||
except Exception as err:
|
||||
logger.warning(err)
|
||||
|
||||
class Meta:
|
||||
"""A class for defining class attributes to be excluded or included in the dict property
|
||||
|
||||
Attributes:
|
||||
exclude (set): A set of attributes to be excluded
|
||||
include (set): Specific attributes to be returned. Include supercedes exclude.
|
||||
"""
|
||||
exclude = {'mt5', "Config"}
|
||||
include = set()
|
||||
|
||||
@classmethod
|
||||
@property
|
||||
def filter(cls) -> set:
|
||||
"""Combine the exclude and include attributes to return a set of attributes to be excluded.
|
||||
|
||||
Returns:
|
||||
set: A set of attributes to be excluded
|
||||
"""
|
||||
return cls.exclude.difference(cls.include)
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
import os
|
||||
from pathlib import Path
|
||||
from sys import _getframe
|
||||
from typing import Iterator
|
||||
import json
|
||||
from logging import getLogger
|
||||
|
||||
logger = getLogger(__name__)
|
||||
|
||||
|
||||
class Config:
|
||||
"""A class for handling configuration settings for the aiomql package.
|
||||
|
||||
Keyword Args:
|
||||
**kwargs: Configuration settings as keyword arguments.
|
||||
Variables set this way supersede those set in the config file.
|
||||
|
||||
Attributes:
|
||||
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
|
||||
path (str): Path to terminal file
|
||||
timeout (int): Timeout for terminal connection
|
||||
|
||||
Notes:
|
||||
By default, the config class looks for a file named aiomql.json.
|
||||
You can change this by passing the filename keyword argument to the constructor.
|
||||
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 = ''
|
||||
timeout: int = 60000
|
||||
record_trades: bool = True
|
||||
filename: str = 'aiomql.json'
|
||||
win_percentage: float = 0.85
|
||||
records_dir = Path.home() / 'Documents' / 'Aiomql' / 'Trade Records' if record_trades else None
|
||||
_load = 1
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
if not hasattr(cls, '_instance'):
|
||||
cls._instance = super().__new__(cls)
|
||||
return cls._instance
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.load_config(reload=False)
|
||||
[setattr(self, key, value) for key, value in kwargs]
|
||||
|
||||
|
||||
@staticmethod
|
||||
def walk_to_root(path: str) -> Iterator[str]:
|
||||
|
||||
if not os.path.exists(path):
|
||||
raise IOError('Starting path not found')
|
||||
|
||||
if os.path.isfile(path):
|
||||
path = os.path.dirname(path)
|
||||
|
||||
last_dir = None
|
||||
current_dir = os.path.abspath(path)
|
||||
while last_dir != current_dir:
|
||||
yield current_dir
|
||||
parent_dir = os.path.abspath(os.path.join(current_dir, os.path.pardir))
|
||||
last_dir, current_dir = current_dir, parent_dir
|
||||
|
||||
def find_config(self):
|
||||
current_file = __file__
|
||||
frame = _getframe()
|
||||
while frame.f_code.co_filename == current_file:
|
||||
if frame.f_back is None:
|
||||
return None
|
||||
frame = frame.f_back
|
||||
frame_filename = frame.f_code.co_filename
|
||||
path = os.path.dirname(os.path.abspath(frame_filename))
|
||||
|
||||
for dirname in self.walk_to_root(path):
|
||||
check_path = os.path.join(dirname, self.filename)
|
||||
if os.path.isfile(check_path):
|
||||
return check_path
|
||||
return None
|
||||
|
||||
def load_config(self, file: str = None, reload: bool = True):
|
||||
if reload:
|
||||
self._load = 1
|
||||
if self._load != 1:
|
||||
return
|
||||
|
||||
self._load = 0
|
||||
data = {}
|
||||
if (file := (file or self.find_config())) is None:
|
||||
logger.warning('No Config File Found')
|
||||
else:
|
||||
fh = open(file, mode='r')
|
||||
data = json.load(fh)
|
||||
fh.close()
|
||||
[setattr(self, key, value) for key, value in data.items()]
|
||||
self.records_dir.mkdir(parents=True, exist_ok=True) if self.records_dir else ...
|
||||
|
||||
def account_info(self) -> dict['login', 'password', 'server']:
|
||||
"""Returns Account login details as found in the config object if available
|
||||
|
||||
Returns:
|
||||
dict: A dictionary of login details
|
||||
"""
|
||||
return {'login': self.login, 'password': self.password, 'server': self.server}
|
||||
@@ -0,0 +1,786 @@
|
||||
from enum import IntEnum, IntFlag
|
||||
|
||||
import MetaTrader5 as mt5
|
||||
|
||||
"""
|
||||
MetaTrader5 constants as IntEnum types with Python style class names and nice string representation
|
||||
|
||||
Examples:
|
||||
>>> from aiomql import OrderFilling
|
||||
>>> fok = OrderFilling.FOK
|
||||
>>> print(fok)
|
||||
"ORDER_FILLING_FOK"
|
||||
"""
|
||||
|
||||
|
||||
class Repr:
|
||||
__enum_name__ = ""
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.__enum_name__}_{self.name}"
|
||||
|
||||
|
||||
class TradeAction(Repr, IntEnum):
|
||||
"""TRADE_REQUEST_ACTION Enum.
|
||||
|
||||
Attributes:
|
||||
DEAL (int): Delete the pending order placed previously Place a trade order for an immediate execution with the
|
||||
specified parameters (market order).
|
||||
PENDING (int): Delete the pending order placed previously
|
||||
SLTP (int): Modify Stop Loss and Take Profit values of an opened position
|
||||
MODIFY (int): Modify the parameters of the order placed previously
|
||||
REMOVE (int): Delete the pending order placed previously
|
||||
CLOSE_BY (int): Close a position by an opposite one
|
||||
"""
|
||||
__enum_name__ = "TRADE_ACTION"
|
||||
DEAL = mt5.TRADE_ACTION_DEAL
|
||||
PENDING = mt5.TRADE_ACTION_PENDING
|
||||
SLTP = mt5.TRADE_ACTION_SLTP
|
||||
MODIFY = mt5.TRADE_ACTION_MODIFY
|
||||
REMOVE = mt5.TRADE_ACTION_MODIFY
|
||||
CLOSE_BY = mt5.TRADE_ACTION_CLOSE_BY
|
||||
|
||||
|
||||
class OrderFilling(Repr, IntEnum):
|
||||
"""ORDER_TYPE_FILLING Enum.
|
||||
|
||||
Attributes:
|
||||
FOK (int): This execution policy means that an order can be executed only in the specified volume.
|
||||
If the necessary amount of a financial instrument is currently unavailable in the market, the order will
|
||||
not be executed. The desired volume can be made up of several available offers.
|
||||
|
||||
IOC (int): An agreement to execute a deal at the maximum volume available in the market within the volume
|
||||
specified in the order. If the request cannot be filled completely, an order with the available volume will
|
||||
be executed, and the remaining volume will be canceled.
|
||||
|
||||
RETURN (int): This policy is used only for market (ORDER_TYPE_BUY and ORDER_TYPE_SELL), limit and stop limit
|
||||
orders (ORDER_TYPE_BUY_LIMIT, ORDER_TYPE_SELL_LIMIT,ORDER_TYPE_BUY_STOP_LIMIT and
|
||||
ORDER_TYPE_SELL_STOP_LIMIT) and only for the symbols with Market or Exchange execution modes. If filled
|
||||
partially, a market or limit order with the remaining volume is not canceled, and is processed further.
|
||||
During activation of the ORDER_TYPE_BUY_STOP_LIMIT and ORDER_TYPE_SELL_STOP_LIMIT orders, an appropriate
|
||||
limit order ORDER_TYPE_BUY_LIMIT/ORDER_TYPE_SELL_LIMIT with the ORDER_FILLING_RETURN type is created.
|
||||
"""
|
||||
__enum_name__ = "ORDER_FILLING"
|
||||
FOK = mt5.ORDER_FILLING_FOK
|
||||
IOC = mt5.ORDER_FILLING_IOC
|
||||
RETURN = mt5.ORDER_FILLING_RETURN
|
||||
|
||||
|
||||
class OrderTime(Repr, IntEnum):
|
||||
"""ORDER_TIME Enum.
|
||||
|
||||
Attributes:
|
||||
GTC (int): Good till cancel order
|
||||
DAY (int): Good till current trade day order
|
||||
SPECIFIED (int): The order is active until the specified date
|
||||
SPECIFIED_DAY (int): The order is active until 23:59:59 of the specified day. If this time appears to be out of
|
||||
a trading session, the expiration is processed at the nearest trading time.
|
||||
"""
|
||||
__enum_name__ = "ORDER_TIME"
|
||||
GTC = mt5.ORDER_TIME_GTC
|
||||
DAY = mt5.ORDER_TIME_DAY
|
||||
SPECIFIED = mt5.ORDER_TIME_SPECIFIED
|
||||
SPECIFIED_DAY = mt5.ORDER_TIME_SPECIFIED_DAY
|
||||
|
||||
|
||||
class OrderType(Repr, IntEnum):
|
||||
"""ORDER_TYPE Enum.
|
||||
|
||||
Attributes:
|
||||
BUY (int): Market buy order
|
||||
SELL (int): Market sell order
|
||||
BUY_LIMIT (int): Buy Limit pending order
|
||||
SELL_LIMIT (int): Sell Limit pending order
|
||||
BUY_STOP (int): Buy Stop pending order
|
||||
SELL_STOP (int): Sell Stop pending order
|
||||
BUY_STOP_LIMIT (int): Upon reaching the order price, Buy Limit pending order is placed at StopLimit price
|
||||
SELL_STOP_LIMIT (int): Upon reaching the order price, Sell Limit pending order is placed at StopLimit price
|
||||
CLOSE_BY (int): Order for closing a position by an opposite one
|
||||
|
||||
Properties:
|
||||
opposite (int): Gets the opposite of an order type
|
||||
"""
|
||||
__enum_name__ = "ORDER_TYPE"
|
||||
BUY = mt5.ORDER_TYPE_BUY
|
||||
SELL = mt5.ORDER_TYPE_SELL
|
||||
BUY_LIMIT = mt5.ORDER_TYPE_BUY_LIMIT
|
||||
SELL_LIMIT = mt5.ORDER_TYPE_SELL_LIMIT
|
||||
BUY_STOP = mt5.ORDER_TYPE_BUY_STOP
|
||||
SELL_STOP = mt5.ORDER_TYPE_SELL_STOP
|
||||
BUY_STOP_LIMIT = mt5.ORDER_TYPE_BUY_STOP_LIMIT
|
||||
SELL_STOP_LIMIT = mt5.ORDER_TYPE_SELL_STOP_LIMIT
|
||||
CLOSE_BY = mt5.ORDER_TYPE_CLOSE_BY
|
||||
|
||||
@property
|
||||
def opposite(self):
|
||||
"""Gets the opposite of an order type for closing an open position
|
||||
|
||||
Returns:
|
||||
int: integer value of opposite order type
|
||||
"""
|
||||
return {0: 1, 1: 0, 2: 3, 3: 2, 4: 5, 5: 4, 6: 7, 7: 6, 8: 8}[self]
|
||||
|
||||
|
||||
class BookType(Repr, IntEnum):
|
||||
"""BOOK_TYPE Enum.
|
||||
|
||||
Attributes:
|
||||
SELL (int): Sell order (Offer)
|
||||
BUY (int): Buy order (Bid)
|
||||
SELL_MARKET (int): Sell order by Market
|
||||
BUY_MARKET (int): Buy order by Market
|
||||
"""
|
||||
__enum_name__ = "BOOK_TYPE"
|
||||
SELL = mt5.BOOK_TYPE_SELL
|
||||
BUY = mt5.BOOK_TYPE_BUY
|
||||
SELL_MARKET = mt5.BOOK_TYPE_SELL_MARKET
|
||||
BUY_MARKET = mt5.BOOK_TYPE_BUY_MARKET
|
||||
|
||||
|
||||
class TimeFrame(Repr, IntEnum):
|
||||
"""TIMEFRAME Enum.
|
||||
|
||||
Attributes:
|
||||
M1 (int): One Minute
|
||||
M2 (int): Two Minutes
|
||||
M3 (int): Three Minutes
|
||||
M4 (int): Four Minutes
|
||||
M5 (int): Five Minutes
|
||||
M6 (int): Six Minutes
|
||||
M10 (int): Ten Minutes
|
||||
M15 (int): Fifteen Minutes
|
||||
M20 (int): Twenty Minutes
|
||||
M30 (int): Thirty Minutes
|
||||
H1 (int): One Hour
|
||||
H2 (int): Two Hours
|
||||
H3 (int): Three Hours
|
||||
H4 (int): Four Hours
|
||||
H6 (int): Six Hours
|
||||
H8 (int): Eight Hours
|
||||
D1 (int): One Day
|
||||
W1 (int): One Week
|
||||
MN1 (int): One Month
|
||||
|
||||
Properties:
|
||||
time: return the value of the timeframe object in seconds. Used as a property
|
||||
|
||||
Methods:
|
||||
get: get a timeframe object from a time value in seconds
|
||||
"""
|
||||
__enum_name__ = "TIMEFRAME"
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
M1 = mt5.TIMEFRAME_M1
|
||||
M2 = mt5.TIMEFRAME_M2
|
||||
M3 = mt5.TIMEFRAME_M3
|
||||
M4 = mt5.TIMEFRAME_M4
|
||||
M5 = mt5.TIMEFRAME_M5
|
||||
M6 = mt5.TIMEFRAME_M6
|
||||
M10 = mt5.TIMEFRAME_M10
|
||||
M15 = mt5.TIMEFRAME_M15
|
||||
M20 = mt5.TIMEFRAME_M20
|
||||
M30 = mt5.TIMEFRAME_M30
|
||||
H1 = mt5.TIMEFRAME_H1
|
||||
H2 = mt5.TIMEFRAME_H2
|
||||
H3 = mt5.TIMEFRAME_H3
|
||||
H4 = mt5.TIMEFRAME_H4
|
||||
H6 = mt5.TIMEFRAME_H6
|
||||
H8 = mt5.TIMEFRAME_H8
|
||||
H12 = mt5.TIMEFRAME_H12
|
||||
D1 = mt5.TIMEFRAME_D1
|
||||
W1 = mt5.TIMEFRAME_W1
|
||||
MN1 = mt5.TIMEFRAME_MN1
|
||||
|
||||
@property
|
||||
def time(self):
|
||||
"""The number of seconds in a TIMEFRAME
|
||||
|
||||
Returns:
|
||||
int: The number of seconds in a TIMEFRAME
|
||||
|
||||
Examples:
|
||||
>>> t = TimeFrame.H1
|
||||
>>> print(t.time)
|
||||
3600
|
||||
"""
|
||||
times = {1: 60, 2: 120, 3: 180, 4: 240, 5: 300, 6: 360, 10: 600, 15: 900, 20: 1200, 30: 1800, 16385: 3600,
|
||||
16386: 7200, 16387: 10800, 16388: 14400, 16390: 21600, 16392: 28800, 16396: 43200, 16408: 86400,
|
||||
32769: 604800, 49153: 2592000}
|
||||
return times[self]
|
||||
|
||||
@classmethod
|
||||
def get(cls, time: int):
|
||||
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}
|
||||
return TimeFrame(times[int(time)])
|
||||
|
||||
|
||||
class CopyTicks(Repr, IntEnum):
|
||||
"""COPY_TICKS Enum. This defines the types of ticks that can be requested using the copy_ticks_from() and
|
||||
copy_ticks_range() functions.
|
||||
|
||||
Attributes:
|
||||
ALL (int): All ticks
|
||||
INFO (int): Ticks containing Bid and/or Ask price changes
|
||||
TRADE (int): Ticks containing Last and/or Volume price changes
|
||||
"""
|
||||
__enum_name__ = "COPY_TICKS"
|
||||
ALL = mt5.COPY_TICKS_ALL
|
||||
INFO = mt5.COPY_TICKS_INFO
|
||||
TRADE = mt5.COPY_TICKS_TRADE
|
||||
|
||||
|
||||
class PositionType(Repr, IntEnum):
|
||||
"""POSITION_TYPE Enum. Direction of an open position (buy or sell)
|
||||
|
||||
Attributes:
|
||||
BUY (int): Buy
|
||||
SELL (int): Sell
|
||||
"""
|
||||
__enum_name__ = "POSITION_TYPE"
|
||||
BUY = mt5.POSITION_TYPE_BUY
|
||||
SELL = mt5.POSITION_TYPE_SELL
|
||||
|
||||
|
||||
class PositionReason(Repr, IntEnum):
|
||||
"""POSITION_REASON Enum. The reason for opening a position is contained in the POSITION_REASON Enum
|
||||
|
||||
Attributes:
|
||||
CLIENT (int): The position was opened as a result of activation of an order placed from a desktop terminal
|
||||
MOBILE (int): The position was opened as a result of activation of an order placed from a mobile application
|
||||
WEB (int): The position was opened as a result of activation of an order placed from the web platform
|
||||
EXPERT (int): The position was opened as a result of activation of an order placed from an MQL5 program,
|
||||
i.e. an Expert Advisor or a script
|
||||
"""
|
||||
__enum_name__ = "POSITION_REASON"
|
||||
CLIENT = mt5.POSITION_REASON_CLIENT
|
||||
MOBILE = mt5.POSITION_REASON_MOBILE
|
||||
WEB = mt5.POSITION_REASON_WEB
|
||||
EXPERT = mt5.POSITION_REASON_EXPERT
|
||||
|
||||
|
||||
class DealType(Repr, IntEnum):
|
||||
"""DEAL_TYPE enum. Each deal is characterized by a type, allowed values are enumerated in this enum
|
||||
|
||||
Attributes:
|
||||
BUY (int): Buy
|
||||
SELL (int): Sell
|
||||
BALANCE (int): Balance
|
||||
CREDIT (int): Credit
|
||||
CHARGE (int): Additional Charge
|
||||
CORRECTION (int): Correction
|
||||
BONUS (int): Bonus
|
||||
COMMISSION (int): Additional Commission
|
||||
COMMISSION_DAILY (int): Daily Commission
|
||||
COMMISSION_MONTHLY (int): Monthly Commission
|
||||
COMMISSION_AGENT_DAILY (int): Daily Agent Commission
|
||||
COMMISSION_AGENT_MONTHLY (int): Monthly Agent Commission
|
||||
INTEREST (int): Interest Rate
|
||||
DEAL_DIVIDEND (int): Dividend Operations
|
||||
DEAL_DIVIDEND_FRANKED (int): Franked (non-taxable) dividend operations
|
||||
DEAL_TAX (int): Tax Charges
|
||||
|
||||
BUY_CANCELED (int): Canceled buy deal. There can be a situation when a previously executed buy deal is canceled.
|
||||
In this case, the type of the previously executed deal (DEAL_TYPE_BUY) is changed to DEAL_TYPE_BUY_CANCELED,
|
||||
and its profit/loss is zeroized. Previously obtained profit/loss is charged/withdrawn using a separated
|
||||
balance operation
|
||||
|
||||
SELL_CANCELED (int): Canceled sell deal. There can be a situation when a previously executed sell deal is
|
||||
canceled. In this case, the type of the previously executed deal (DEAL_TYPE_SELL) is changed to
|
||||
DEAL_TYPE_SELL_CANCELED, and its profit/loss is zeroized. Previously obtained profit/loss is
|
||||
charged/withdrawn using a separated balance operation.
|
||||
"""
|
||||
__enum_name__ = "DEAL_TYPE"
|
||||
BUY = mt5.DEAL_TYPE_BUY
|
||||
SELL = mt5.DEAL_TYPE_SELL
|
||||
BALANCE = mt5.DEAL_TYPE_BALANCE
|
||||
CREDIT = mt5.DEAL_TYPE_CREDIT
|
||||
CHARGE = mt5.DEAL_TYPE_CHARGE
|
||||
CORRECTION = mt5.DEAL_TYPE_CORRECTION
|
||||
BONUS = mt5.DEAL_TYPE_BONUS
|
||||
COMMISSION = mt5.DEAL_TYPE_COMMISSION
|
||||
COMMISSION_DAILY = mt5.DEAL_TYPE_COMMISSION_DAILY
|
||||
COMMISSION_MONTHLY = mt5.DEAL_TYPE_COMMISSION_MONTHLY
|
||||
COMMISSION_AGENT_DAILY = mt5.DEAL_TYPE_COMMISSION_AGENT_DAILY
|
||||
COMMISSION_AGENT_MONTHLY = mt5.DEAL_TYPE_COMMISSION_AGENT_MONTHLY
|
||||
INTEREST = mt5.DEAL_TYPE_INTEREST
|
||||
BUY_CANCELED = mt5.DEAL_TYPE_BUY_CANCELED
|
||||
SELL_CANCELED = mt5.DEAL_TYPE_SELL_CANCELED
|
||||
DEAL_DIVIDEND = mt5.DEAL_DIVIDEND
|
||||
DEAL_DIVIDEND_FRANKED = mt5.DEAL_DIVIDEND_FRANKED
|
||||
DEAL_TAX = mt5.DEAL_TAX
|
||||
|
||||
def __str__(self):
|
||||
if self.name in ('DEAL_DIVIDEND', 'DEAL_DIVIDEND_FRANKED', 'DEAL_TAX'):
|
||||
return self.name
|
||||
return super().__str__()
|
||||
|
||||
|
||||
class DealEntry(Repr, IntEnum):
|
||||
"""DEAL_ENTRY Enum. Deals differ not only in their types set in DEAL_TYPE enum, but also in the way they change
|
||||
positions. This can be a simple position opening, or accumulation of a previously opened position (market entering),
|
||||
position closing by an opposite deal of a corresponding volume (market exiting), or position reversing, if the
|
||||
opposite-direction deal covers the volume of the previously opened position.
|
||||
|
||||
Attributes:
|
||||
IN (int): Entry In
|
||||
OUT (int): Entry Out
|
||||
INOUT (int): Reverse
|
||||
OUT_BY (int): Close a position by an opposite one
|
||||
"""
|
||||
__enum_name__ = "DEAL_ENTRY"
|
||||
IN = mt5.DEAL_ENTRY_IN
|
||||
OUT = mt5.DEAL_ENTRY_OUT
|
||||
INOUT = mt5.DEAL_ENTRY_INOUT
|
||||
OUT_BY = mt5.DEAL_ENTRY_OUT_BY
|
||||
|
||||
|
||||
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
|
||||
of the StopOut event, variation margin calculation, etc.
|
||||
|
||||
Attributes:
|
||||
CLIENT (int): The deal was executed as a result of activation of an order placed from a desktop terminal
|
||||
MOBILE (int): The deal was executed as a result of activation of an order placed from a desktop terminal
|
||||
WEB (int): The deal was executed as a result of activation of an order placed from the web platform
|
||||
EXPERT (int): The deal was executed as a result of activation of an order placed from an MQL5 program, i.e.
|
||||
an Expert Advisor or a script
|
||||
SL (int): The deal was executed as a result of Stop Loss activation
|
||||
TP (int): The deal was executed as a result of Take Profit activation
|
||||
SO (int): The deal was executed as a result of the Stop Out event
|
||||
ROLLOVER (int): The deal was executed due to a rollover
|
||||
VMARGIN (int): The deal was executed after charging the variation margin
|
||||
SPLIT (int): The deal was executed after the split (price reduction) of an instrument, which had an open
|
||||
position during split announcement
|
||||
"""
|
||||
__enum_name__ = "DEAL_REASON"
|
||||
CLIENT = mt5.DEAL_REASON_CLIENT
|
||||
MOBILE = mt5.DEAL_REASON_MOBILE
|
||||
WEB = mt5.DEAL_REASON_WEB
|
||||
EXPERT = mt5.DEAL_REASON_EXPERT
|
||||
SL = mt5.DEAL_REASON_SL
|
||||
TP = mt5.DEAL_REASON_TP
|
||||
SO = mt5.DEAL_REASON_SO
|
||||
ROLLOVER = mt5.DEAL_REASON_ROLLOVER
|
||||
VMARGIN = mt5.DEAL_REASON_VMARGIN
|
||||
SPLIT = mt5.DEAL_REASON_SPLIT
|
||||
|
||||
|
||||
class OrderReason(Repr, IntEnum):
|
||||
"""ORDER_REASON Enum.
|
||||
|
||||
Attributes:
|
||||
CLIENT (int): The order was placed from a desktop terminal
|
||||
MOBILE (int): The order was placed from a mobile application
|
||||
WEB (int): The order was placed from a web platform
|
||||
EXPERT (int): The order was placed from an MQL5-program, i.e. by an Expert Advisor or a script
|
||||
SL (int): The order was placed as a result of Stop Loss activation
|
||||
TP (int): The order was placed as a result of Take Profit activation
|
||||
SO (int): The order was placed as a result of the Stop Out event
|
||||
"""
|
||||
__enum_name__ = "ORDER_REASON"
|
||||
CLIENT = mt5.ORDER_REASON_CLIENT
|
||||
MOBILE = mt5.ORDER_REASON_MOBILE
|
||||
WEB = mt5.ORDER_REASON_WEB
|
||||
EXPERT = mt5.ORDER_REASON_EXPERT
|
||||
SL = mt5.ORDER_REASON_SL
|
||||
TP = mt5.ORDER_REASON_TP
|
||||
SO = mt5.ORDER_REASON_SO
|
||||
|
||||
|
||||
class SymbolChartMode(Repr, IntEnum):
|
||||
"""SYMBOL_CHART_MODE Enum. A symbol price chart can be based on Bid or Last prices. The price selected for symbol
|
||||
charts also affects the generation and display of bars in the terminal.
|
||||
Possible values of the SYMBOL_CHART_MODE property are described in this enum
|
||||
|
||||
Attributes:
|
||||
BID (int): Bars are based on Bid prices
|
||||
LAST (int): Bars are based on last prices
|
||||
"""
|
||||
__enum_name__ = "SYMBOL_CHART_MODE"
|
||||
BID = mt5.SYMBOL_CHART_MODE_BID
|
||||
LAST = mt5.SYMBOL_CHART_MODE_LAST
|
||||
|
||||
|
||||
class SymbolCalcMode(Repr, IntEnum):
|
||||
"""SYMBOL_CALC_MODE Enum. The SYMBOL_CALC_MODE enumeration is used for obtaining information about how the margin
|
||||
requirements for a symbol are calculated.
|
||||
|
||||
Attributes:
|
||||
FOREX (int): Forex mode - calculation of profit and margin for Forex
|
||||
FOREX_NO_LEVERAGE (int): Forex No Leverage mode – calculation of profit and margin for Forex symbols without
|
||||
taking into account the leverage
|
||||
FUTURES (int): Futures mode - calculation of margin and profit for futures
|
||||
CFD (int): CFD mode - calculation of margin and profit for CFD
|
||||
CFDINDEX (int): CFD index mode - calculation of margin and profit for CFD by indexes
|
||||
CFDLEVERAGE (int): CFD Leverage mode - calculation of margin and profit for CFD at leverage trading
|
||||
EXCH_STOCKS (int): Calculation of margin and profit for trading securities on a stock exchange
|
||||
EXCH_FUTURES (int): Calculation of margin and profit for trading futures contracts on a stock exchange
|
||||
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_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.
|
||||
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
|
||||
"""
|
||||
__enum_name__ = "SYMBOL_CALC_MODE"
|
||||
FOREX = mt5.SYMBOL_CALC_MODE_FOREX
|
||||
FOREX_NO_LEVERAGE = mt5.SYMBOL_CALC_MODE_FOREX_NO_LEVERAGE
|
||||
FUTURES = mt5.SYMBOL_CALC_MODE_FUTURES
|
||||
CFD = mt5.SYMBOL_CALC_MODE_CFD
|
||||
CFDINDEX = mt5.SYMBOL_CALC_MODE_CFDINDEX
|
||||
CFDLEVERAGE = mt5.SYMBOL_CALC_MODE_CFDLEVERAGE
|
||||
EXCH_STOCKS = mt5.SYMBOL_CALC_MODE_EXCH_STOCKS
|
||||
EXCH_FUTURES = mt5.SYMBOL_CALC_MODE_EXCH_FUTURES
|
||||
EXCH_OPTIONS = mt5.SYMBOL_CALC_MODE_EXCH_OPTIONS
|
||||
EXCH_OPTIONS_MARGIN = mt5.SYMBOL_CALC_MODE_EXCH_OPTIONS_MARGIN
|
||||
EXCH_BONDS = mt5.SYMBOL_CALC_MODE_EXCH_BONDS
|
||||
EXCH_STOCKS_MOEX = mt5.SYMBOL_CALC_MODE_EXCH_STOCKS_MOEX
|
||||
EXCH_BONDS_MOEX = mt5.SYMBOL_CALC_MODE_EXCH_BONDS_MOEX
|
||||
SERV_COLLATERAL = mt5.SYMBOL_CALC_MODE_SERV_COLLATERAL
|
||||
|
||||
|
||||
class SymbolTradeMode(Repr, IntEnum):
|
||||
"""SYMBOL_TRADE_MODE Enum. There are several symbol trading modes. Information about trading modes of a certain
|
||||
symbol is reflected in the values this enumeration
|
||||
|
||||
Attributes:
|
||||
DISABLED (int): Trade is disabled for the symbol
|
||||
LONGONLY (int): Allowed only long positions
|
||||
SHORTONLY (int): Allowed only short positions
|
||||
CLOSEONLY (int): Allowed only position close operations
|
||||
FULL (int): No trade restrictions
|
||||
"""
|
||||
|
||||
__enum_name__ = "SYMBOL_TRADE_MODE"
|
||||
DISABLED = mt5.SYMBOL_TRADE_MODE_DISABLED
|
||||
LONGONLY = mt5.SYMBOL_TRADE_MODE_LONGONLY
|
||||
SHORTONLY = mt5.SYMBOL_TRADE_MODE_SHORTONLY
|
||||
CLOSEONLY = mt5.SYMBOL_TRADE_MODE_CLOSEONLY
|
||||
FULL = mt5.SYMBOL_TRADE_MODE_FULL
|
||||
|
||||
|
||||
class SymbolTradeExecution(Repr, IntEnum):
|
||||
"""SYMBOL_TRADE_EXECUTION Enum. The modes, or execution policies, define the rules for cases when the price has
|
||||
changed or the requested volume cannot be completely fulfilled at the moment.
|
||||
|
||||
Attributes:
|
||||
REQUEST (int): Executing a market order at the price previously received from the broker. Prices for a certain
|
||||
market order are requested from the broker before the order is sent. Upon receiving the prices, order
|
||||
execution at the given price can be either confirmed or rejected.
|
||||
|
||||
INSTANT (int): Executing a market order at the specified price immediately. When sending a trade request to be
|
||||
executed, the platform automatically adds the current prices to the order.
|
||||
- If the broker accepts the price, the order is executed.
|
||||
- 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.
|
||||
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.
|
||||
"""
|
||||
|
||||
__enum_name__ = "SYMBOL_TRADE_EXECUTION"
|
||||
REQUEST = mt5.SYMBOL_TRADE_EXECUTION_REQUEST
|
||||
INSTANT = mt5.SYMBOL_TRADE_EXECUTION_INSTANT
|
||||
MARKET = mt5.SYMBOL_TRADE_EXECUTION_MARKET
|
||||
EXCHANGE = mt5.SYMBOL_TRADE_EXECUTION_EXCHANGE
|
||||
|
||||
|
||||
class SymbolSwapMode(Repr, IntEnum):
|
||||
"""SYMBOL_SWAP_MODE Enum. Methods of swap calculation at position transfer are specified in enumeration
|
||||
ENUM_SYMBOL_SWAP_MODE. The method of swap calculation determines the units of measure of the SYMBOL_SWAP_LONG and
|
||||
SYMBOL_SWAP_SHORT parameters. For example, if swaps are charged in the client deposit currency, then the values of
|
||||
those parameters are specified as an amount of money in the client deposit currency.
|
||||
|
||||
Attributes:
|
||||
DISABLED (int): Swaps disabled (no swaps)
|
||||
POINTS (int): Swaps are charged in points
|
||||
CURRENCY_SYMBOL (int): Swaps are charged in money in base currency of the symbol
|
||||
CURRENCY_MARGIN (int): Swaps are charged in money in margin currency of the symbol
|
||||
CURRENCY_DEPOSIT (int): Swaps are charged in money, in client deposit currency
|
||||
|
||||
INTEREST_CURRENT (int): Swaps are charged as the specified annual interest from the instrument price at
|
||||
calculation of swap (standard bank year is 360 days)
|
||||
|
||||
INTEREST_OPEN (int): Swaps are charged as the specified annual interest from the open price of position
|
||||
(standard bank year is 360 days)
|
||||
|
||||
REOPEN_CURRENT (int): Swaps are charged by reopening positions. At the end of a trading day the position is
|
||||
closed. Next day it is reopened by the close price +/- specified number of points
|
||||
(parameters SYMBOL_SWAP_LONG and SYMBOL_SWAP_SHORT)
|
||||
|
||||
REOPEN_BID (int): Swaps are charged by reopening positions. At the end of a trading day the position is closed.
|
||||
Next day it is reopened by the current Bid price +/- specified number of
|
||||
points (parameters SYMBOL_SWAP_LONG and SYMBOL_SWAP_SHORT)
|
||||
"""
|
||||
__enum_name__ = "SYMBOL_SWAP_MODE"
|
||||
DISABLED = mt5.SYMBOL_SWAP_MODE_DISABLED
|
||||
POINTS = mt5.SYMBOL_SWAP_MODE_POINTS
|
||||
CURRENCY_SYMBOL = mt5.SYMBOL_SWAP_MODE_CURRENCY_SYMBOL
|
||||
CURRENCY_MARGIN = mt5.SYMBOL_SWAP_MODE_CURRENCY_MARGIN
|
||||
CURRENCY_DEPOSIT = mt5.SYMBOL_SWAP_MODE_CURRENCY_DEPOSIT
|
||||
INTEREST_CURRENT = mt5.SYMBOL_SWAP_MODE_INTEREST_CURRENT
|
||||
INTEREST_OPEN = mt5.SYMBOL_SWAP_MODE_INTEREST_OPEN
|
||||
REOPEN_CURRENT = mt5.SYMBOL_SWAP_MODE_REOPEN_CURRENT
|
||||
REOPEN_BID = mt5.SYMBOL_SWAP_MODE_REOPEN_BID
|
||||
|
||||
|
||||
class DayOfWeek(Repr, IntEnum):
|
||||
"""DAY_OF_WEEK Enum.
|
||||
|
||||
Attributes:
|
||||
SUNDAY (int): Sunday
|
||||
MONDAY (int): Monday
|
||||
TUESDAY (int): Tuesday
|
||||
WEDNESDAY (int): Wednesday
|
||||
THURSDAY (int): Thursday
|
||||
FRIDAY (int): Friday
|
||||
SATURDAY (int): Saturday
|
||||
"""
|
||||
__enum__name__ = "DAY_OF_WEEK"
|
||||
SUNDAY = mt5.DAY_OF_WEEK_SUNDAY
|
||||
MONDAY = mt5.DAY_OF_WEEK_MONDAY
|
||||
TUESDAY = mt5.DAY_OF_WEEK_TUESDAY
|
||||
WEDNESDAY = mt5.DAY_OF_WEEK_WEDNESDAY
|
||||
THURSDAY = mt5.DAY_OF_WEEK_THURSDAY
|
||||
FRIDAY = mt5.DAY_OF_WEEK_FRIDAY
|
||||
SATURDAY = mt5.DAY_OF_WEEK_SATURDAY
|
||||
|
||||
|
||||
class SymbolOrderGTCMode(Repr, IntEnum):
|
||||
"""SYMBOL_ORDER_GTC_MODE Enum. If the SYMBOL_EXPIRATION_MODE property is set to SYMBOL_EXPIRATION_GTC
|
||||
(good till canceled), the expiration of pending orders, as well as of
|
||||
Stop Loss/Take Profit orders should be additionally set using the ENUM_SYMBOL_ORDER_GTC_MODE enumeration.
|
||||
|
||||
Attributes:
|
||||
GTC (int): Pending orders and Stop Loss/Take Profit levels are valid for an unlimited period
|
||||
until theirConstants, Enumerations and explicit cancellation
|
||||
|
||||
DAILY (int): Orders are valid during one trading day. At the end of the day, all Stop Loss and
|
||||
Take Profit levels, as well as pending orders are deleted.
|
||||
|
||||
DAILY_NO_STOPS (int): When a trade day changes, only pending orders are deleted,
|
||||
while Stop Loss and Take Profit levels are preserved
|
||||
"""
|
||||
__enum_name__ = "SYMBOL_ORDERS"
|
||||
GTC = mt5.SYMBOL_ORDERS_GTC
|
||||
DAILY = mt5.SYMBOL_ORDERS_DAILY
|
||||
DAILY_NO_STOPS = mt5.SYMBOL_ORDERS_DAILY_NO_STOPS
|
||||
|
||||
|
||||
class SymbolOptionRight(Repr, IntEnum):
|
||||
"""SYMBOL_OPTION_RIGHT Enum. An option is a contract, which gives the right, but not the obligation,
|
||||
to buy or sell an underlying asset (goods, stocks, futures, etc.) at a specified price on or before a specific date.
|
||||
The following enumerations describe option properties, including the option type and the right arising from it.
|
||||
|
||||
Attributes:
|
||||
CALL (int): A call option gives you the right to buy an asset at a specified price.
|
||||
PUT (int): A put option gives you the right to sell an asset at a specified price.
|
||||
"""
|
||||
__enum_name__ = "SYMBOL_OPTION_RIGHT"
|
||||
CALL = mt5.SYMBOL_OPTION_RIGHT_CALL
|
||||
PUT = mt5.SYMBOL_OPTION_RIGHT_PUT
|
||||
|
||||
|
||||
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)
|
||||
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.
|
||||
"""
|
||||
__enum_name__ = "SYMBOL_OPTION_MODE"
|
||||
EUROPEAN = mt5.SYMBOL_OPTION_MODE_EUROPEAN
|
||||
AMERICAN = mt5.SYMBOL_OPTION_MODE_AMERICAN
|
||||
|
||||
|
||||
class AccountTradeMode(Repr, IntEnum):
|
||||
"""ACCOUNT_TRADE_MODE Enum. There are several types of accounts that can be opened on a trade server.
|
||||
The type of account on which an MQL5 program is running can be found out using
|
||||
the ENUM_ACCOUNT_TRADE_MODE enumeration.
|
||||
|
||||
Attributes:
|
||||
DEMO: Demo account
|
||||
CONTEST: Contest account
|
||||
REAL: Real Account
|
||||
"""
|
||||
__enum_name__ = "ACCOUNT_TRADE_MODE"
|
||||
DEMO = mt5.ACCOUNT_TRADE_MODE_DEMO
|
||||
CONTEST = mt5.ACCOUNT_TRADE_MODE_CONTEST
|
||||
REAL = mt5.ACCOUNT_TRADE_MODE_REAL
|
||||
|
||||
|
||||
class TickFlag(Repr, IntFlag):
|
||||
"""TICK_FLAG Enum. TICK_FLAG defines possible flags for ticks. These flags are used to describe ticks obtained by the
|
||||
copy_ticks_from() and copy_ticks_range() functions.
|
||||
|
||||
Attributes:
|
||||
BID (int): Bid price changed
|
||||
ASK (int): Ask price changed
|
||||
LAST (int): Last price changed
|
||||
VOLUME (int): Volume changed
|
||||
BUY (int): last Buy price changed
|
||||
SELL (int): last Sell price changed
|
||||
"""
|
||||
__enum_name__ = "TICK_FLAG"
|
||||
BID = mt5.TICK_FLAG_BID
|
||||
ASK = mt5.TICK_FLAG_ASK
|
||||
LAST = mt5.TICK_FLAG_LAST
|
||||
VOLUME = mt5.TICK_FLAG_VOLUME
|
||||
BUY = mt5.TICK_FLAG_BUY
|
||||
SELL = mt5.TICK_FLAG_SELL
|
||||
|
||||
|
||||
class TradeRetcode(Repr, IntEnum):
|
||||
"""TRADE_RETCODE Enum. Return codes for order send/check operations
|
||||
|
||||
Attributes:
|
||||
REQUOTE (int): Requote
|
||||
REJECT (int): Request rejected
|
||||
CANCEL (int): Request canceled by trader
|
||||
PLACED (int): Order placed
|
||||
DONE (int): Request completed
|
||||
DONE_PARTIAL (int): Only part of the request was completed
|
||||
ERROR (int): Request processing error
|
||||
TIMEOUT (int): Request canceled by timeout
|
||||
INVALID (int): Invalid request
|
||||
INVALID_VOLUME (int): Invalid volume in the request
|
||||
INVALID_PRICE (int): Invalid price in the request
|
||||
INVALID_STOPS (int): Invalid stops in the request
|
||||
TRADE_DISABLED (int): Trade is disabled
|
||||
MARKET_CLOSED (int): Market is closed
|
||||
NO_MONEY (int): There is not enough money to complete the request
|
||||
PRICE_CHANGED (int): Prices changed
|
||||
PRICE_OFF (int): There are no quotes to process the request
|
||||
INVALID_EXPIRATION (int): Invalid order expiration date in the request
|
||||
ORDER_CHANGED (int): Order state changed
|
||||
TOO_MANY_REQUESTS (int): Too frequent requests
|
||||
NO_CHANGES (int): No changes in request
|
||||
SERVER_DISABLES_AT (int): Autotrading disabled by server
|
||||
CLIENT_DISABLES_AT (int): Autotrading disabled by client terminal
|
||||
LOCKED (int): Request locked for processing
|
||||
FROZEN (int): Order or position frozen
|
||||
INVALID_FILL (int): Invalid order filling type
|
||||
CONNECTION (int): No connection with the trade server
|
||||
ONLY_REAL (int): Operation is allowed only for live accounts
|
||||
LIMIT_ORDERS (int): The number of pending orders has reached the limit
|
||||
LIMIT_VOLUME (int): The volume of orders and positions for the symbol has reached the limit
|
||||
INVALID_ORDER (int): Incorrect or prohibited order type
|
||||
POSITION_CLOSED (int): Position with the specified POSITION_IDENTIFIER has already been closed
|
||||
INVALID_CLOSE_VOLUME (int): A close volume exceeds the current position volume
|
||||
|
||||
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 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
|
||||
|
||||
LIMIT_POSITIONS (int): The number of open positions simultaneously present on an account can be limited by the
|
||||
server settings.After a limit is reached, the server returns the TRADE_RETCODE_LIMIT_POSITIONS error when
|
||||
attempting to place an order. The limitation operates differently depending on the position accounting type:
|
||||
· Netting — number of open positions is considered. When a limit is reached, the platform does not let
|
||||
placing new orders whose execution may increase the number of open positions. In fact, the platform
|
||||
allows placing orders only for the symbols that already have open positions.
|
||||
The current pending orders are not considered since their execution may lead to changes in the current
|
||||
positions but it cannot increase their number.
|
||||
|
||||
· Hedging — pending orders are considered together with open positions, since a pending order activation
|
||||
always leads to opening a new position. When a limit is reached, the platform does not allow placing
|
||||
both new market orders for opening positions and pending orders.
|
||||
|
||||
REJECT_CANCEL (int): The pending order activation request is rejected, the order is canceled.
|
||||
LONG_ONLY (int): The request is rejected, because the "Only long positions are allowed" rule is set for the
|
||||
symbol (POSITION_TYPE_BUY)
|
||||
SHORT_ONLY (int): The request is rejected, because the "Only short positions are allowed" rule is set for the
|
||||
symbol (POSITION_TYPE_SELL)
|
||||
CLOSE_ONLY (int): The request is rejected, because the "Only position closing is allowed" rule is set for the
|
||||
symbol
|
||||
FIFO_CLOSE (int): The request is rejected, because "Position closing is allowed only by FIFO rule" flag is set
|
||||
for the trading account (ACCOUNT_FIFO_CLOSE=true)
|
||||
"""
|
||||
__enum_name__ = "TRADE_RETCODE"
|
||||
REQUOTE = mt5.TRADE_RETCODE_REQUOTE
|
||||
REJECT = mt5.TRADE_RETCODE_REJECT
|
||||
CANCEL = mt5.TRADE_RETCODE_CANCEL
|
||||
PLACED = mt5.TRADE_RETCODE_PLACED
|
||||
DONE = mt5.TRADE_RETCODE_DONE
|
||||
DONE_PARTIAL = mt5.TRADE_RETCODE_DONE_PARTIAL
|
||||
ERROR = mt5.TRADE_RETCODE_ERROR
|
||||
TIMEOUT = mt5.TRADE_RETCODE_TIMEOUT
|
||||
INVALID = mt5.TRADE_RETCODE_INVALID
|
||||
INVALID_VOLUME = mt5.TRADE_RETCODE_INVALID_VOLUME
|
||||
INVALID_PRICE = mt5.TRADE_RETCODE_INVALID_PRICE
|
||||
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
|
||||
PRICE_CHANGED = mt5.TRADE_RETCODE_PRICE_CHANGED
|
||||
PRICE_OFF = mt5.TRADE_RETCODE_PRICE_OFF
|
||||
INVALID_EXPIRATION = mt5.TRADE_RETCODE_INVALID_EXPIRATION
|
||||
ORDER_CHANGED = mt5.TRADE_RETCODE_ORDER_CHANGED
|
||||
TOO_MANY_REQUESTS = mt5.TRADE_RETCODE_TOO_MANY_REQUESTS
|
||||
NO_CHANGES = mt5.TRADE_RETCODE_NO_CHANGES
|
||||
SERVER_DISABLES_AT = mt5.TRADE_RETCODE_SERVER_DISABLES_AT
|
||||
CLIENT_DISABLES_AT = mt5.TRADE_RETCODE_CLIENT_DISABLES_AT
|
||||
LOCKED = mt5.TRADE_RETCODE_LOCKED
|
||||
FROZEN = mt5.TRADE_RETCODE_FROZEN
|
||||
INVALID_FILL = mt5.TRADE_RETCODE_INVALID_FILL
|
||||
CONNECTION = mt5.TRADE_RETCODE_CONNECTION
|
||||
ONLY_REAL = mt5.TRADE_RETCODE_ONLY_REAL
|
||||
LIMIT_ORDERS = mt5.TRADE_RETCODE_LIMIT_ORDERS
|
||||
LIMIT_VOLUME = mt5.TRADE_RETCODE_LIMIT_VOLUME
|
||||
INVALID_ORDER = mt5.TRADE_RETCODE_INVALID_ORDER
|
||||
POSITION_CLOSED = mt5.TRADE_RETCODE_POSITION_CLOSED
|
||||
INVALID_CLOSE_VOLUME = mt5.TRADE_RETCODE_INVALID_CLOSE_VOLUME
|
||||
CLOSE_ORDER_EXIST = mt5.TRADE_RETCODE_CLOSE_ORDER_EXIST
|
||||
LIMIT_POSITIONS = mt5.TRADE_RETCODE_LIMIT_POSITIONS
|
||||
REJECT_CANCEL = mt5.TRADE_RETCODE_REJECT_CANCEL
|
||||
LONG_ONLY = mt5.TRADE_RETCODE_LONG_ONLY
|
||||
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.
|
||||
|
||||
Attributes:
|
||||
PERCENT (int): Account stop out mode in percents
|
||||
MONEY (int): Account stop out mode in money
|
||||
"""
|
||||
|
||||
__enum_name__ = "ACCOUNT_STOPOUT_MODE"
|
||||
PERCENT = mt5.ACCOUNT_STOPOUT_MODE_PERCENT
|
||||
MONEY = mt5.ACCOUNT_STOPOUT_MODE_MONEY
|
||||
|
||||
|
||||
class AccountMarginMode(Repr, IntEnum):
|
||||
"""ACCOUNT_MARGIN_MODE Enum.
|
||||
|
||||
Attributes:
|
||||
RETAIL_NETTING (int): Used for the OTC markets to interpret positions in the "netting"
|
||||
mode (only one position can exist for one symbol). The margin is calculated based on the symbol
|
||||
type (SYMBOL_TRADE_CALC_MODE).
|
||||
|
||||
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
|
||||
(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
|
||||
@@ -0,0 +1,30 @@
|
||||
class Error:
|
||||
"""Error class for handling errors from MetaTrader 5."""
|
||||
descriptions = {
|
||||
# common errors
|
||||
1: 'Successful',
|
||||
-1: 'generic fail',
|
||||
-2: 'invalid arguments/parameters',
|
||||
-3: 'no memory condition',
|
||||
-4: 'no history',
|
||||
-5: 'invalid version',
|
||||
-6: 'authorization failed',
|
||||
-7: 'unsupported method',
|
||||
-8: 'auto-trading disabled',
|
||||
# internal errors
|
||||
-10000: 'internal IPC general error',
|
||||
-10001: 'internal IPC send failed',
|
||||
-10002: 'internal IPC recv failed',
|
||||
-10003: 'internal IPC initialization fail',
|
||||
-10004: 'internal IPC no ipc',
|
||||
-10005: 'internal timeout',
|
||||
}
|
||||
def __init__(self, code: int, description: str = ''):
|
||||
self.code = code
|
||||
self.description = description or self.descriptions.get(code, 'Unknown Error')
|
||||
|
||||
def __repr__(self):
|
||||
return f"""
|
||||
Error Code: {self.code}
|
||||
Error Description: {self.description}
|
||||
"""
|
||||
@@ -0,0 +1,19 @@
|
||||
"""Exceptions for the aiomql package."""
|
||||
|
||||
__all__ = ['LoginError', 'VolumeError', 'SymbolError', 'OrderError']
|
||||
|
||||
class LoginError(Exception):
|
||||
"""Raised when an error occurs when logging in."""
|
||||
pass
|
||||
|
||||
class VolumeError(Exception):
|
||||
"""Raised when a volume is not valid or out of range for a symbol."""
|
||||
pass
|
||||
|
||||
|
||||
class SymbolError(Exception):
|
||||
"""Raised when a symbol is not provided where required or not available in the Market Watch."""
|
||||
|
||||
|
||||
class OrderError(Exception):
|
||||
"""Raised when an error occurs when working with the order class."""
|
||||
@@ -0,0 +1,355 @@
|
||||
from datetime import datetime
|
||||
import asyncio
|
||||
from logging import getLogger
|
||||
from typing import Callable
|
||||
|
||||
import MetaTrader5
|
||||
|
||||
from MetaTrader5 import BookInfo, SymbolInfo, AccountInfo, Tick, TerminalInfo, TradeOrder, TradeDeal,\
|
||||
TradePosition, OrderSendResult, OrderCheckResult
|
||||
|
||||
from .constants import TimeFrame, CopyTicks, OrderType
|
||||
from .errors import Error
|
||||
from .config import Config
|
||||
|
||||
logger = getLogger()
|
||||
|
||||
|
||||
class BaseMeta(type):
|
||||
def __new__(mcs, cls_name, bases, cls_dict):
|
||||
defaults = MetaTrader5.__dict__
|
||||
defaults = {f'_{key}': value for key, value in defaults.items() if not key.startswith('_')}
|
||||
cls_dict |= defaults
|
||||
return super().__new__(mcs, cls_name, bases, cls_dict)
|
||||
|
||||
|
||||
class MetaTrader(metaclass=BaseMeta):
|
||||
_account_info: Callable
|
||||
_copy_rates_from: Callable
|
||||
_copy_rates_from_pos: Callable
|
||||
_copy_rates_range: Callable
|
||||
_copy_ticks_from: Callable
|
||||
_copy_ticks_range: Callable
|
||||
_history_deals_get: Callable
|
||||
_history_deals_total: Callable
|
||||
_history_orders_get: Callable
|
||||
_history_orders_total: Callable
|
||||
_initialize: Callable
|
||||
_last_error: Callable
|
||||
_login: Callable
|
||||
_market_book_add: Callable
|
||||
_market_book_get: Callable
|
||||
_market_book_release: Callable
|
||||
_order_calc_margin: Callable
|
||||
_order_calc_profit: Callable
|
||||
_order_check: Callable
|
||||
_order_send: Callable
|
||||
_orders_get: Callable
|
||||
_orders_total: Callable
|
||||
_positions_get: Callable
|
||||
_positions_total: Callable
|
||||
_shutdown: Callable
|
||||
_symbol_info: Callable
|
||||
_symbol_info_tick: Callable
|
||||
_symbol_select: Callable
|
||||
_symbols_get: Callable
|
||||
_symbols_total: Callable
|
||||
_terminal_info: Callable
|
||||
_version: Callable
|
||||
|
||||
async def __aenter__(self) -> 'MetaTrader':
|
||||
"""
|
||||
Async context manager entry point.
|
||||
Initializes the connection to the MetaTrader terminal.
|
||||
|
||||
Returns:
|
||||
MetaTrader: An instance of the MetaTrader class.
|
||||
"""
|
||||
await self.initialize(**Config().account_info())
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
||||
"""
|
||||
Async context manager exit point. Closes the connection to the MetaTrader terminal.
|
||||
"""
|
||||
await self.shutdown()
|
||||
|
||||
async def login(self, login: int, password: str, server: str, timeout: int = 60000) -> bool:
|
||||
"""
|
||||
Connects to the MetaTrader terminal using the specified login, password and server.
|
||||
|
||||
Args:
|
||||
login (int): The trading account number.
|
||||
password (str): The trading account password.
|
||||
server (str): The trading server name.
|
||||
timeout (int): The timeout for the connection in seconds.
|
||||
|
||||
Returns:
|
||||
bool: True if successful, False otherwise.
|
||||
"""
|
||||
return await asyncio.to_thread(self._login, login, password=password, server=server, timeout=timeout)
|
||||
|
||||
async def initialize(self, path: str = "", login: int = 0, password: str = "", server: str = "",
|
||||
timeout: int | None = None, portable=False) -> bool:
|
||||
"""
|
||||
Initializes the connection to the MetaTrader terminal. All parameters are optional.
|
||||
|
||||
Keyword Args:
|
||||
path (str): The path to the MetaTrader terminal executable.
|
||||
login (int): The trading account number.
|
||||
password (str): The trading account password.
|
||||
server (str): The trading server name.
|
||||
timeout (int): The timeout for the connection in seconds.
|
||||
portable (bool): If True, the terminal will be launched in portable mode.
|
||||
|
||||
Returns:
|
||||
bool: True if successful, False otherwise.
|
||||
"""
|
||||
args = (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)
|
||||
|
||||
async def shutdown(self) -> None:
|
||||
"""
|
||||
Closes the connection to the MetaTrader terminal.
|
||||
|
||||
Returns:
|
||||
None: None
|
||||
"""
|
||||
return await asyncio.to_thread(self._shutdown)
|
||||
|
||||
async def last_error(self) -> tuple[int, str]:
|
||||
return await asyncio.to_thread(self._last_error)
|
||||
|
||||
async def version(self) -> tuple[int, int, str] | None:
|
||||
""""""
|
||||
res = await asyncio.to_thread(self._version)
|
||||
if res is None:
|
||||
err = await self.last_error()
|
||||
logger.warning(f'Error in obtaining version information.{Error(*err)}')
|
||||
return res
|
||||
|
||||
async def account_info(self) -> AccountInfo | None:
|
||||
""""""
|
||||
res = await asyncio.to_thread(self._account_info)
|
||||
|
||||
if res is None:
|
||||
err = await self.last_error()
|
||||
logger.warning(f'Error in obtaining account information.{Error(*err)}')
|
||||
|
||||
return res
|
||||
|
||||
async def terminal_info(self) -> TerminalInfo | None:
|
||||
res = await asyncio.to_thread(self._terminal_info)
|
||||
|
||||
if res is None:
|
||||
err = await self.last_error()
|
||||
logger.warning(f'Error in obtaining terminal information.{Error(*err)}')
|
||||
return res
|
||||
|
||||
return res
|
||||
|
||||
async def symbols_total(self) -> int:
|
||||
return await asyncio.to_thread(self._symbols_total)
|
||||
|
||||
async def symbols_get(self, group: str = "") -> tuple[SymbolInfo] | None:
|
||||
kwargs = {'group': group} if group else {}
|
||||
res = await asyncio.to_thread(self._symbols_get, **kwargs)
|
||||
|
||||
if res is None:
|
||||
err = await self.last_error()
|
||||
logger.warning(f'Error in obtaining symbols.{Error(*err)}')
|
||||
return res
|
||||
|
||||
return res
|
||||
|
||||
async def symbol_info(self, symbol: str) -> SymbolInfo | None:
|
||||
res = await asyncio.to_thread(self._symbol_info, symbol)
|
||||
|
||||
if res is None:
|
||||
err = await self.last_error()
|
||||
logger.warning(f'Error in obtaining information for {symbol}.{Error(*err)}')
|
||||
return res
|
||||
|
||||
return res
|
||||
|
||||
async def symbol_info_tick(self, symbol: str) -> Tick | None:
|
||||
res = await asyncio.to_thread(self._symbol_info_tick, symbol)
|
||||
|
||||
if res is None:
|
||||
err = await self.last_error()
|
||||
logger.warning(f'Error in obtaining tick for {symbol}.{Error(*err)}')
|
||||
return res
|
||||
|
||||
return res
|
||||
|
||||
async def symbol_select(self, symbol: str, enable: bool) -> bool:
|
||||
return await asyncio.to_thread(self._symbol_select, symbol, enable)
|
||||
|
||||
async def market_book_add(self, symbol: str) -> bool:
|
||||
return await asyncio.to_thread(self._market_book_add, symbol)
|
||||
|
||||
async def market_book_get(self, symbol: str) -> tuple[BookInfo] | None:
|
||||
res = await asyncio.to_thread(self._market_book_get, symbol)
|
||||
|
||||
if res is None:
|
||||
err = await self.last_error()
|
||||
logger.warning(f'Error in obtaining market depth content for {symbol}.{Error(*err)}')
|
||||
return res
|
||||
|
||||
return res
|
||||
|
||||
async def market_book_release(self, symbol: str) -> bool:
|
||||
return await asyncio.to_thread(self._market_book_release, symbol)
|
||||
|
||||
async def copy_rates_from(self, symbol: str, timeframe: TimeFrame, date_from: datetime | int, count: int):
|
||||
res = await asyncio.to_thread(self._copy_rates_from, symbol, timeframe, date_from, count)
|
||||
|
||||
if res is None:
|
||||
err = await self.last_error()
|
||||
logger.warning(f'Error in obtaining rates for {symbol}.{Error(*err)}')
|
||||
return res
|
||||
|
||||
return res
|
||||
|
||||
async def copy_rates_from_pos(self, symbol: str, timeframe: TimeFrame, start_pos: int, count: int):
|
||||
res = await asyncio.to_thread(self._copy_rates_from_pos, symbol, timeframe, start_pos, count)
|
||||
|
||||
if res is None:
|
||||
err = await self.last_error()
|
||||
logger.warning(f'Error in obtaining rates for {symbol}.{Error(*err)}')
|
||||
return res
|
||||
|
||||
return res
|
||||
|
||||
async def copy_rates_range(self, symbol: str, timeframe: TimeFrame, date_from: datetime | int,
|
||||
date_to: datetime | int):
|
||||
res = await asyncio.to_thread(self._copy_rates_range, symbol, timeframe, date_from, date_to)
|
||||
|
||||
if res is None:
|
||||
err = await self.last_error()
|
||||
logger.warning(f'Error in obtaining rates for {symbol}.{Error(*err)}')
|
||||
return res
|
||||
|
||||
return res
|
||||
|
||||
async def copy_ticks_from(self, symbol: str, date_from: datetime | int, count: int, flags: CopyTicks):
|
||||
res = await asyncio.to_thread(self._copy_ticks_from, symbol, date_from, count, flags)
|
||||
|
||||
if res is None:
|
||||
err = await self.last_error()
|
||||
logger.warning(f'Error in obtaining ticks for {symbol}.{Error(*err)}')
|
||||
return res
|
||||
|
||||
return res
|
||||
|
||||
async def copy_ticks_range(self, symbol: str, date_from: datetime | int, date_to: datetime | int, 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()
|
||||
logger.warning(f'Error in obtaining ticks for {symbol}.{Error(*err)}')
|
||||
return res
|
||||
|
||||
return res
|
||||
|
||||
async def orders_total(self) -> int:
|
||||
return await asyncio.to_thread(self._orders_total)
|
||||
|
||||
async def orders_get(self, group: str = "", ticket: int = 0, symbol: str = "") -> tuple[TradeOrder] | None:
|
||||
"""Get active orders with the ability to filter by symbol or ticket. There are three call options.
|
||||
Call without parameters. Return active orders on all symbols
|
||||
|
||||
Keyword Args:
|
||||
symbol (str): Symbol name. Optional named parameter. If a symbol is specified, the ticket parameter is ignored.
|
||||
|
||||
group (str): The filter for arranging a group of necessary symbols. Optional named parameter. If the group is specified, the function
|
||||
returns only active orders meeting a specified criteria for a symbol name.
|
||||
|
||||
ticket (int): Order ticket (ORDER_TICKET). Optional named parameter.
|
||||
|
||||
Returns:
|
||||
list[TradeOrder]: A list of active trade orders as TradeOrder objects
|
||||
"""
|
||||
kwargs = {key: value for key, value in (('group', group), ('ticket', ticket), ('symbol', symbol)) if value}
|
||||
res = await asyncio.to_thread(self._orders_get, **kwargs)
|
||||
|
||||
if res is None:
|
||||
err = await self.last_error()
|
||||
logger.warning(f'Error in obtaining orders.{Error(*err)}')
|
||||
return res
|
||||
|
||||
return res
|
||||
|
||||
async def order_calc_margin(self, action: OrderType, symbol: str, volume: float, price: float) -> float | None:
|
||||
res = await asyncio.to_thread(self._order_calc_margin, action, symbol, volume, price)
|
||||
|
||||
if res is None:
|
||||
err = await self.last_error()
|
||||
logger.warning(f'Error in calculating margin.{Error(*err)}')
|
||||
return res
|
||||
|
||||
return res
|
||||
|
||||
async def order_calc_profit(self, action: OrderType, symbol: str, volume: float, price_open: float,
|
||||
price_close: float) -> float | None:
|
||||
res = await asyncio.to_thread(self._order_calc_profit, action, symbol, volume, price_open, price_close)
|
||||
|
||||
if res is None:
|
||||
err = await self.last_error()
|
||||
logger.warning(f'Error in calculating profit.{Error(*err)}')
|
||||
return res
|
||||
|
||||
return res
|
||||
|
||||
async def order_check(self, request: dict) -> OrderCheckResult:
|
||||
return await asyncio.to_thread(self._order_check, request)
|
||||
|
||||
async def order_send(self, request: dict) -> OrderSendResult:
|
||||
return await asyncio.to_thread(self._order_send, request)
|
||||
|
||||
async def positions_total(self) -> int:
|
||||
return await asyncio.to_thread(self._positions_total)
|
||||
|
||||
async def positions_get(self, group: str = "", ticket: int = 0, symbol: str = "") -> tuple[TradePosition] | None:
|
||||
kwargs = {key: value for key, value in (('group', group), ('ticket', ticket), ('symbol', symbol)) if value}
|
||||
res = await asyncio.to_thread(self._positions_get, **kwargs)
|
||||
|
||||
if res is None:
|
||||
err = await self.last_error()
|
||||
logger.warning(f'Error in obtaining open positions.{Error(*err)}')
|
||||
return res
|
||||
|
||||
return res
|
||||
|
||||
async def history_orders_total(self, date_from: datetime | int, date_to: datetime | int) -> int:
|
||||
return await asyncio.to_thread(self._history_orders_total, date_from, date_to)
|
||||
|
||||
async def history_orders_get(self, date_from: datetime | int = None, date_to: datetime | int = 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}
|
||||
res = await asyncio.to_thread(self._history_orders_get, **kwargs)
|
||||
|
||||
if res is None:
|
||||
err = await self.last_error()
|
||||
logger.warning(f'Error in getting orders.{Error(*err)}')
|
||||
return res
|
||||
|
||||
return res
|
||||
|
||||
async def history_deals_total(self, date_from: datetime | int, date_to: datetime | int) -> int:
|
||||
return await asyncio.to_thread(self._history_deals_total, date_from, date_to)
|
||||
|
||||
async def history_deals_get(self, date_from: datetime | int = None, date_to: datetime | int = None, group: str = '',
|
||||
ticket: int = 0, position: int = 0) -> tuple[TradeDeal] | None:
|
||||
kwargs = {key: value for key, value in (('date_from', date_from), ('date_to', date_to), ('group', group),
|
||||
('ticket', ticket), ('position', position)) if value}
|
||||
res = await asyncio.to_thread(self._history_deals_get, **kwargs)
|
||||
if res is None:
|
||||
err = await self.last_error()
|
||||
logger.warning(f'Error in getting deals.{Error(*err)}')
|
||||
return res
|
||||
|
||||
return res
|
||||
@@ -0,0 +1,613 @@
|
||||
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 .base import Base
|
||||
|
||||
"""
|
||||
This module contains data models used in this library.
|
||||
They are used as base classes to other classes having the same properties but with more methods.
|
||||
"""
|
||||
|
||||
|
||||
class AccountInfo(Base):
|
||||
"""Account Information Class.
|
||||
|
||||
Attributes:
|
||||
login: int
|
||||
password: str
|
||||
server: str
|
||||
trade_mode: AccountTradeMode
|
||||
balance: float
|
||||
leverage: float
|
||||
profit: float
|
||||
point: float
|
||||
amount: float = 0
|
||||
equity: float
|
||||
credit: float
|
||||
margin: float
|
||||
margin_level: float
|
||||
margin_free: float
|
||||
margin_mode: AccountMarginMode
|
||||
margin_so_mode: AccountStopoutMode
|
||||
margin_so_call: float
|
||||
margin_so_so: float
|
||||
margin_initial: float
|
||||
margin_maintenance: float
|
||||
fifo_close: bool
|
||||
limit_orders: float
|
||||
currency: str = "USD"
|
||||
trade_allowed: bool = True
|
||||
trade_expert: bool = True
|
||||
currency_digits: int
|
||||
assets: float
|
||||
liabilities: float
|
||||
commission_blocked: float
|
||||
name: str
|
||||
company: str
|
||||
"""
|
||||
login: int = 0
|
||||
password: str = ''
|
||||
server: str = ''
|
||||
trade_mode: AccountTradeMode
|
||||
balance: float
|
||||
leverage: float
|
||||
profit: float
|
||||
point: float
|
||||
amount: float = 0
|
||||
equity: float
|
||||
credit: float
|
||||
margin: float
|
||||
margin_level: float
|
||||
margin_free: float
|
||||
margin_mode: AccountMarginMode
|
||||
margin_so_mode: AccountStopOutMode
|
||||
margin_so_call: float
|
||||
margin_so_so: float
|
||||
margin_initial: float
|
||||
margin_maintenance: float
|
||||
fifo_close: bool
|
||||
limit_orders: float
|
||||
currency: str = "USD"
|
||||
trade_allowed: bool = True
|
||||
trade_expert: bool = True
|
||||
currency_digits: int
|
||||
assets: float
|
||||
liabilities: float
|
||||
commission_blocked: float
|
||||
name: str
|
||||
company: str
|
||||
|
||||
|
||||
class TerminalInfo(Base):
|
||||
"""Terminal information class. Holds information about the terminal.
|
||||
|
||||
Attributes:
|
||||
community_account: bool
|
||||
community_connection: bool
|
||||
connected: bool
|
||||
dlls_allowed: bool
|
||||
trade_allowed: bool
|
||||
tradeapi_disabled: bool
|
||||
email_enabled: bool
|
||||
ftp_enabled: bool
|
||||
notifications_enabled: bool
|
||||
mqid: bool
|
||||
build: int
|
||||
maxbars: int
|
||||
codepage: int
|
||||
ping_last: int
|
||||
community_balance: float
|
||||
retransmission: float
|
||||
company: str
|
||||
name: str
|
||||
language: str
|
||||
path: str
|
||||
data_path: str
|
||||
commondata_path: str
|
||||
"""
|
||||
community_account: bool
|
||||
community_connection: bool
|
||||
connected: bool
|
||||
dlls_allowed: bool
|
||||
trade_allowed: bool
|
||||
tradeapi_disabled: bool
|
||||
email_enabled: bool
|
||||
ftp_enabled: bool
|
||||
notifications_enabled: bool
|
||||
mqid: bool
|
||||
build: int
|
||||
maxbars: int
|
||||
codepage: int
|
||||
ping_last: int
|
||||
community_balance: float
|
||||
retransmission: float
|
||||
company: str
|
||||
name: str
|
||||
language: str
|
||||
path: str
|
||||
data_path: str
|
||||
commondata_path: str
|
||||
|
||||
|
||||
class SymbolInfo(Base):
|
||||
"""Symbol Information Class. Symbols are financial instruments available for trading in the MetaTrader 5 terminal.
|
||||
|
||||
Attributes:
|
||||
name: str
|
||||
custom: bool
|
||||
chart_mode: SymbolChartMode
|
||||
select: bool
|
||||
visible: bool
|
||||
session_deals: int
|
||||
session_buy_orders: int
|
||||
session_sell_orders: int
|
||||
volume: float
|
||||
volumehigh: float
|
||||
volumelow: float
|
||||
time: int
|
||||
digits: int
|
||||
spread: float
|
||||
spread_float: bool
|
||||
ticks_bookdepth: int
|
||||
trade_calc_mode: SymbolCalcMode
|
||||
trade_mode: SymbolTradeMode
|
||||
start_time: int
|
||||
expiration_time: int
|
||||
trade_stops_level: int
|
||||
trade_freeze_level: int
|
||||
trade_exemode: SymbolTradeExecution
|
||||
swap_mode: SymbolSwapMode
|
||||
swap_rollover3days: DayOfWeek
|
||||
margin_hedged_use_leg: bool
|
||||
expiration_mode: int
|
||||
filling_mode: int
|
||||
order_mode: int
|
||||
order_gtc_mode: SymbolOrderGTCMode
|
||||
option_mode: SymbolOptionMode
|
||||
option_right: SymbolOptionRight
|
||||
bid: float
|
||||
bidhigh: float
|
||||
bidlow: float
|
||||
ask: float
|
||||
askhigh: float
|
||||
asklow: float
|
||||
last: float
|
||||
lasthigh: float
|
||||
lastlow: float
|
||||
volume_real: float
|
||||
volumehigh_real: float
|
||||
volumelow_real: float
|
||||
option_strike: float
|
||||
point: float
|
||||
trade_tick_value: float
|
||||
trade_tick_value_profit: float
|
||||
trade_tick_value_loss: float
|
||||
trade_tick_size: float
|
||||
trade_contract_size: float
|
||||
trade_accrued_interest: float
|
||||
trade_face_value: float
|
||||
trade_liquidity_rate: float
|
||||
volume_min: float
|
||||
volume_max: float
|
||||
volume_step: float
|
||||
volume_limit: float
|
||||
swap_long: float
|
||||
swap_short: float
|
||||
margin_initial: float
|
||||
margin_maintenance: float
|
||||
session_volume: float
|
||||
session_turnover: float
|
||||
session_interest: float
|
||||
session_buy_orders_volume: float
|
||||
session_sell_orders_volume: float
|
||||
session_open: float
|
||||
session_close: float
|
||||
session_aw: float
|
||||
session_price_settlement: float
|
||||
session_price_limit_min: float
|
||||
session_price_limit_max: float
|
||||
margin_hedged: float
|
||||
price_change: float
|
||||
price_volatility: float
|
||||
price_theoretical: float
|
||||
price_greeks_delta: float
|
||||
price_greeks_theta: float
|
||||
price_greeks_gamma: float
|
||||
price_greeks_vega: float
|
||||
price_greeks_rho: float
|
||||
price_greeks_omega: float
|
||||
price_sensitivity: float
|
||||
basis: str
|
||||
category: str
|
||||
currency_base: str
|
||||
currency_profit: str
|
||||
currency_margin: Any
|
||||
bank: str
|
||||
description: str
|
||||
exchange: str
|
||||
formula: Any
|
||||
isin: Any
|
||||
name: str
|
||||
page: str
|
||||
path: str
|
||||
"""
|
||||
custom: bool
|
||||
chart_mode: SymbolChartMode
|
||||
select: bool
|
||||
visible: bool
|
||||
session_deals: int
|
||||
session_buy_orders: int
|
||||
session_sell_orders: int
|
||||
volume: float
|
||||
volumehigh: float
|
||||
volumelow: float
|
||||
time: int
|
||||
digits: int
|
||||
spread: float
|
||||
spread_float: bool
|
||||
ticks_bookdepth: int
|
||||
trade_calc_mode: SymbolCalcMode
|
||||
trade_mode: SymbolTradeMode
|
||||
start_time: int
|
||||
expiration_time: int
|
||||
trade_stops_level: int
|
||||
trade_freeze_level: int
|
||||
trade_exemode: SymbolTradeExecution
|
||||
swap_mode: SymbolSwapMode
|
||||
swap_rollover3days: DayOfWeek
|
||||
margin_hedged_use_leg: bool
|
||||
expiration_mode: int
|
||||
filling_mode: int
|
||||
order_mode: int
|
||||
order_gtc_mode: SymbolOrderGTCMode
|
||||
option_mode: SymbolOptionMode
|
||||
option_right: SymbolOptionRight
|
||||
bid: float
|
||||
bidhigh: float
|
||||
bidlow: float
|
||||
ask: float
|
||||
askhigh: float
|
||||
asklow: float
|
||||
last: float
|
||||
lasthigh: float
|
||||
lastlow: float
|
||||
volume_real: float
|
||||
volumehigh_real: float
|
||||
volumelow_real: float
|
||||
option_strike: float
|
||||
point: float
|
||||
trade_tick_value: float
|
||||
trade_tick_value_profit: float
|
||||
trade_tick_value_loss: float
|
||||
trade_tick_size: float
|
||||
trade_contract_size: float
|
||||
trade_accrued_interest: float
|
||||
trade_face_value: float
|
||||
trade_liquidity_rate: float
|
||||
volume_min: float
|
||||
volume_max: float
|
||||
volume_step: float
|
||||
volume_limit: float
|
||||
swap_long: float
|
||||
swap_short: float
|
||||
margin_initial: float
|
||||
margin_maintenance: float
|
||||
session_volume: float
|
||||
session_turnover: float
|
||||
session_interest: float
|
||||
session_buy_orders_volume: float
|
||||
session_sell_orders_volume: float
|
||||
session_open: float
|
||||
session_close: float
|
||||
session_aw: float
|
||||
session_price_settlement: float
|
||||
session_price_limit_min: float
|
||||
session_price_limit_max: float
|
||||
margin_hedged: float
|
||||
price_change: float
|
||||
price_volatility: float
|
||||
price_theoretical: float
|
||||
price_greeks_delta: float
|
||||
price_greeks_theta: float
|
||||
price_greeks_gamma: float
|
||||
price_greeks_vega: float
|
||||
price_greeks_rho: float
|
||||
price_greeks_omega: float
|
||||
price_sensitivity: float
|
||||
basis: str
|
||||
category: str
|
||||
currency_base: str
|
||||
currency_profit: str
|
||||
currency_margin: str
|
||||
bank: str
|
||||
description: str
|
||||
exchange: str
|
||||
formula: str
|
||||
isin: str
|
||||
name: str
|
||||
page: str
|
||||
path: str
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
if 'name' not in kwargs:
|
||||
raise AttributeError('Symbol Object Must be initialized with a name')
|
||||
self.name = kwargs.pop('name')
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def __repr__(self):
|
||||
return self.name
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def __eq__(self, other: "SymbolInfo"):
|
||||
return self.name == other.name
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.name)
|
||||
|
||||
|
||||
class BookInfo(Base):
|
||||
"""Book Information Class.
|
||||
|
||||
Attributes:
|
||||
type: BookType
|
||||
price: float
|
||||
volume: float
|
||||
volume_dbl: float
|
||||
"""
|
||||
type: BookType
|
||||
price: float
|
||||
volume: float
|
||||
volume_dbl: float
|
||||
|
||||
|
||||
class TradeOrder(Base):
|
||||
"""Trade Order Class.
|
||||
|
||||
Attributes:
|
||||
ticket: int
|
||||
time_setup: int
|
||||
time_setup_msc: int
|
||||
time_expiration: int
|
||||
time_done: int
|
||||
time_done_msc: int
|
||||
type: OrderType
|
||||
type_time: OrderTime
|
||||
type_filling: OrderFilling
|
||||
state: int
|
||||
magic: int
|
||||
position_id: int
|
||||
position_by_id: int
|
||||
reason: OrderReason
|
||||
volume_current: float
|
||||
volume_initial: float
|
||||
price_open: float
|
||||
sl: float
|
||||
tp: float
|
||||
price_current: float
|
||||
price_stoplimit: float
|
||||
symbol: str
|
||||
comment: str
|
||||
external_id: str
|
||||
"""
|
||||
ticket: int
|
||||
time_setup: int
|
||||
time_setup_msc: int
|
||||
time_expiration: int
|
||||
time_done: int
|
||||
time_done_msc: int
|
||||
type: OrderType
|
||||
type_time: OrderTime
|
||||
type_filling: OrderFilling
|
||||
state: int
|
||||
magic: int
|
||||
position_id: int
|
||||
position_by_id: int
|
||||
reason: OrderReason
|
||||
volume_current: float
|
||||
volume_initial: float
|
||||
price_open: float
|
||||
sl: float
|
||||
tp: float
|
||||
price_current: float
|
||||
price_stoplimit: float
|
||||
symbol: str
|
||||
comment: str
|
||||
external_id: str
|
||||
|
||||
|
||||
class TradeRequest(Base):
|
||||
"""Trade Request Class.
|
||||
|
||||
Attributes:
|
||||
action: TradeAction
|
||||
type: OrderType
|
||||
order: int
|
||||
symbol: str
|
||||
volume: float
|
||||
sl: float
|
||||
tp: float
|
||||
price: float
|
||||
deviation: float
|
||||
stop_limit: float
|
||||
type_time: OrderTime
|
||||
type_filling: OrderFilling
|
||||
expiration: int
|
||||
position: int
|
||||
position_by: int
|
||||
comment: str
|
||||
magic: int
|
||||
deviation: int
|
||||
comment: str
|
||||
"""
|
||||
action: TradeAction
|
||||
type: OrderType
|
||||
order: int
|
||||
symbol: str
|
||||
volume: float
|
||||
sl: float
|
||||
tp: float
|
||||
price: float
|
||||
deviation: float
|
||||
stop_limit: float
|
||||
type_time: OrderTime
|
||||
type_filling: OrderFilling
|
||||
expiration: int
|
||||
position: int
|
||||
position_by: int
|
||||
comment: str
|
||||
magic: int
|
||||
deviation: int
|
||||
comment: str
|
||||
|
||||
|
||||
class OrderCheckResult(Base):
|
||||
"""Order Check Result
|
||||
|
||||
Attributes:
|
||||
retcode: int
|
||||
balance: float
|
||||
equity: float
|
||||
profit: float
|
||||
margin: float
|
||||
margin_free: float
|
||||
margin_level: float
|
||||
comment: str
|
||||
request: TradeRequest
|
||||
"""
|
||||
retcode: int
|
||||
balance: float
|
||||
equity: float
|
||||
profit: float
|
||||
margin: float
|
||||
margin_free: float
|
||||
margin_level: float
|
||||
comment: str
|
||||
request: mt5.TradeRequest
|
||||
|
||||
|
||||
class OrderSendResult(Base):
|
||||
"""Order Send Result
|
||||
|
||||
Attributes:
|
||||
retcode: int
|
||||
deal: int
|
||||
order: int
|
||||
volume: float
|
||||
price: float
|
||||
bid: float
|
||||
ask: float
|
||||
comment: str
|
||||
request: TradeRequest
|
||||
request_id: int
|
||||
retcode_external: int
|
||||
profit: float
|
||||
"""
|
||||
retcode: int
|
||||
deal: int
|
||||
order: int
|
||||
volume: float
|
||||
price: float
|
||||
bid: float
|
||||
ask: float
|
||||
comment: str
|
||||
request: mt5.TradeRequest
|
||||
request_id: int
|
||||
retcode_external: int
|
||||
profit: float
|
||||
|
||||
|
||||
class TradePosition(Base):
|
||||
"""Trade Position
|
||||
|
||||
Attributes:
|
||||
ticket: int
|
||||
time: int
|
||||
time_msc: int
|
||||
time_update: int
|
||||
time_update_msc: int
|
||||
type: OrderType
|
||||
magic: float
|
||||
identifier: int
|
||||
reason: PositionReason
|
||||
volume: float
|
||||
price_open: float
|
||||
sl: float
|
||||
tp: float
|
||||
price_current: float
|
||||
swap: float
|
||||
profit: float
|
||||
symbol: str
|
||||
comment: str
|
||||
external_id: str
|
||||
"""
|
||||
ticket: int
|
||||
time: int
|
||||
time_msc: int
|
||||
time_update: int
|
||||
time_update_msc: int
|
||||
type: OrderType
|
||||
magic: float
|
||||
identifier: int
|
||||
reason: PositionReason
|
||||
volume: float
|
||||
price_open: float
|
||||
sl: float
|
||||
tp: float
|
||||
price_current: float
|
||||
swap: float
|
||||
profit: float
|
||||
symbol: str
|
||||
comment: str
|
||||
external_id: str
|
||||
|
||||
|
||||
class TradeDeal(Base):
|
||||
"""Trade Deal
|
||||
|
||||
Attributes:
|
||||
ticket: int
|
||||
order: int
|
||||
time: int
|
||||
time_msc: int
|
||||
type: DealType
|
||||
entry: DealEntry
|
||||
magic: int
|
||||
position_id: int
|
||||
reason: DealReason
|
||||
volume: float
|
||||
price: float
|
||||
commission: float
|
||||
swap: float
|
||||
profit: float
|
||||
fee: float
|
||||
sl: float
|
||||
tp: float
|
||||
symbol: str
|
||||
comment: str
|
||||
external_id: str
|
||||
"""
|
||||
ticket: int
|
||||
order: int
|
||||
time: int
|
||||
time_msc: int
|
||||
type: DealType
|
||||
entry: DealEntry
|
||||
magic: int
|
||||
position_id: int
|
||||
reason: DealReason
|
||||
volume: float
|
||||
price: float
|
||||
commission: float
|
||||
swap: float
|
||||
profit: float
|
||||
fee: float
|
||||
sl: float
|
||||
tp: float
|
||||
symbol: str
|
||||
comment: str
|
||||
external_id: str
|
||||
@@ -0,0 +1,87 @@
|
||||
import asyncio
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from typing import Sequence, Coroutine, Callable
|
||||
|
||||
from .strategy import Strategy
|
||||
|
||||
|
||||
class Executor:
|
||||
"""Executor class for running multiple strategies on multiple symbols concurrently.
|
||||
|
||||
Attributes:
|
||||
executor (ThreadPoolExecutor): The executor object.
|
||||
workers (list): List of strategies.
|
||||
coros (dict[Coroutine, dict]): A dictionary of coroutines to run in the executor
|
||||
funcs (dict[Callable, dict]): A dictionary of functions to run in the executor
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, bot=None):
|
||||
self.executor = ThreadPoolExecutor
|
||||
self.workers: list[type(Strategy)] = []
|
||||
self.coros: dict[Coroutine|Callable: dict] = {}
|
||||
self.funcs: dict[Callable: dict] = {}
|
||||
self.bot = bot
|
||||
|
||||
def add_func(self, func, kwargs):
|
||||
self.funcs[func] = kwargs | {'bot': self.bot}
|
||||
|
||||
def add_coro(self, coro, kwargs):
|
||||
self.coros[coro] = kwargs | {'bot': self.bot}
|
||||
|
||||
def add_workers(self, strategies: Sequence[type(Strategy)]):
|
||||
"""Add multiple strategies at once
|
||||
|
||||
Args:
|
||||
strategies (Sequence[Strategy]): A sequence of strategies.
|
||||
"""
|
||||
self.workers.extend(strategies)
|
||||
|
||||
def remove_workers(self):
|
||||
"""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]
|
||||
|
||||
def add_worker(self, strategy: type(Strategy)):
|
||||
"""Add a strategy instance to the list of workers
|
||||
|
||||
Args:
|
||||
strategy (Strategy): A strategy object
|
||||
"""
|
||||
self.workers.append(strategy)
|
||||
|
||||
@staticmethod
|
||||
def trade(strategy: type(Strategy)):
|
||||
"""Wraps the coroutine trade method of each strategy with 'asyncio.run'.
|
||||
|
||||
Args:
|
||||
strategy (Strategy): A strategy object
|
||||
"""
|
||||
asyncio.run(strategy.trade())
|
||||
|
||||
def run(self, func, kwargs: dict):
|
||||
"""
|
||||
Run a coroutine function
|
||||
|
||||
Args:
|
||||
func: The coroutine. A variadic function.
|
||||
kwargs: A dictionary of keyword arguments for the function
|
||||
"""
|
||||
asyncio.run(func(**kwargs))
|
||||
|
||||
async def execute(self, workers: int = 0):
|
||||
"""Run the strategies with a threadpool executor.
|
||||
|
||||
Args:
|
||||
workers: Number of workers to use in executor pool. Defaults to zero which uses all workers.
|
||||
|
||||
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.funcs), len(self.coros)])
|
||||
workers = max(workers, 5)
|
||||
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.coros.items()]
|
||||
[loop.run_in_executor(executor, func, kwargs) for func, kwargs in self.funcs.items()]
|
||||
@@ -0,0 +1,119 @@
|
||||
import asyncio
|
||||
from datetime import datetime
|
||||
from logging import getLogger
|
||||
|
||||
from .core.config import Config
|
||||
from .core.meta_trader import MetaTrader
|
||||
from .core.models import TradeDeal, TradeOrder
|
||||
|
||||
logger = getLogger(__name__)
|
||||
|
||||
|
||||
class History:
|
||||
"""The history class handles completed trade deals and trade orders in the trading history of an account.
|
||||
|
||||
Attributes:
|
||||
deals (list[TradeDeal]): Iterable of trade deals
|
||||
orders (list[TradeOrder]): Iterable of trade orders
|
||||
total_deals: Total number of deals
|
||||
total_orders (int): Total number orders
|
||||
group (str): Filter for selecting history by symbols.
|
||||
ticket (int): Filter for selecting history by ticket number
|
||||
position (int): Filter for selecting history deals by position
|
||||
initialized (bool): check if initial request has been sent to the terminal to get history.
|
||||
mt5 (MetaTrader): MetaTrader instance
|
||||
config (Config): Config instance
|
||||
"""
|
||||
mt5: MetaTrader = MetaTrader()
|
||||
config: Config = Config()
|
||||
|
||||
def __init__(self, *, date_from: datetime | float = None, date_to: datetime | float = None,
|
||||
group: str = "", ticket: int = 0, position: int = 0):
|
||||
"""
|
||||
Args:
|
||||
date_from (datetime, float): Date the orders are requested from. Set by the 'datetime' object or as a
|
||||
number of seconds elapsed since 1970.01.01. Defaults to twenty-four hours from the current time in 'utc'
|
||||
|
||||
date_to (datetime, float): Date up to which the orders are requested. Set by the 'datetime' object or as a
|
||||
number of seconds elapsed since 1970.01.01. Defaults to the current time in "utc"
|
||||
|
||||
group (str): Filter for selecting history by symbols.
|
||||
ticket (int): Filter for selecting history by ticket number
|
||||
position (int): Filter for selecting history deals by position
|
||||
"""
|
||||
self.date_from = date_from
|
||||
self.date_to = date_to
|
||||
self.group = group
|
||||
self.ticket = ticket
|
||||
self.position = position
|
||||
self.deals: list[TradeDeal] = []
|
||||
self.orders: list[TradeOrder] = []
|
||||
self.total_deals: int = 0
|
||||
self.total_orders: int = 0
|
||||
self.initialized = False
|
||||
|
||||
async def init(self, deals=True, orders=True) -> bool:
|
||||
"""Get history deals and orders
|
||||
|
||||
Keyword Args:
|
||||
deals (bool): If true get history deals during initial request to terminal
|
||||
orders (bool): If true get history orders during initial request to terminal
|
||||
|
||||
Returns:
|
||||
bool: True if all requests were successful else False
|
||||
"""
|
||||
tasks = []
|
||||
tasks.append(self.get_deals()) if deals else ...
|
||||
tasks.append(self.get_orders()) if orders else ...
|
||||
res = await asyncio.gather(*tasks)
|
||||
self.initialized = all(res)
|
||||
return self.initialized
|
||||
|
||||
async def get_deals(self) -> list[TradeDeal]:
|
||||
"""Get deals from trading history using the parameters set in the constructor.
|
||||
|
||||
Returns:
|
||||
list[TradeDeal]: A list of trade deals
|
||||
"""
|
||||
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 not None:
|
||||
self.deals = [TradeDeal(**deal._asdict()) for deal in deals] if deals else []
|
||||
self.total_deals = len(self.deals)
|
||||
return self.deals
|
||||
|
||||
return self.deals
|
||||
|
||||
async def deals_total(self) -> int:
|
||||
"""Get total number of deals within the specified period in the constructor.
|
||||
|
||||
Returns:
|
||||
int: Total number of Deals
|
||||
"""
|
||||
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]:
|
||||
"""Get orders from trading history using the parameters set in the constructor.
|
||||
|
||||
Returns:
|
||||
list[TradeOrder]: A list of trade orders
|
||||
"""
|
||||
|
||||
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:
|
||||
return self.orders
|
||||
|
||||
self.orders = [TradeOrder(**order._asdict()) for order in orders]
|
||||
self.total_orders = len(self.orders)
|
||||
return self.orders
|
||||
|
||||
async def orders_total(self) -> int:
|
||||
"""Get total number of orders within the specified period in the constructor.
|
||||
|
||||
Returns:
|
||||
int: Total number of orders
|
||||
"""
|
||||
self.total_orders = await self.mt5.history_orders_total(self.date_from, self.date_to)
|
||||
return self.total_orders
|
||||
@@ -0,0 +1,112 @@
|
||||
"""Order Class"""
|
||||
from logging import getLogger
|
||||
|
||||
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
|
||||
logger = getLogger(__name__)
|
||||
|
||||
|
||||
class Order(TradeRequest):
|
||||
"""Trade order related functions and properties. Subclass of TradeRequest."""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""Initialize the order object with keyword arguments, symbol must be provided.
|
||||
Provide default values for action, type_time and type_filling if not provided.
|
||||
|
||||
Args:
|
||||
**kwargs: Keyword arguments must match the attributes of TradeRequest as well as the attributes of
|
||||
Order class as specified in the annotations in the class definition.
|
||||
|
||||
Default Values:
|
||||
action (TradeAction.DEAL): Trade action
|
||||
type_time (OrderTime.DAY): Order time
|
||||
type_filling (OrderFilling.FOK): Order filling
|
||||
|
||||
Raises:
|
||||
SymbolError: If symbol is not provided
|
||||
"""
|
||||
if 'symbol' not in kwargs:
|
||||
raise SymbolError('symbol is required')
|
||||
sym = kwargs.pop('symbol')
|
||||
self.symbol = sym.name if isinstance(sym, Symbol) else sym
|
||||
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)
|
||||
|
||||
async def orders_total(self):
|
||||
"""Get the number of active orders.
|
||||
|
||||
Returns:
|
||||
(int): total number of active orders
|
||||
"""
|
||||
return await self.mt5.orders_total()
|
||||
|
||||
async def orders(self) -> tuple[TradeOrder]:
|
||||
"""Get the list of active orders for the current symbol.
|
||||
|
||||
Returns:
|
||||
tuple[TradeOrder]: A Tuple of active trade orders as TradeOrder objects
|
||||
"""
|
||||
orders = await self.mt5.orders_get(symbol=self.symbol)
|
||||
orders = (TradeOrder(**order._asdict()) for order in orders)
|
||||
return tuple(orders)
|
||||
|
||||
async def check(self) -> OrderCheckResult:
|
||||
"""Check funds sufficiency for performing a required trading operation and the possibility to execute it at
|
||||
|
||||
Returns:
|
||||
OrderCheckResult: An OrderCheckResult object
|
||||
|
||||
Raises:
|
||||
OrderError: If not successful
|
||||
"""
|
||||
res = await self.mt5.order_check(self.dict)
|
||||
if res is None:
|
||||
raise OrderError(f'Failed to check order {self.symbol} {self.type} {self.volume} {self.price} {res}')
|
||||
return OrderCheckResult(**res._asdict())
|
||||
|
||||
async def send(self) -> OrderSendResult:
|
||||
"""Send a request to perform a trading operation from the terminal to the trade server.
|
||||
|
||||
Returns:
|
||||
OrderSendResult: An OrderSendResult object
|
||||
|
||||
Raises:
|
||||
OrderError: If not successful
|
||||
"""
|
||||
res = await self.mt5.order_send(self.dict)
|
||||
if res is None:
|
||||
raise OrderError(f'Failed to send order {self.symbol} {self.type} {self.volume} {self.price} {res}')
|
||||
return OrderSendResult(**res._asdict())
|
||||
|
||||
async def calc_margin(self) -> float:
|
||||
"""Return the required margin in the account currency to perform a specified trading operation.
|
||||
|
||||
Returns:
|
||||
float: Returns float value if successful
|
||||
|
||||
Raises:
|
||||
OrderError: If not successful
|
||||
"""
|
||||
res = await self.mt5.order_calc_margin(self.type, self.symbol, self.volume, self.price)
|
||||
if res is None:
|
||||
raise OrderError(f'Failed to calculate margin for {self.symbol} {self.type} {self.volume} {self.price} {res}')
|
||||
return res
|
||||
|
||||
async def calc_profit(self) -> float:
|
||||
"""Return profit in the account currency for a specified trading operation.
|
||||
|
||||
Returns:
|
||||
float: Returns float value if successful
|
||||
|
||||
Raises:
|
||||
OrderError: If not successful
|
||||
"""
|
||||
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} {self.type} {self.volume} {self.price} {self.tp}')
|
||||
return res
|
||||
@@ -0,0 +1,82 @@
|
||||
"""Handle Open positions."""
|
||||
import asyncio
|
||||
from logging import getLogger
|
||||
|
||||
from .core import MetaTrader, TradePosition, TradeAction, OrderType
|
||||
from .order import Order
|
||||
|
||||
logger = getLogger(__name__)
|
||||
|
||||
class Positions:
|
||||
"""Get Open Positions.
|
||||
|
||||
Attributes:
|
||||
symbol (str): Financial instrument name.
|
||||
group (str): The filter for arranging a group of necessary symbols. Optional named parameter.
|
||||
If the group is specified, the function returns only positions meeting a specified criteria for a symbol name.
|
||||
ticket (int): Position ticket.
|
||||
mt5 (MetaTrader): MetaTrader instance.
|
||||
"""
|
||||
mt5: MetaTrader = MetaTrader()
|
||||
|
||||
def __init__(self, *, symbol: str = "", group: str = "", ticket: int = 0):
|
||||
"""Get Open Positions.
|
||||
|
||||
Keyword Args:
|
||||
symbol (str): Financial instrument name.
|
||||
group (str): The filter for arranging a group of necessary symbols. Optional named parameter. If the group
|
||||
is specified, the function returns only positions meeting a specified criteria for a symbol name.
|
||||
ticket (int): Position ticket
|
||||
|
||||
"""
|
||||
self.symbol = symbol
|
||||
self.group = group
|
||||
self.ticket = ticket
|
||||
|
||||
async def positions_total(self) -> int:
|
||||
"""Get the number of open positions.
|
||||
|
||||
Returns:
|
||||
int: Return total number of open positions
|
||||
"""
|
||||
return await self.mt5.positions_total()
|
||||
|
||||
async def positions_get(self, symbol: str = '', group: str = '', ticket: int = 0):
|
||||
"""Get open positions with the ability to filter by symbol or ticket.
|
||||
|
||||
Keyword Args:
|
||||
symbol (str): Financial instrument name.
|
||||
group (str): The filter for arranging a group of necessary symbols. Optional named parameter. If the group
|
||||
is specified, the function returns only positions meeting a specified criteria for a symbol name.
|
||||
ticket (int): Position ticket
|
||||
|
||||
Returns:
|
||||
list[TradePosition]: A list of open trade positions
|
||||
"""
|
||||
symbol = symbol or self.symbol
|
||||
group = group or self.group
|
||||
ticket = ticket or self.ticket
|
||||
positions = await self.mt5.positions_get(group=group, symbol=symbol, ticket=ticket)
|
||||
if not positions:
|
||||
return []
|
||||
return [TradePosition(**pos._asdict()) for pos in positions]
|
||||
|
||||
async def close_all(self, symbol: str = '', group: str = '') -> int:
|
||||
"""Close all open positions for the trading account.
|
||||
|
||||
Returns:
|
||||
int: Return number of positions closed.
|
||||
"""
|
||||
orders = [Order(action=TradeAction.DEAL, price=pos.price_current, position=pos.ticket,
|
||||
type=OrderType(pos.type).opposite,
|
||||
**pos.get_dict(include={'symbol', 'volume'})) for pos in
|
||||
(await self.positions_get(symbol=symbol, group=group))]
|
||||
|
||||
results = await asyncio.gather(*[order.send() for order in orders], return_exceptions=True)
|
||||
amount_closed = len([res for res in results if res.retcode == 10009])
|
||||
pos = await self.positions_total()
|
||||
if pos > 0:
|
||||
logger.warning(f'Failed to close {pos} positions')
|
||||
else:
|
||||
logger.info('All positions closed')
|
||||
return amount_closed
|
||||
@@ -0,0 +1,64 @@
|
||||
"""Risk Assessment and Management"""
|
||||
from .account import Account
|
||||
from .symbol import Symbol
|
||||
|
||||
|
||||
class RAM:
|
||||
account: Account = Account()
|
||||
risk_to_reward: float
|
||||
risk: float
|
||||
amount: float
|
||||
pips: float
|
||||
volume: float
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""Risk Assessment and Management. All provided keyword arguments are set as attributes.
|
||||
|
||||
Args:
|
||||
kwargs (Dict): Keyword arguments.
|
||||
|
||||
Defaults:
|
||||
risk_to_reward (float): Risk to reward ratio 1
|
||||
risk (float): Percentage of account balance to risk per trade 0.01 # 1%
|
||||
amount (float): Amount to risk per trade in terms of account currency 0
|
||||
pips (float): Target pips 0
|
||||
volume (float): Volume to trade 0
|
||||
"""
|
||||
self.risk_to_reward = kwargs.pop('risk_to_reward', 1)
|
||||
self.risk = kwargs.pop('risk', 0.01)
|
||||
self.amount = kwargs.pop('amount', 0)
|
||||
self.pips = kwargs.pop('pips', 0)
|
||||
self.volume = kwargs.pop('volume', 0)
|
||||
[setattr(self, key, value) for key, value in kwargs.items()]
|
||||
|
||||
async def get_amount(self, risk: float = 0) -> float:
|
||||
"""Calculate the amount to risk per trade as a percentage of free margin.
|
||||
|
||||
Keyword Args:
|
||||
risk (float): Percentage of account balance to risk per trade. Defaults to zero.
|
||||
|
||||
Returns:
|
||||
float: Amount to risk per trade
|
||||
"""
|
||||
await self.account.refresh()
|
||||
risk = risk or self.risk
|
||||
return self.account.margin_free * risk
|
||||
|
||||
async def get_volume(self, *, symbol: Symbol, pips: float = 0, amount: float = 0) -> float:
|
||||
"""Calculate the volume to trade. if pips is not provided, the pips attribute is used.
|
||||
If the amount attribute or amount argument is zero, the amount is calculated using the get_amount method based on the risk.
|
||||
|
||||
Args:
|
||||
symbol (Symbol): Financial instrument
|
||||
|
||||
Keyword Args:
|
||||
pips (float): Target pips. Defaults to zero.
|
||||
amount (float): Amount to risk per trade. Defaults to zero.
|
||||
|
||||
Returns:
|
||||
float: Volume to trade
|
||||
"""
|
||||
pips = pips or self.pips
|
||||
amount = amount or self.amount or await self.get_amount()
|
||||
return await symbol.compute_volume(amount=amount, pips=pips)
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
"""This module contains the Records class, which is used to read and update trade records from csv files."""
|
||||
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
import csv
|
||||
|
||||
from .history import History
|
||||
from .core import Config
|
||||
|
||||
|
||||
class Records:
|
||||
"""This utility class read trade records from csv files, and update them based on their closing positions.
|
||||
|
||||
Attributes:
|
||||
config: Config object
|
||||
records_dir(Path): Path to directory containing record of placed trades, If not given takes the default
|
||||
from the config
|
||||
"""
|
||||
config: Config = Config()
|
||||
|
||||
def __init__(self, records_dir: Path = ''):
|
||||
"""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.
|
||||
"""
|
||||
self.records_dir = records_dir or self.config.records_dir
|
||||
|
||||
async def get_records(self):
|
||||
"""Get trade records from records_dir folder
|
||||
|
||||
Yields:
|
||||
files: Trade record files
|
||||
"""
|
||||
for file in self.records_dir.iterdir():
|
||||
if file.is_file() and file.name.endswith('.csv'):
|
||||
yield file
|
||||
|
||||
async def read_update(self, file: Path):
|
||||
"""Read and update trade records
|
||||
|
||||
Args:
|
||||
file: Trade record file
|
||||
"""
|
||||
fr = open(file, mode='r', newline='')
|
||||
reader = csv.DictReader(fr)
|
||||
rows = [row for row in reader]
|
||||
rows = await self.update_rows(rows)
|
||||
fr.close()
|
||||
fw = open(file, mode='w', newline='')
|
||||
writer = csv.DictWriter(fw, fieldnames=reader.fieldnames)
|
||||
writer.writeheader()
|
||||
writer.writerows(rows)
|
||||
fw.close()
|
||||
|
||||
async def update_rows(self, rows: list[dict]) -> list[dict]:
|
||||
"""Update the rows of entered trades in the csv file with the actual profit.
|
||||
|
||||
Args:
|
||||
rows: A list of dictionaries from the dictionary writer object of the csv file.
|
||||
|
||||
Returns:
|
||||
list[dict]: A list of dictionaries with the actual profit and win status.
|
||||
"""
|
||||
tasks = [History(position=int(row['order'])).get_deals() for row in rows]
|
||||
deals = [deal for deals in await asyncio.gather(*tasks) for deal in deals]
|
||||
deals = {str(deal.position_id): deal.profit for deal in deals if deal.order != deal.position_id}
|
||||
[row.update(actual_profit=(profit := deals[order]), win=profit > 0) for row in rows if (order := row['order']) in deals]
|
||||
return rows
|
||||
|
||||
async def update_records(self):
|
||||
"""Update trade records in the records_dir folder."""
|
||||
records = [self.read_update(record) async for record in self.get_records()]
|
||||
await asyncio.gather(*records)
|
||||
|
||||
async def update_record(self, file: Path | str):
|
||||
"""Update a single trade record file."""
|
||||
await self.read_update(file)
|
||||
@@ -0,0 +1,58 @@
|
||||
import asyncio
|
||||
import csv
|
||||
from logging import getLogger
|
||||
|
||||
from .core import Config
|
||||
from .core.models import OrderSendResult
|
||||
|
||||
logger = getLogger(__name__)
|
||||
|
||||
|
||||
class Result:
|
||||
"""A base class for handling trade results and strategy parameters for record keeping and reference purpose.
|
||||
The data property must be implemented in the subclass
|
||||
|
||||
Attributes:
|
||||
config (Config): The configuration object
|
||||
name: Any desired name for the result file object
|
||||
"""
|
||||
config = Config()
|
||||
data: dict
|
||||
|
||||
def __init__(self, result: OrderSendResult, parameters: dict = None, name: str = ''):
|
||||
"""
|
||||
Prepare result data
|
||||
Args:
|
||||
result:
|
||||
parameters:
|
||||
name:
|
||||
"""
|
||||
self.parameters = parameters or {}
|
||||
self.result = result
|
||||
self.name = name or parameters.get('name', 'Strategy')
|
||||
|
||||
def get_data(self) -> dict:
|
||||
result = self.result.get_dict(exclude={'retcode', 'retcode_external', 'request_id', 'request'})
|
||||
return self.parameters | result | {'actual_profit': 0, 'closed': False, 'win': False}
|
||||
|
||||
def to_csv(self):
|
||||
"""Record trade results and associated parameters as a csv file
|
||||
"""
|
||||
try:
|
||||
self.data = self.get_data()
|
||||
file = self.config.records_dir / f"{self.name}.csv"
|
||||
exists = file.exists()
|
||||
with open(file, 'a', newline='') as fh:
|
||||
writer = csv.DictWriter(fh, fieldnames=sorted(list(self.data.keys())), extrasaction='ignore', restval=None)
|
||||
if not exists:
|
||||
writer.writeheader()
|
||||
writer.writerow(self.data)
|
||||
except Exception as err:
|
||||
logger.error(f'Error: {err}. Unable to save trade results')
|
||||
|
||||
async def save_csv(self):
|
||||
"""Save trade results and associated parameters as a csv file in a separate thread
|
||||
"""
|
||||
# exe = self.config.executor
|
||||
loop = asyncio.get_running_loop()
|
||||
loop.run_in_executor(None, self.to_csv)
|
||||
@@ -0,0 +1,60 @@
|
||||
from datetime import time, timedelta, datetime
|
||||
from asyncio import sleep
|
||||
|
||||
|
||||
class Session:
|
||||
|
||||
def __init__(self, start: int | time, end: int | time):
|
||||
self.start = start if isinstance(start, time) else time(hour=start)
|
||||
self.end = end if isinstance(end, time) else time(hour=end)
|
||||
self.__from = self.delta(self.start)
|
||||
self.__to = self.delta(self.end)
|
||||
|
||||
def __contains__(self, item: time):
|
||||
return self.start <= item < self.end
|
||||
|
||||
def delta(self, obj):
|
||||
return timedelta(hours=obj.hour, minutes=obj.minute, seconds=obj.second, microseconds=obj.microsecond)
|
||||
|
||||
def __len__(self):
|
||||
return (self.__to - self.__from).seconds
|
||||
|
||||
def until(self):
|
||||
now = lambda: datetime.utcnow().time()
|
||||
return (self.__from - self.delta(now())).seconds
|
||||
|
||||
|
||||
class Sessions:
|
||||
def __init__(self, *sessions: Session):
|
||||
self.sessions = list(sessions)
|
||||
self.sessions.sort(key=lambda x: x.start)
|
||||
|
||||
def find(self, obj):
|
||||
for session in self.sessions:
|
||||
if obj in session:
|
||||
return session
|
||||
return None
|
||||
|
||||
def find_next(self, obj):
|
||||
for session in self.sessions:
|
||||
if obj < session.start:
|
||||
return session
|
||||
return self.sessions[-1]
|
||||
|
||||
def __contains__(self, item: time):
|
||||
return True if self.find(item) is not None else False
|
||||
|
||||
async def __aenter__(self):
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
||||
pass
|
||||
|
||||
async def check(self):
|
||||
now = datetime.utcnow().time()
|
||||
if now in self:
|
||||
return
|
||||
next_session = self.find_next(now)
|
||||
secs = next_session.until()
|
||||
print(f'sleeping for {secs} seconds')
|
||||
await sleep(secs)
|
||||
@@ -0,0 +1,73 @@
|
||||
"""The base class for creating strategies."""
|
||||
import asyncio
|
||||
from time import time
|
||||
from typing import TypeVar
|
||||
from abc import ABC, abstractmethod
|
||||
from datetime import time as dtime
|
||||
|
||||
from .core.meta_trader import MetaTrader
|
||||
from .symbol import Symbol as _Symbol
|
||||
from .account import Account
|
||||
from .core import Config
|
||||
from .sessions import Sessions, Session
|
||||
|
||||
Symbol = TypeVar('Symbol', bound=_Symbol)
|
||||
|
||||
|
||||
class Strategy(ABC):
|
||||
"""The base class for creating strategies.
|
||||
|
||||
Attributes:
|
||||
symbol (Symbol): The Financial Instrument as a Symbol Object
|
||||
parameters (Dict): A dictionary of parameters for the strategy.
|
||||
|
||||
Class Attributes:
|
||||
name (str): A name for the strategy.
|
||||
account (Account): Account instance.
|
||||
mt5 (MetaTrader): MetaTrader instance.
|
||||
config (Config): Config instance.
|
||||
|
||||
Notes:
|
||||
Define the name of a strategy as a class attribute. If not provided, the class name will be used as the name.
|
||||
"""
|
||||
name: str = ''
|
||||
account = Account()
|
||||
mt5: MetaTrader()
|
||||
config = Config()
|
||||
|
||||
def __init__(self, *, symbol: Symbol, params: dict = None, session: Session):
|
||||
"""Initiate the parameters dict and add name and symbol fields.
|
||||
Use class name as strategy name if name is not provided
|
||||
|
||||
Args:
|
||||
symbol (Symbol): The Financial instrument
|
||||
params (Dict): Trading strategy parameters
|
||||
"""
|
||||
self.symbol = symbol
|
||||
self.parameters = params.copy() if isinstance(params, dict) else {}
|
||||
self.parameters['symbol'] = symbol.name
|
||||
self.parameters['name'] = self.name or self.__class__.__name__
|
||||
self.session = session or Sessions(Session(8, 13))
|
||||
|
||||
def __repr__(self):
|
||||
return f"{self.name}({self.symbol!r})"
|
||||
|
||||
@staticmethod
|
||||
async def sleep(secs: float):
|
||||
"""Sleep for the needed amount of seconds in between requests to the terminal.
|
||||
computes the accurate amount of time needed to sleep ensuring that the next request is made at the start of
|
||||
a new bar and making cooperative multitasking possible.
|
||||
|
||||
Args:
|
||||
secs (float): The time in seconds. Usually the timeframe you are trading on.
|
||||
"""
|
||||
mod = time() % secs
|
||||
secs = secs - mod if mod != 0 else mod
|
||||
await asyncio.sleep(secs + 0.1)
|
||||
|
||||
|
||||
@abstractmethod
|
||||
async def trade(self):
|
||||
"""Place trades using this method. This is the main method of the strategy.
|
||||
It will be called by the strategy runner.
|
||||
"""
|
||||
@@ -0,0 +1,301 @@
|
||||
"""Symbol class for handling a financial instrument."""
|
||||
from datetime import datetime
|
||||
from logging import getLogger
|
||||
|
||||
from .core.constants import TimeFrame, CopyTicks
|
||||
from .core.models import SymbolInfo, BookInfo
|
||||
from .ticks import Tick
|
||||
from .account import Account
|
||||
from .candle import Candles
|
||||
from .ticks import Ticks
|
||||
|
||||
logger = getLogger(__name__)
|
||||
|
||||
|
||||
class Symbol(SymbolInfo):
|
||||
"""Main class for handling a financial instrument. A subclass of SymbolInfo and Base it has attributes and methods
|
||||
for working with a financial instrument.
|
||||
|
||||
Attributes:
|
||||
tick (Tick): Price tick object for instrument
|
||||
account: An instance of the current trading account
|
||||
|
||||
Notes:
|
||||
Full properties are on the SymbolInfo Object.
|
||||
Make sure Symbol is always initialized with a name argument
|
||||
"""
|
||||
tick: Tick
|
||||
account = Account()
|
||||
|
||||
@property
|
||||
def pip(self):
|
||||
"""Returns the pip value of the symbol. This is ten times the point value for forex symbols.
|
||||
|
||||
Returns:
|
||||
float: The pip value of the symbol.
|
||||
"""
|
||||
return self.point * 10
|
||||
|
||||
async def info_tick(self, *, name: str = "") -> Tick:
|
||||
"""Get the current price tick of a financial instrument.
|
||||
|
||||
Args:
|
||||
name: if name is supplied get price tick of that financial instrument
|
||||
|
||||
Returns:
|
||||
Tick: Return a Tick Object
|
||||
|
||||
Raises:
|
||||
ValueError: If request was unsuccessful and None was returned
|
||||
"""
|
||||
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
|
||||
|
||||
async def symbol_select(self, *, enable: bool = True) -> bool:
|
||||
"""Select a symbol in the MarketWatch window or remove a symbol from the window.
|
||||
Update the select property
|
||||
|
||||
Args:
|
||||
enable (bool): Switch. Optional unnamed parameter. If 'false', a symbol should be removed from
|
||||
the MarketWatch window.
|
||||
|
||||
Returns:
|
||||
bool: True if successful, otherwise – False.
|
||||
"""
|
||||
self.select = await self.mt5.symbol_select(self.name, enable)
|
||||
return self.select
|
||||
|
||||
async def info(self) -> SymbolInfo:
|
||||
"""Get data on the specified financial instrument and update the symbol object properties
|
||||
|
||||
Returns:
|
||||
(SymbolInfo): SymbolInfo if successful
|
||||
|
||||
Raises:
|
||||
ValueError: If request was unsuccessful and None was returned
|
||||
"""
|
||||
|
||||
info = await self.mt5.symbol_info(self.name)
|
||||
if info:
|
||||
self.set_attributes(**info._asdict())
|
||||
return SymbolInfo(**info._asdict())
|
||||
raise ValueError(f'Could not get info for {self.name}')
|
||||
|
||||
async def init(self) -> bool:
|
||||
"""Initialized the symbol by pulling properties from the terminal
|
||||
|
||||
Returns:
|
||||
bool: Returns True if symbol info was successful initialized
|
||||
"""
|
||||
try:
|
||||
if await self.symbol_select():
|
||||
await self.book_add()
|
||||
await self.info()
|
||||
return True
|
||||
logger.warning(f'Unable to initialized symbol {self}')
|
||||
return False
|
||||
except Exception as err:
|
||||
self.select = False
|
||||
logger.warning(err)
|
||||
return False
|
||||
|
||||
async def book_add(self) -> bool:
|
||||
"""Subscribes the MetaTrader 5 terminal to the Market Depth change events for a specified symbol.
|
||||
If the symbol is not in the list of instruments for the market, This method will return False
|
||||
|
||||
Returns:
|
||||
bool: True if successful, otherwise – False.
|
||||
"""
|
||||
return await self.mt5.market_book_add(self.name)
|
||||
|
||||
async def book_get(self) -> tuple[BookInfo]:
|
||||
"""Returns a tuple of BookInfo featuring Market Depth entries for the specified symbol.
|
||||
|
||||
Returns:
|
||||
tuple[BookInfo]: Returns the Market Depth contents as a tuples of BookInfo Objects
|
||||
|
||||
Raises:
|
||||
ValueError: If request was unsuccessful and None was returned
|
||||
"""
|
||||
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)
|
||||
|
||||
async def book_release(self) -> bool:
|
||||
"""Cancels subscription of the MetaTrader 5 terminal to the Market Depth change events for a specified symbol.
|
||||
|
||||
Returns:
|
||||
bool: True if successful, otherwise – False.
|
||||
"""
|
||||
return await self.mt5.market_book_release(self.name)
|
||||
|
||||
async def compute_volume(self, *, amount: float, pips: float, use_minimum: bool = True) -> float:
|
||||
"""Computes the volume of a trade based on the amount and the number of pips to target.
|
||||
This is a dummy method that returns the minimum volume of the symbol. It is meant to be overridden by a subclass
|
||||
Checkout Forex Symbol implementation in src\aiomql\lib\ForexSymbol.py
|
||||
|
||||
Args:
|
||||
amount (float): Amount to risk in the trade
|
||||
pips (float): Number of pips to target
|
||||
|
||||
Keyword Args:
|
||||
use_minimum (bool): If True, the minimum volume is returned if the computed volume is less than the minimum volume.
|
||||
|
||||
Returns:
|
||||
float: Returns the volume of the trade
|
||||
"""
|
||||
return self.volume_min
|
||||
|
||||
async def currency_conversion(self, *, amount: float, base: str, quote: str) -> float:
|
||||
"""Convert from one currency to the other.
|
||||
|
||||
Args:
|
||||
amount: amount to convert given in terms of the quote currency
|
||||
base: The base currency of the pair
|
||||
quote: The quote currency of the pair
|
||||
|
||||
Returns:
|
||||
float: Amount in terms of the base currency or None if it failed to convert
|
||||
|
||||
Raises:
|
||||
ValueError: If conversion is impossible
|
||||
"""
|
||||
try:
|
||||
pair = f'{base}{quote}'
|
||||
if self.account.has_symbol(pair):
|
||||
tick = await self.info_tick(name=pair)
|
||||
if tick is not None:
|
||||
return amount / tick.ask
|
||||
|
||||
pair = f'{quote}{base}'
|
||||
if self.account.has_symbol(pair):
|
||||
tick = await self.info_tick(name=pair)
|
||||
if tick is not None:
|
||||
amount = amount * tick.bid
|
||||
return amount
|
||||
except Exception as err:
|
||||
logger.warning(f'Currency conversion failed: Unable to convert {amount} in {quote} to {base}')
|
||||
raise ValueError(f'Currency Conversion Failed: {err}')
|
||||
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:
|
||||
"""
|
||||
Get bars from the MetaTrader 5 terminal starting from the specified date.
|
||||
|
||||
Args:
|
||||
timeframe (TimeFrame): Timeframe the bars are requested for. Set by a value from the TimeFrame enumeration. Required unnamed parameter.
|
||||
|
||||
date_from (datetime | int): Date of opening of the first bar from the requested sample. Set by the 'datetime' object or as a number
|
||||
of seconds elapsed since 1970.01.01. Required unnamed parameter.
|
||||
|
||||
count (int): Number of bars to receive. Required unnamed parameter.
|
||||
|
||||
Returns:
|
||||
Candles: Returns a Candles object as a collection of rates ordered chronologically
|
||||
|
||||
Raises:
|
||||
ValueError: If request was unsuccessful and None was returned
|
||||
"""
|
||||
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}')
|
||||
|
||||
async def copy_rates_from_pos(self, *,timeframe: TimeFrame, count: int = 500, start_position: int = 0) -> Candles:
|
||||
"""Get bars from the MetaTrader 5 terminal starting from the specified index.
|
||||
|
||||
Args:
|
||||
timeframe (TimeFrame): TimeFrame value from TimeFrame Enum. Required keyword only parameter
|
||||
|
||||
count (int): Number of bars to return. Keyword argument defaults to 500
|
||||
|
||||
start_position (int): Initial index of the bar the data are requested from. The numbering of bars goes from
|
||||
present to past. Thus, the zero bar means the current one. Keyword argument defaults to 0.
|
||||
|
||||
Returns:
|
||||
Candles: Returns a Candles object as a collection of rates ordered chronologically.
|
||||
|
||||
Raises:
|
||||
ValueError: If request was unsuccessful and None was returned
|
||||
"""
|
||||
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}')
|
||||
|
||||
async def copy_rates_range(self, *, timeframe: TimeFrame, date_from: datetime | int,
|
||||
date_to: datetime | int) -> 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_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.
|
||||
|
||||
Raises:
|
||||
ValueError: If request was unsuccessful and None was returned
|
||||
"""
|
||||
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}')
|
||||
|
||||
async def copy_ticks_from(self, *, date_from: datetime | int, count: int = 100, flags: CopyTicks = CopyTicks.ALL) -> Ticks:
|
||||
"""
|
||||
Get ticks from the MetaTrader 5 terminal starting from the specified date.
|
||||
|
||||
Args: date_from (datetime | int): Date the ticks are requested from. Set by the 'datetime' object or as a
|
||||
number of seconds elapsed since 1970.01.01.
|
||||
|
||||
count (int): Number of requested ticks. Defaults to 100
|
||||
|
||||
flags (CopyTicks): A flag to define the type of the requested ticks from CopyTicks enum. INFO is the default
|
||||
|
||||
Returns:
|
||||
Candles: Returns a Candles object as a collection of ticks ordered chronologically.
|
||||
|
||||
Raises:
|
||||
ValueError: If request was unsuccessful and None was returned
|
||||
"""
|
||||
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}')
|
||||
|
||||
async def copy_ticks_range(self, *, date_from: datetime | int, date_to: datetime | int, flags: CopyTicks = CopyTicks.ALL) -> 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_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):
|
||||
|
||||
Returns:
|
||||
Candles: Returns a Candles object as a collection of ticks ordered chronologically.
|
||||
|
||||
Raises:
|
||||
ValueError: If request was unsuccessful and None was returned.
|
||||
"""
|
||||
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}')
|
||||
@@ -0,0 +1,70 @@
|
||||
"""Terminal related functions and properties"""
|
||||
|
||||
from typing import NamedTuple
|
||||
from logging import getLogger
|
||||
from .core.models import TerminalInfo
|
||||
|
||||
logger = getLogger()
|
||||
|
||||
|
||||
class Terminal(TerminalInfo):
|
||||
"""Terminal Class. Get information about the MetaTrader 5 terminal. The class is a subclass of the TerminalInfo
|
||||
class. It inherits all the attributes and methods of the TerminalInfo class and adds some useful methods.
|
||||
|
||||
Notes:
|
||||
Other attributes are defined in the TerminalInfo Class
|
||||
"""
|
||||
|
||||
Version = NamedTuple("Version", (('version', str), ('build', int), ('release_date', str)))
|
||||
|
||||
async def initialize(self) -> bool:
|
||||
"""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.
|
||||
|
||||
Returns:
|
||||
bool: True if successful else False
|
||||
"""
|
||||
self.connected = await self.mt5.initialize(**self.config.account_info())
|
||||
|
||||
if not self.connected:
|
||||
err = await self.mt5.last_error()
|
||||
logger.critical(f'Failed to initialize Terminal. Error Code: {err}')
|
||||
raise SystemExit
|
||||
return self.connected
|
||||
|
||||
async def version(self):
|
||||
"""Get the MetaTrader 5 terminal version. This method returns the terminal version, build and release date as
|
||||
a tuple of three values
|
||||
|
||||
Returns:
|
||||
Version: version of tuple as Version object
|
||||
|
||||
Raises:
|
||||
ValueError: If the terminal version cannot be obtained
|
||||
"""
|
||||
res = await self.mt5.version()
|
||||
if res is None:
|
||||
raise ValueError('Failed to get terminal version')
|
||||
return self.Version(*res)
|
||||
|
||||
async def info(self):
|
||||
"""Get the connected MetaTrader 5 client terminal status and settings. gets terminal info in the form of a
|
||||
named tuple structure (namedtuple). Return None in case of an error. The info on the error can be
|
||||
obtained using last_error().
|
||||
|
||||
Returns:
|
||||
Terminal: Terminal status and settings as a terminal object.
|
||||
"""
|
||||
info = await self.mt5.terminal_info()
|
||||
self.set_attributes(**info._asdict())
|
||||
return self
|
||||
|
||||
async def symbols_total(self) -> int:
|
||||
"""Get the number of all financial instruments in the MetaTrader 5 terminal.
|
||||
|
||||
Returns:
|
||||
int: Total number of available symbols
|
||||
"""
|
||||
return await self.mt5.symbols_total()
|
||||
@@ -0,0 +1,167 @@
|
||||
"""Module for working with price ticks."""
|
||||
|
||||
from typing import TypeVar, Iterable
|
||||
import reprlib
|
||||
|
||||
from pandas import DataFrame, Series
|
||||
import pandas_ta as ta
|
||||
|
||||
from .core.constants import TickFlag
|
||||
|
||||
|
||||
Self = TypeVar('Self', bound='Ticks')
|
||||
|
||||
|
||||
class Tick:
|
||||
"""Price Tick of a Financial Instrument.
|
||||
|
||||
Attributes:
|
||||
time (int): Time of the last prices update for the symbol
|
||||
bid (float): Current Bid price
|
||||
ask (float): Current Ask price
|
||||
last (float): Price of the last deal (Last)
|
||||
volume (float): Volume for the current Last price
|
||||
time_msc (int): Time of the last prices update for the symbol in milliseconds
|
||||
flags (TickFlag): Tick flags
|
||||
volume_real (float): Volume for the current Last price
|
||||
Index (int): Custom attribute representing the position of the tick in a sequence.
|
||||
"""
|
||||
time: float
|
||||
bid: float
|
||||
ask: float
|
||||
last: float
|
||||
volume: float
|
||||
time_msc:float
|
||||
flags: float
|
||||
volume_real:float
|
||||
Index: int
|
||||
def __init__(self, **kwargs):
|
||||
self.time = kwargs.pop('time', 0)
|
||||
self.Index = kwargs.pop('Index', 0)
|
||||
self.set_attributes(**kwargs)
|
||||
|
||||
def __repr__(self):
|
||||
keys = reprlib.repr(', '.join('%s=%s' % (i, j) for i, j in self.__dict__.items()))[1:-1]
|
||||
return '%(class)s(%(args)s)' % {'class': self.__class__.__name__, 'args': keys}
|
||||
|
||||
|
||||
def set_attributes(self, **kwargs):
|
||||
"""Set attributes from keyword arguments"""
|
||||
for key, value in kwargs.items():
|
||||
setattr(self, key, value)
|
||||
|
||||
_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
|
||||
"""
|
||||
time: Series
|
||||
bid: Series
|
||||
ask: Series
|
||||
last: Series
|
||||
volume: Series
|
||||
time_msc: Series
|
||||
flags: Series
|
||||
volume_real: Series
|
||||
Index: Series
|
||||
|
||||
def __init__(self, *, data: DataFrame | Iterable, flip=False):
|
||||
"""Initialize the Ticks class. Creates a DataFrame of price ticks from the data argument.
|
||||
|
||||
Args:
|
||||
data (DataFrame | Iterable): Dataframe of price ticks or any iterable object that can be converted to a
|
||||
pandas DataFrame
|
||||
flip (bool): If flip is True reverse data chronological order.
|
||||
"""
|
||||
if isinstance(data, DataFrame):
|
||||
data = data
|
||||
elif isinstance(data, type(self)):
|
||||
data = DataFrame(data.data)
|
||||
elif isinstance(data, Iterable):
|
||||
data = DataFrame(data)
|
||||
else:
|
||||
raise ValueError(f'Cannot create DataFrame from object of {type(data)}')
|
||||
self._data = data.iloc[::-1] if flip else data
|
||||
|
||||
def __repr__(self):
|
||||
return self._data.__repr__()
|
||||
|
||||
def __len__(self):
|
||||
return self._data.shape[0]
|
||||
|
||||
def __contains__(self, item: Tick) -> bool:
|
||||
return item.time_msc == self[item.Index].time_msc
|
||||
|
||||
def __getattr__(self, item):
|
||||
if item in list(self._data.columns.values):
|
||||
return self._data[item]
|
||||
raise AttributeError(f'Attribute {item} not defined on class {self.__class__.__name__}')
|
||||
|
||||
def __getitem__(self, index) -> Tick | Self:
|
||||
if isinstance(index, slice):
|
||||
cls = self.__class__
|
||||
data = self._data.iloc[index]
|
||||
data.reset_index(drop=True, inplace=True)
|
||||
return cls(data=data)
|
||||
|
||||
if isinstance(index, str):
|
||||
return self._data[index]
|
||||
|
||||
item = self._data.iloc[index]
|
||||
return Tick(Index=index, **item)
|
||||
|
||||
def __setitem__(self, index, value: Series):
|
||||
if isinstance(value, Series):
|
||||
self._data[index] = value
|
||||
return
|
||||
raise TypeError(f'Expected Series got {type(value)}')
|
||||
|
||||
def __iter__(self):
|
||||
return (Tick(**row._asdict()) for row in self._data.itertuples())
|
||||
|
||||
@property
|
||||
def ta(self):
|
||||
"""Access to the pandas_ta library for performing technical analysis on the underlying data attribute.
|
||||
|
||||
Returns:
|
||||
pandas_ta: The pandas_ta library
|
||||
"""
|
||||
return self._data.ta
|
||||
|
||||
@property
|
||||
def ta_lib(self):
|
||||
"""Access to the ta library for performing technical analysis. Not dependent on the underlying data attribute.
|
||||
|
||||
Returns:
|
||||
ta: The ta library
|
||||
"""
|
||||
return ta
|
||||
|
||||
@property
|
||||
def data(self) -> DataFrame:
|
||||
"""DataFrame of price ticks arranged in chronological order."""
|
||||
return self._data
|
||||
|
||||
def rename(self, inplace=True, **kwargs) -> _Ticks | None :
|
||||
"""Rename columns of the candle class.
|
||||
|
||||
Keyword Args:
|
||||
inplace (bool): Rename the columns inplace or return a new instance of the class with the renamed columns
|
||||
**kwargs: The new names of the columns
|
||||
|
||||
Returns:
|
||||
Ticks: A new instance of the class with the renamed columns if inplace is False.
|
||||
None: If inplace is True
|
||||
"""
|
||||
res = self._data.rename(columns=kwargs, inplace=inplace)
|
||||
return res if inplace else self.__class__(data=res)
|
||||
@@ -0,0 +1,121 @@
|
||||
"""Trader class module. Handles the creation of an order and the placing of trades"""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import TypeVar
|
||||
from logging import getLogger
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
from .order import Order
|
||||
from .symbol import Symbol as _Symbol
|
||||
from .ram import RAM
|
||||
from .core.models import OrderType
|
||||
from .core.config import Config
|
||||
from .utils import dict_to_string
|
||||
from .result import Result
|
||||
|
||||
logger = getLogger(__name__)
|
||||
Symbol = TypeVar('Symbol', bound=_Symbol)
|
||||
|
||||
|
||||
class Trader:
|
||||
"""Base class for creating a Trader object. Handles the creation of an order and the placing of trades
|
||||
|
||||
Attributes:
|
||||
symbol (Symbol): Financial instrument class Symbol class or any subclass of it.
|
||||
ram (RAM): RAM instance
|
||||
order (Order): Trade order
|
||||
|
||||
Class Attributes:
|
||||
name (str): A name for the strategy.
|
||||
account (Account): Account instance.
|
||||
mt5 (MetaTrader): MetaTrader instance.
|
||||
config (Config): Config instance.
|
||||
"""
|
||||
config = Config()
|
||||
|
||||
def __init__(self, *, symbol: Symbol, ram: RAM = None):
|
||||
"""Initializes the order object and RAM instance
|
||||
|
||||
Args:
|
||||
symbol (Symbol): Financial instrument
|
||||
ram (RAM): Risk Assessment and Management instance
|
||||
"""
|
||||
self.symbol = symbol
|
||||
self.order = Order(symbol=symbol.name)
|
||||
self.ram = ram or RAM()
|
||||
|
||||
async def create_order(self, *, order_type: OrderType, **kwargs):
|
||||
"""Complete the order object with the required values. Creates a simple order.
|
||||
Uses the ram instance to set the volume.
|
||||
|
||||
Args:
|
||||
order_type (OrderType): Type of order
|
||||
kwargs: keyword arguments as required for the specific trader
|
||||
"""
|
||||
# check if pips is passed in as a keyword argument, if not use the pips attribute of the ram instance
|
||||
pips = kwargs.get('pips', 0) or self.ram.pips
|
||||
self.order.volume = self.ram.volume or await self.ram.get_volume(symbol=self.symbol, pips=pips)
|
||||
self.order.type = order_type
|
||||
await self.set_order_limits(pips=pips)
|
||||
|
||||
async def set_order_limits(self, pips: float):
|
||||
"""Sets the stop loss and take profit for the order.
|
||||
This method uses pips as defined for forex instruments.
|
||||
|
||||
Args:
|
||||
pips: Target pips
|
||||
"""
|
||||
# use passed in pips and the pip value of the symbol to calculate the stop loss and take profit.
|
||||
# this is sure to work for forex instruments.
|
||||
pips = pips * self.symbol.pip
|
||||
sl, tp = pips, pips * self.ram.risk_to_reward
|
||||
tick = await self.symbol.info_tick()
|
||||
if self.order.type == OrderType.BUY:
|
||||
self.order.sl, self.order.tp = tick.ask - sl, tick.ask + tp
|
||||
self.order.price = tick.ask
|
||||
else:
|
||||
self.order.sl, self.order.tp = tick.bid + sl, tick.bid - tp
|
||||
self.order.price = tick.bid
|
||||
|
||||
async def place_trade(self, order_type: OrderType, params: dict = None, **kwargs):
|
||||
"""Places a trade based on the order_type.
|
||||
|
||||
Args:
|
||||
order_type (OrderType): Type of order
|
||||
params: parameters to be saved with the trade
|
||||
kwargs: keyword arguments as required for the specific trader
|
||||
"""
|
||||
try:
|
||||
await self.create_order(order_type=order_type, **kwargs)
|
||||
|
||||
# Check the order before placing it
|
||||
check = await self.order.check()
|
||||
if check.retcode != 0:
|
||||
logger.warning(
|
||||
f"Symbol: {self.order.symbol}\nResult:\n{dict_to_string(check.get_dict(include={'comment', 'retcode'}), multi=True)}")
|
||||
return
|
||||
|
||||
# check expected profit
|
||||
profit = await self.order.calc_profit()
|
||||
|
||||
# Send the order.
|
||||
result = await self.order.send()
|
||||
if result.retcode != 10009:
|
||||
logger.warning(
|
||||
f"Symbol: {self.order.symbol}\nResult:\n{dict_to_string(result.get_dict(include={'comment', 'retcode'}), multi=True)}")
|
||||
return
|
||||
|
||||
logger.info(f"Symbol: {self.order.symbol}\nOrder: {dict_to_string(result.dict, multi=True)}\n")
|
||||
|
||||
# save trade result and passed in parameters
|
||||
if result.retcode == 10009 and self.config.record_trades:
|
||||
params = params or {}
|
||||
params['expected_profit'] = profit
|
||||
date = datetime.utcnow()
|
||||
date = date.replace(tzinfo=ZoneInfo('UTC'))
|
||||
params['date'] = date
|
||||
params['time'] = date.timestamp()
|
||||
res = Result(result=result, parameters=params)
|
||||
await res.save_csv()
|
||||
except Exception as err:
|
||||
logger.error(f"{err}. Symbol: {self.order.symbol}\n {self.__class__.__name__}.place_trade")
|
||||
@@ -0,0 +1,13 @@
|
||||
"""Utility functions for aiomql."""
|
||||
def dict_to_string(data: dict, multi=False) -> str:
|
||||
"""Convert a dict to a string. Use 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 False.
|
||||
|
||||
Returns:
|
||||
str: The string representation of the dict.
|
||||
"""
|
||||
sep = '\n' if multi else ', '
|
||||
return f"{sep}".join(f"{key}: {value}\n" for key, value in data.items())
|
||||
Reference in New Issue
Block a user