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