mirror of
https://github.com/Ichinga-Samuel/aiomql.git
synced 2026-08-22 00:08:08 +00:00
v3.21
This commit is contained in:
@@ -6,6 +6,7 @@ from .strategy import Strategy
|
||||
from .bot_builder import Bot
|
||||
from .result import Result
|
||||
from .records import Records
|
||||
from .trade_records import TradeRecords
|
||||
from .candle import Candle, Candles
|
||||
from .positions import Positions
|
||||
from .executor import Executor
|
||||
|
||||
@@ -83,7 +83,7 @@ class Account(AccountInfo):
|
||||
if ini and res:
|
||||
return True
|
||||
else:
|
||||
await asyncio.sleep(tries)
|
||||
await asyncio.sleep(5+tries)
|
||||
return await self._login(acc=acc, tries=tries-1)
|
||||
|
||||
def has_symbol(self, symbol: str | SymbolInfo):
|
||||
|
||||
@@ -37,11 +37,16 @@ class Bot:
|
||||
self.executor = Executor()
|
||||
|
||||
@classmethod
|
||||
def run_bots(cls, bots: dict[Callable: dict] = None, num_workers: int = None):
|
||||
"""Run multiple bots at the same time."""
|
||||
num_workers = num_workers or len(bots) * 2
|
||||
def run_bots(cls, funcs: dict[Callable: dict] = None, num_workers: int = None):
|
||||
"""Run multiple scripts or bots in parallel with different accounts.
|
||||
|
||||
Args:
|
||||
funcs (dict): A dictionary of functions to run with their respective keyword arguments as a dictionary
|
||||
num_workers (int): Number of workers to run the functions
|
||||
"""
|
||||
num_workers = num_workers or len(funcs) * 2
|
||||
with ProcessPoolExecutor(max_workers=num_workers) as executor:
|
||||
for bot, kwargs in bots.items():
|
||||
for bot, kwargs in funcs.items():
|
||||
executor.submit(bot, **kwargs)
|
||||
|
||||
async def initialize(self):
|
||||
|
||||
+42
-13
@@ -4,7 +4,9 @@ from typing import Type, TypeVar, Generic, Iterable
|
||||
from logging import getLogger
|
||||
|
||||
from pandas import DataFrame, Series
|
||||
import pandas as pd
|
||||
import pandas_ta as ta
|
||||
import mplfinance as mplt
|
||||
|
||||
from .core.constants import TimeFrame
|
||||
|
||||
@@ -12,7 +14,7 @@ logger = getLogger(__name__)
|
||||
|
||||
|
||||
class Candle:
|
||||
"""A class representing bars from the MetaTrader 5 terminal as a customized class analogous to Japanese
|
||||
"""A customized class representing rates from the MetaTrader 5 terminal analogous to Japanese
|
||||
Candlesticks. You can subclass this class for added customization.
|
||||
|
||||
Attributes:
|
||||
@@ -25,7 +27,6 @@ class Candle:
|
||||
real_volume (float): Trade volume
|
||||
spread (float): Spread
|
||||
Index (int): Custom attribute representing the position of the candle in a sequence.
|
||||
mid (float): The median of the high and low price.
|
||||
"""
|
||||
time: float
|
||||
open: float
|
||||
@@ -36,7 +37,6 @@ class Candle:
|
||||
spread: float
|
||||
tick_volume: float
|
||||
Index: int
|
||||
mid: float
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""Create a Candle object from keyword arguments. This class must always be instantiated with open, high, low
|
||||
@@ -49,17 +49,15 @@ class Candle:
|
||||
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)
|
||||
self.set_attributes(**kwargs)
|
||||
|
||||
def __repr__(self):
|
||||
return ("%(class)s(Index=%(Index)s, time=%(time)s, open=%(open)s, high=%(high)s, low=%(low)s, close=%(close)s,"
|
||||
" mid=%(mid)s)") % {"class": self.__class__.__name__, "open": self.open, "high": self.high,
|
||||
"low": self.low, "close": self.close, "time": self.time, "mid": self.mid,
|
||||
'Index': self.Index}
|
||||
return ("%(class)s(Index=%(Index)s, time=%(time)s, open=%(open)s, high=%(high)s, low=%(low)s, close=%(close)s)"
|
||||
% {"class": self.__class__.__name__, "open": self.open, "high": self.high,
|
||||
"low": self.low, "close": self.close, "time": self.time, 'Index': self.Index})
|
||||
|
||||
def __str__(self):
|
||||
return self.dict()
|
||||
return str(self.dict())
|
||||
|
||||
def __eq__(self, other: "Candle"):
|
||||
return self.time == other.time
|
||||
@@ -152,7 +150,6 @@ class Candles(Generic[_Candle]):
|
||||
tick_volume: Series
|
||||
real_volume: Series
|
||||
spread: Series
|
||||
mid: Series
|
||||
Candle: Type[Candle]
|
||||
timeframe: TimeFrame
|
||||
_data: DataFrame
|
||||
@@ -177,9 +174,6 @@ class Candles(Generic[_Candle]):
|
||||
raise ValueError(f"Cannot create DataFrame from object of {type(data)}")
|
||||
|
||||
self._data = data.loc[::-1].reset_index(drop=True) if flip else data
|
||||
if 'mid' not in self._data.columns.values:
|
||||
mid = (self._data['high'] + self._data['low']) / 2
|
||||
self._data.insert(0, 'mid', mid)
|
||||
self.Candle = candle_class or Candle
|
||||
|
||||
def __repr__(self):
|
||||
@@ -268,3 +262,38 @@ class Candles(Generic[_Candle]):
|
||||
"""
|
||||
res = self._data.rename(columns=kwargs, inplace=inplace)
|
||||
return self if inplace else self.__class__(data=res)
|
||||
|
||||
def make_addplot(self, *, count: int = 50, columns: list = None, **kwargs) -> dict:
|
||||
"""
|
||||
Make subplots for adding to the main plot
|
||||
|
||||
Args:
|
||||
count (int): The numbers of candles to make the addplot for. Defaults to 50.
|
||||
columns (list[str]): The columns to make the plot from. Defaults to None.
|
||||
**kwargs: Valid arguments for the mplfinance make_addplot function
|
||||
"""
|
||||
columns = columns or []
|
||||
data = self._data[-count:]
|
||||
data.index = pd.to_datetime(data['time'], unit='s')
|
||||
return mplt.make_addplot(data[columns], **kwargs)
|
||||
|
||||
def visualize(self, *, count: int = 50, type='candle', savefig: str | dict = None, addplot: dict = None,
|
||||
style: str = 'charles', ylabel: str = 'Price', title: str = 'Chart', **kwargs):
|
||||
"""Visualize the candles using the mplfinance library.
|
||||
Args:
|
||||
count (int): The number of candles to visualize, counting from behind, i.e the most recent candles.
|
||||
Defaults to 50.
|
||||
type: Type of chart, defaults to candle
|
||||
savefig (str|dict): The path to save the figure or a dictionary of parameters to pass to the savefig method.
|
||||
addplot: Additional plots to add to the chart. Defaults to None. They should match the dimension of the
|
||||
original data which is specified via the count parameter.
|
||||
style (str): The style of the chart. Defaults to 'charles'.
|
||||
ylabel (str): The label of the y-axis. Defaults to 'Price'.
|
||||
title (str): The title of the chart. Defaults to 'Chart'.
|
||||
kwargs: valid kwargs for the plot function.
|
||||
"""
|
||||
kwargs |= {key: arg for key, arg in (('savefig', savefig), ('addplot', addplot), ('style', style),
|
||||
('ylabel', ylabel), ('title', title), ('type', type)) if arg}
|
||||
data = self._data[-count:]
|
||||
data.index = pd.to_datetime(data['time'], unit='s')
|
||||
mplt.plot(data, **kwargs)
|
||||
|
||||
+37
-24
@@ -1,12 +1,13 @@
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Iterator
|
||||
from typing import Iterator, Literal, TypeVar
|
||||
import json
|
||||
from logging import getLogger
|
||||
|
||||
from .task_queue import TaskQueue
|
||||
|
||||
logger = getLogger(__name__)
|
||||
Bot = TypeVar("Bot")
|
||||
|
||||
|
||||
class Config:
|
||||
@@ -14,6 +15,7 @@ class Config:
|
||||
|
||||
Attributes:
|
||||
record_trades (bool): Whether to keep record of trades or not.
|
||||
trade_record_mode: How to save trade, json or csv. Defaults to json
|
||||
filename (str): Name of the config file
|
||||
records_dir (str): Path to the directory where trade records are saved
|
||||
login (int): Trading account number
|
||||
@@ -31,20 +33,21 @@ class Config:
|
||||
By passing reload=True to the load_config method, you can reload and search again for the config file.
|
||||
"""
|
||||
login: int = 0
|
||||
trade_record_mode: Literal['csv', 'json'] = 'csv'
|
||||
password: str = ""
|
||||
server: str = ""
|
||||
path: str | Path = ""
|
||||
timeout: int = 60000
|
||||
record_trades: bool = True
|
||||
filename: str = "aiomql.json"
|
||||
win_percentage: float = 0.85
|
||||
records_dir: str | Path = 'records'
|
||||
config_dir: str = ''
|
||||
_initialize = True
|
||||
state: dict = {}
|
||||
root_dir: Path = Path('.').absolute().resolve()
|
||||
root: Path
|
||||
root_dir: Path
|
||||
records_dir: Path
|
||||
config_dir: str = ''
|
||||
task_queue: TaskQueue = TaskQueue()
|
||||
bot: 'Bot' = None
|
||||
bot: Bot = None
|
||||
_instance: 'Config'
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
@@ -54,19 +57,16 @@ 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)
|
||||
self.load_config(reload=reload, **kwargs)
|
||||
|
||||
def set_root(self, *, root: str | Path):
|
||||
root = Path(root) if str else root
|
||||
self.root = root.absolute().resolve()
|
||||
self.root_dir = self.root
|
||||
|
||||
def __setattr__(self, key, value):
|
||||
if key == 'root_dir':
|
||||
value = Path(value).absolute().resolve()
|
||||
if key == 'records_dir':
|
||||
self.create_records_dir(records_dir=value)
|
||||
return
|
||||
if key == 'path':
|
||||
value = self.root_dir / Path(value) if not Path(value).exists() else value
|
||||
value = str(self.root_dir / Path(value).absolute().resolve())
|
||||
super().__setattr__(key, value)
|
||||
|
||||
@staticmethod
|
||||
@@ -96,40 +96,53 @@ class Config:
|
||||
return
|
||||
|
||||
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.
|
||||
"""Create records directory if it does not exist. By default, it is relative to the root directory of the
|
||||
project unless an absolute path is provided.
|
||||
|
||||
Keyword Args:
|
||||
records_dir (str|Path): The name of the directory to create
|
||||
records_dir (str|Path): The directory to save trade records. Default is 'records'
|
||||
"""
|
||||
try:
|
||||
records_dir = Path(records_dir) if isinstance(records_dir, str) else records_dir
|
||||
records_dir = self.root_dir / records_dir
|
||||
if isinstance(records_dir, str):
|
||||
records_dir = self.root_dir / records_dir
|
||||
elif isinstance(records_dir, Path):
|
||||
records_dir = records_dir.absolute().resolve()
|
||||
records_dir.mkdir(parents=True, exist_ok=True)
|
||||
super().__setattr__('records_dir', records_dir)
|
||||
return records_dir
|
||||
self.records_dir = records_dir
|
||||
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 = ''):
|
||||
def load_config(self, *, file: str = None, reload: bool = True, filename: str = None,
|
||||
config_dir: str = '', **kwargs):
|
||||
"""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
|
||||
root_dir (str): The root directory of the project
|
||||
kwargs: Additional keyword arguments
|
||||
"""
|
||||
if not (self._initialize or reload):
|
||||
return
|
||||
self._initialize = False
|
||||
data = {}
|
||||
self.filename = filename or self.filename
|
||||
self.config_dir = config_dir or self.config_dir
|
||||
root_dir = kwargs.pop('root_dir', None)
|
||||
records_dir = kwargs.pop('records_dir', 'records')
|
||||
if self._initialize or (root_dir is not None):
|
||||
self.set_root(root=(root_dir or '.'))
|
||||
self.create_records_dir(records_dir=records_dir)
|
||||
|
||||
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()
|
||||
data |= kwargs
|
||||
[setattr(self, key, value) for key, value in data.items()]
|
||||
self._initialize = False
|
||||
|
||||
def account_info(self) -> dict[str, int | str]:
|
||||
"""Returns Account login details as found in the config object if available
|
||||
|
||||
@@ -310,7 +310,7 @@ class MetaTrader(metaclass=BaseMeta):
|
||||
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:
|
||||
async def positions_get(self, group: str = "", ticket: int = None, 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:
|
||||
@@ -324,11 +324,10 @@ class MetaTrader(metaclass=BaseMeta):
|
||||
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 = '',
|
||||
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)
|
||||
group: str = '', ticket: int = None, position: int = None) -> tuple[TradeOrder] | None:
|
||||
kwargs = {key: value for key, value in (('group', group), ('ticket', ticket), ('position', position)) if value}
|
||||
args = tuple(arg for arg in (date_from, date_to) if arg)
|
||||
res = await asyncio.to_thread(self._history_orders_get, *args, **kwargs)
|
||||
if res is None:
|
||||
err = await self.last_error()
|
||||
self.error = Error(*err)
|
||||
@@ -340,10 +339,10 @@ class MetaTrader(metaclass=BaseMeta):
|
||||
return await asyncio.to_thread(self._history_deals_total, date_from, date_to)
|
||||
|
||||
async def history_deals_get(self, date_from: datetime | float = None, date_to: datetime | float = 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)
|
||||
group: str = '', ticket: int = None, position: int = None) -> tuple[TradeDeal] | None:
|
||||
kwargs = {key: value for key, value in (('group', group), ('ticket', ticket), ('position', position)) if value}
|
||||
args = tuple(arg for arg in (date_from, date_to) if arg)
|
||||
res = await asyncio.to_thread(self._history_deals_get, *args, **kwargs)
|
||||
if res is None:
|
||||
err = await self.last_error()
|
||||
self.error = Error(*err)
|
||||
|
||||
@@ -18,7 +18,8 @@ class QueueItem:
|
||||
else:
|
||||
return self.task(*self.args, **self.kwargs)
|
||||
except Exception as err:
|
||||
logger.error(f'Error in running {self.task.__name__} with {str(self.args)}, {self.kwargs}: {err}')
|
||||
logger.error(f"Error in running {getattr(self.task, '__name__', str(self.task))}"
|
||||
f" with {str(self.args)}, {self.kwargs}: {err}")
|
||||
|
||||
|
||||
class TaskQueue:
|
||||
|
||||
+134
-29
@@ -2,8 +2,11 @@ import asyncio
|
||||
from datetime import datetime
|
||||
from logging import getLogger
|
||||
|
||||
from pandas import DataFrame
|
||||
import pandas as pd
|
||||
|
||||
from .core.config import Config
|
||||
from .core.meta_trader import MetaTrader
|
||||
from .core.meta_trader import MetaTrader, CopyTicks, OrderType
|
||||
from .core.models import TradeDeal, TradeOrder
|
||||
|
||||
logger = getLogger(__name__)
|
||||
@@ -27,8 +30,8 @@ class History:
|
||||
mt5: MetaTrader
|
||||
config: Config
|
||||
|
||||
def __init__(self, *, date_from: datetime | float = None, date_to: datetime | float = None,
|
||||
group: str = "", ticket: int = 0, position: int = 0):
|
||||
def __init__(self, *, date_from: datetime | int = None, date_to: datetime | int = None,
|
||||
group: str = "", ticket: int = None, position: int = None):
|
||||
"""
|
||||
Args:
|
||||
date_from (datetime, float): Date the orders are requested from. Set by the 'datetime' object or as a
|
||||
@@ -71,63 +74,165 @@ class History:
|
||||
self.initialized = all(res)
|
||||
return self.initialized
|
||||
|
||||
async def get_deals(self, retries=3) -> list[TradeDeal]:
|
||||
async def get_deals(self, *, date_from: datetime | int = None, date_to: datetime | int = None, group: str = '',
|
||||
retries: int = 3) -> tuple[TradeDeal, ...]:
|
||||
"""Get deals from trading history using the parameters set in the constructor.
|
||||
|
||||
Returns:
|
||||
list[TradeDeal]: A list of trade deals
|
||||
tuple[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)
|
||||
return tuple()
|
||||
|
||||
date_from, date_to, group = date_from or self.date_from, date_to or self.date_to, group or self.group
|
||||
deals = await self.mt5.history_deals_get(date_from=date_from, date_to=date_to, group=group)
|
||||
|
||||
if deals is not None:
|
||||
self.deals = [TradeDeal(**deal._asdict()) for deal in deals]
|
||||
self.deals = tuple(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)
|
||||
return await self.get_deals(date_from=date_from, date_to=date_to, group=group, retries=retries-1)
|
||||
|
||||
logger.warning(f'Failed to get deals: {self.mt5.error}')
|
||||
return []
|
||||
return tuple()
|
||||
|
||||
async def deals_total(self) -> int:
|
||||
async def get_deals_ticket(self, *, ticket: int = None) -> tuple[TradeDeal, ...]:
|
||||
"""Call specifying the order ticket. Return all deals having the specified order ticket in the DEAL_ORDER
|
||||
property.
|
||||
|
||||
Args:
|
||||
ticket (int): The order ticket
|
||||
|
||||
Returns:
|
||||
tuple[TradeDeal]: A tuple of all deals with the order ticket
|
||||
"""
|
||||
ticket = ticket or self.ticket
|
||||
assert ticket is not None, 'ticket not provided'
|
||||
deals = await self.mt5.history_deals_get(ticket=ticket)
|
||||
return tuple(sorted([TradeDeal(**deal._asdict()) for deal in deals], key=lambda x: x.time_msc))
|
||||
|
||||
async def get_deals_position(self, *, position: int = None) -> tuple[TradeDeal, ...]:
|
||||
"""
|
||||
Get all deals with the specified position ticket in the DEAL_POSITION_ID property
|
||||
Args:
|
||||
position (int): The position ticket
|
||||
|
||||
Returns:
|
||||
tuple[TradeDeal]: A tuple of all deals with the position ticket
|
||||
"""
|
||||
position = position or self.position
|
||||
assert position is not None, 'position not provided'
|
||||
deals = await self.mt5.history_deals_get(position=position)
|
||||
return tuple(sorted([TradeDeal(**deal._asdict()) for deal in deals], key=lambda x: x.time_msc))
|
||||
|
||||
async def deals_total(self, *, date_from: int | datetime = None, date_to: int | datetime = None) -> int:
|
||||
"""Get total number of deals within the specified period in the constructor.
|
||||
|
||||
Args:
|
||||
date_from (int|datetime): Date the orders are requested from. Set by the 'datetime' object or as a number of
|
||||
seconds elapsed since 1970.01.01.
|
||||
date_to (int|datetime): Date up to which the orders are requested. Set by the 'datetime' object or as a
|
||||
number of seconds elapsed since 1970.01.01.
|
||||
Returns:
|
||||
int: Total number of Deals
|
||||
"""
|
||||
self.total_deals = await self.mt5.history_deals_total(self.date_from, self.date_to)
|
||||
return self.total_deals
|
||||
date_from, date_to = date_from or self.date_from, date_to or self.date_to
|
||||
assert date_from is not None and date_to is not None, 'date_from and/or date_to not provided'
|
||||
total_deals = await self.mt5.history_deals_total(date_from, date_to)
|
||||
return total_deals
|
||||
|
||||
async def get_orders(self, retries=3) -> list[TradeOrder]:
|
||||
"""Get orders from trading history using the parameters set in the constructor.
|
||||
async def get_orders(self, *, date_from: datetime | int = None, date_to: datetime | int = None, group: str = '',
|
||||
retries: int = 3) -> tuple[TradeOrder, ...]:
|
||||
"""Get orders from trading history using the parameters set in the constructor or the method arguments.
|
||||
|
||||
Returns:
|
||||
list[TradeOrder]: A list of trade orders
|
||||
"""
|
||||
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)
|
||||
return tuple()
|
||||
|
||||
date_from, date_to, group = date_from or self.date_from, date_to or self.date_to, group or self.group
|
||||
orders = await self.mt5.history_orders_get(date_from=date_from, date_to=date_to, group=group)
|
||||
if orders is not None:
|
||||
self.orders = [TradeOrder(**order._asdict()) for order in orders]
|
||||
self.total_orders = len(self.orders)
|
||||
return self.orders
|
||||
return tuple(TradeOrder(**order._asdict()) for order in 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 []
|
||||
return await self.get_orders(date_from=date_from, date_to=date_to, group=group, retries=retries - 1)
|
||||
|
||||
async def orders_total(self) -> int:
|
||||
logger.warning(f'Failed to get orders: {self.mt5.error}')
|
||||
return tuple()
|
||||
|
||||
async def get_order_ticket(self, ticket: int | None = None) -> TradeOrder:
|
||||
ticket = ticket or self.ticket
|
||||
assert isinstance(ticket, int), 'ticket not provided'
|
||||
orders = await self.mt5.history_orders_get(ticket=ticket)
|
||||
order = orders[0]
|
||||
assert order.ticket == ticket
|
||||
return TradeOrder(**order._asdict())
|
||||
|
||||
async def get_orders_position(self, position: int = None) -> tuple[TradeOrder, ...]:
|
||||
"""
|
||||
Call specifying the position ticket. Return all orders with a position ticket specified in the
|
||||
ORDER_POSITION_ID property
|
||||
|
||||
Args:
|
||||
position: The position ticket
|
||||
|
||||
Returns:
|
||||
tuple[TradeOrder]: A tuple of all orders with the position ticket
|
||||
"""
|
||||
position = position or self.position
|
||||
assert isinstance(position, int), 'position not provided'
|
||||
orders = await self.mt5.history_orders_get(position=position)
|
||||
return tuple(sorted([TradeOrder(**order._asdict()) for order in orders], key=lambda x: x.time_done_msc))
|
||||
|
||||
async def orders_total(self, date_from: int | datetime = None, date_to: int | datetime = None) -> int:
|
||||
"""Get total number of orders within the specified period in the constructor.
|
||||
|
||||
Returns:
|
||||
int: Total number of orders
|
||||
"""
|
||||
self.total_orders = await self.mt5.history_orders_total(self.date_from, self.date_to)
|
||||
return self.total_orders
|
||||
date_from, date_to = date_from or self.date_from, date_to or self.date_to
|
||||
assert date_from is not None and date_to is not None, 'date_from and/or date_to not provided'
|
||||
total_orders = await self.mt5.history_orders_total(date_from, date_to)
|
||||
return total_orders
|
||||
|
||||
async def track_order(self, *, position: int = None, end_time: datetime = None) -> DataFrame:
|
||||
"""
|
||||
Track an order from the time it was opened to the time it was closed or any given time.
|
||||
The tracking is done by getting the ticks
|
||||
for the order symbol from the time the order was opened to the time it was closed. The profit for each tick is
|
||||
calculated using the order type, symbol, initial volume, open price and the bid or ask price of the tick
|
||||
depending on the order type.
|
||||
Args:
|
||||
end_time (datetime): The time to stop tracking the order. If not provided, the tracking will continue until
|
||||
the order is closed.
|
||||
position (int): The position ticket
|
||||
end_time (int): The time to stop tracking the order in seconds. If not provided, the tracking will continue
|
||||
until the order is closed.
|
||||
Returns:
|
||||
DataFrame: A pandas DataFrame of the ticks and profit for the order.
|
||||
"""
|
||||
orders = await self.get_orders_position(position=position)
|
||||
deals = await self.get_deals_position(position=position)
|
||||
open_order = orders[0]
|
||||
open_deal = deals[0]
|
||||
close_deal = deals[-1]
|
||||
time_done = datetime.timestamp(end_time) if end_time is not None else close_deal.time
|
||||
time_done_msc = int(time_done * 1000)
|
||||
open_order.set_attributes(time_done_msc=time_done_msc, time_done=time_done, price_open=open_deal.price)
|
||||
ticks = await self.mt5.copy_ticks_range(open_order.symbol, open_order.time_setup, open_order.time_done,
|
||||
CopyTicks.ALL)
|
||||
data = pd.DataFrame(ticks)
|
||||
profit = lambda x: self.mt5._order_calc_profit(open_order.type, open_order.symbol, open_order.volume_initial,
|
||||
open_order.price_open,
|
||||
x.ask if open_order.type == OrderType.BUY else x.bid)
|
||||
data['profits'] = data.apply(profit, axis=1)
|
||||
data['time'] = pd.to_datetime(data['time'], unit='s')
|
||||
data.set_index('time', inplace=True)
|
||||
return data
|
||||
|
||||
@@ -69,15 +69,13 @@ class FingerTrap(Strategy):
|
||||
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)
|
||||
trend = self.ttf.time // self.etf.time
|
||||
bull_trend = cae.iloc[-trend:]
|
||||
bear_trend = cbe.iloc[-trend:]
|
||||
if self.tracker.bullish and any(bull_trend):
|
||||
candles['cae'] = candles.ta_lib.cross(candles.close, candles.ema)
|
||||
candles['cbe'] = candles.ta_lib.cross(candles.close, candles.ema, above=False)
|
||||
current = candles[-1]
|
||||
if self.tracker.bullish and current.cae:
|
||||
sl = find_bullish_fractal(candles).low
|
||||
self.tracker.update(snooze=self.ttf.time, order_type=OrderType.BUY, sl=sl)
|
||||
elif self.tracker.bearish and any(bear_trend):
|
||||
elif self.tracker.bearish and current.cbe:
|
||||
sl = find_bearish_fractal(candles).high
|
||||
self.tracker.update(snooze=self.ttf.time, order_type=OrderType.SELL, sl=sl)
|
||||
else:
|
||||
|
||||
@@ -38,6 +38,26 @@ class Order(TradeRequest):
|
||||
"""
|
||||
return await self.mt5.orders_total()
|
||||
|
||||
async def get_order(self, *, ticket: int, retries: int = 3) -> TradeOrder:
|
||||
"""
|
||||
Get the order by ticket number.
|
||||
Args:
|
||||
ticket (int): Order ticket number
|
||||
retries (int): Number of retries
|
||||
Returns:
|
||||
"""
|
||||
if retries < 1:
|
||||
raise OrderError(f'Failed to get orders for {self.symbol}: {self.mt5.error}')
|
||||
orders = await self.mt5.orders_get(ticket=ticket)
|
||||
if orders is not None:
|
||||
order = TradeOrder(**orders[0]._asdict())
|
||||
assert order.ticket == ticket, f'Order ticket mismatch {order.ticket} != {ticket}'
|
||||
return order
|
||||
if self.mt5.error.is_connection_error():
|
||||
await asyncio.sleep(retries)
|
||||
return await self.get_order(ticket=ticket, retries=retries-1)
|
||||
raise OrderError(f'Failed to get orders for {self.symbol}: {self.mt5.error}')
|
||||
|
||||
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.
|
||||
|
||||
+30
-3
@@ -68,9 +68,30 @@ class Positions:
|
||||
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."""
|
||||
async def position_get(self, *, ticket: int) -> TradePosition:
|
||||
"""Get an open position by ticket.
|
||||
Args:
|
||||
ticket (int): Position ticket.
|
||||
|
||||
Returns:
|
||||
TradePosition: Return an open position
|
||||
"""
|
||||
positions = await self.positions_get(ticket=ticket)
|
||||
position = positions[0] if positions else None
|
||||
if position is None:
|
||||
raise ValueError(f'Position with ticket {ticket} not found')
|
||||
assert position.ticket == ticket, f'Position with ticket {ticket} not found'
|
||||
return position
|
||||
|
||||
async def close(self, *, ticket: int, symbol: str, price: float, volume: float, order_type: OrderType):
|
||||
"""Close an open position for the trading account using the ticket and other parameters.
|
||||
Args:
|
||||
ticket (int): Position ticket.
|
||||
symbol (str): Financial instrument name.
|
||||
price (float): Closing price.
|
||||
volume (float): Volume to close.
|
||||
order_type (OrderType): Order type.
|
||||
"""
|
||||
order = Order(action=TradeAction.DEAL, price=price, position=ticket, symbol=symbol, volume=volume,
|
||||
type=order_type.opposite)
|
||||
return await order.send()
|
||||
@@ -81,6 +102,12 @@ class Positions:
|
||||
price=pos.price_current)
|
||||
return await order.send()
|
||||
|
||||
async def close_position(self, *, position: TradePosition):
|
||||
"""Close an open position for the trading account. Using a position object."""
|
||||
order = Order(position=position.ticket, symbol=position.symbol, volume=position.volume,
|
||||
type=position.type.opposite, price=position.price_current)
|
||||
return await order.send()
|
||||
|
||||
async def close_all(self, symbol: str = '', group: str = '') -> int:
|
||||
"""Close all open positions for the trading account. Specify a symbol or group to filter positions.
|
||||
|
||||
@@ -94,6 +121,6 @@ class Positions:
|
||||
symbol = symbol or self.symbol
|
||||
group = group or self.group
|
||||
positions = [pos for pos in await self.positions_get(symbol=symbol, group=group)]
|
||||
orders = [self.close_by(pos) for pos in positions]
|
||||
orders = [self.close_position(position=pos) for pos in positions]
|
||||
results = await asyncio.gather(*[order for order in orders], return_exceptions=True)
|
||||
return len([res for res in results if (res and res.retcode) == 10009])
|
||||
|
||||
+29
-13
@@ -9,10 +9,11 @@ class RAM:
|
||||
risk: float
|
||||
points: float
|
||||
pips: float
|
||||
min_amount: float
|
||||
max_amount: float
|
||||
balance_level: float = 10
|
||||
min_amount: float = 0
|
||||
max_amount: float = 0
|
||||
risk_level: float = 50
|
||||
loss_limit: int = 3
|
||||
open_limit: int = 6
|
||||
|
||||
def __init__(self, *, risk_to_reward: float = 1, risk: float = 0.01, **kwargs):
|
||||
"""Initialize Risk Assessment and Management with the provided keyword arguments.
|
||||
@@ -28,23 +29,38 @@ class RAM:
|
||||
[setattr(self, key, value) for key, value in kwargs.items()]
|
||||
|
||||
async def get_amount(self) -> float:
|
||||
"""Calculate the amount to risk per trade as a percentage of balance.
|
||||
"""Calculate the amount to risk per trade as a percentage of equity.
|
||||
|
||||
Returns:
|
||||
float: Amount to risk per trade
|
||||
"""
|
||||
await self.account.refresh()
|
||||
return self.account.balance * self.risk
|
||||
amount = self.account.margin_free * self.risk
|
||||
if self.min_amount and self.max_amount:
|
||||
return max(self.min_amount, min(self.max_amount, amount))
|
||||
return amount
|
||||
|
||||
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)
|
||||
async def check_losing_positions(self, *, symbol: str = '') -> bool:
|
||||
"""Check if the number of losing positions is greater than or equal the loss limit.
|
||||
|
||||
Args:
|
||||
symbol (str): Symbol to check. Defaults to ''.
|
||||
"""
|
||||
positions = await Positions().positions_get(symbol=symbol)
|
||||
loosing = [trade for trade in positions if trade.profit <= 0]
|
||||
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."""
|
||||
async def check_open_positions(self, *, symbol: str = '') -> bool:
|
||||
"""Check if the number of open positions is greater than or equal the loss limit.
|
||||
|
||||
Args:
|
||||
symbol (str): Symbol to check. Defaults to ''.
|
||||
"""
|
||||
positions = await Positions().positions_get(symbol=symbol)
|
||||
return len(positions) >= self.open_limit
|
||||
|
||||
async def check_risk_level(self) -> bool:
|
||||
"""Check the risk level."""
|
||||
await self.account.refresh()
|
||||
balance_level = (self.account.margin / self.account.balance) * 100
|
||||
return balance_level >= self.balance_level
|
||||
risk_level = (1 - (self.account.margin_free / self.account.equity)) * 100
|
||||
return risk_level >= self.risk_level
|
||||
|
||||
@@ -4,6 +4,7 @@ import asyncio
|
||||
from pathlib import Path
|
||||
import csv
|
||||
import logging
|
||||
from typing import Iterable
|
||||
|
||||
from .core import Config, MetaTrader
|
||||
|
||||
@@ -50,7 +51,7 @@ class Records:
|
||||
"""
|
||||
try:
|
||||
fr = open(file, mode='r', newline='')
|
||||
reader = csv.DictReader(fr)
|
||||
reader: Iterable[dict] | csv.DictReader = csv.DictReader(fr)
|
||||
rows = [row for row in reader]
|
||||
rows = await self.update_rows(rows)
|
||||
fr.close()
|
||||
|
||||
+51
-13
@@ -1,7 +1,7 @@
|
||||
import csv
|
||||
import json
|
||||
from logging import getLogger
|
||||
from threading import RLock
|
||||
from pathlib import Path
|
||||
from typing import Iterable, Literal
|
||||
|
||||
from .core import Config
|
||||
from .core.models import OrderSendResult
|
||||
@@ -31,26 +31,64 @@ class Result:
|
||||
self.parameters = parameters or {}
|
||||
self.result = result
|
||||
self.name = name or parameters.get('name', 'Trades')
|
||||
if not Path(self.config.records_dir).exists():
|
||||
Path(self.config.records_dir).mkdir(parents=True, exist_ok=True)
|
||||
|
||||
def get_data(self) -> dict:
|
||||
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 save(self, *, trade_record_mode: Literal['csv', 'json'] = None):
|
||||
"""Record trade results as a csv or json file
|
||||
Args:
|
||||
trade_record_mode (Literal['csv'|'json']): Mode of saving trade records
|
||||
"""
|
||||
trade_record_mode = trade_record_mode or self.config.trade_record_mode
|
||||
if trade_record_mode == 'csv':
|
||||
await self.to_csv()
|
||||
else:
|
||||
await self.to_json()
|
||||
|
||||
async def to_csv(self):
|
||||
"""Record trade results and associated parameters as a csv file
|
||||
"""
|
||||
try:
|
||||
data = self.get_data()
|
||||
file = self.config.records_dir / f"{self.name}.csv"
|
||||
exists = file.exists()
|
||||
with RLock():
|
||||
with open(file, 'a', newline='') as fh:
|
||||
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)
|
||||
file.touch(exist_ok=True) if not file.exists() else ...
|
||||
reader: Iterable[dict] = csv.DictReader(file.open('r', newline=''))
|
||||
rows: list[dict] = []
|
||||
headers = set()
|
||||
[(rows.append(row), headers.update(row.keys())) for row in reader]
|
||||
rows.append(data)
|
||||
headers.update(data.keys())
|
||||
writer = csv.DictWriter(file.open('w', newline=''), fieldnames=headers, restval=None,
|
||||
extrasaction='ignore')
|
||||
writer.writeheader()
|
||||
writer.writerows(rows)
|
||||
except Exception as err:
|
||||
logger.error(f'Error: {err}. Unable to save trade results')
|
||||
logger.error(f'Unable to save to csv: {err}')
|
||||
|
||||
@staticmethod
|
||||
def serialize(value) -> str:
|
||||
"""Serialize the trade records and strategy parameters
|
||||
"""
|
||||
try:
|
||||
return str(value)
|
||||
except (ValueError, TypeError) as _:
|
||||
return ""
|
||||
|
||||
async def to_json(self):
|
||||
"""Save trades and strategy parameters in a json file
|
||||
"""
|
||||
try:
|
||||
file = self.config.records_dir / f"{self.name}.json"
|
||||
data = self.get_data()
|
||||
exists = file.touch(exist_ok=True) if not file.exists() else True
|
||||
if not exists:
|
||||
json.dump([], file.open('w'))
|
||||
with file.open('r') as fh:
|
||||
rows = json.load(fh)
|
||||
rows.append(data)
|
||||
with file.open('w') as fh:
|
||||
json.dump(rows, fh, indent=2, skipkeys=True, default=self.serialize)
|
||||
except Exception as err:
|
||||
logger.error(f"Unable to save as json file: {err}")
|
||||
|
||||
@@ -4,6 +4,8 @@ from typing import TypeVar, Iterable
|
||||
|
||||
from pandas import DataFrame, Series
|
||||
import pandas_ta as ta
|
||||
import mplfinance as mplt
|
||||
import pandas as pd
|
||||
|
||||
from .core.constants import TickFlag
|
||||
|
||||
@@ -47,6 +49,21 @@ class Tick:
|
||||
% {"class": self.__class__.__name__, "time": self.time, "bid": self.bid,
|
||||
"ask": self.ask, "last": self.last, "volume": self.volume, 'Index': self.Index})
|
||||
|
||||
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}
|
||||
|
||||
def set_attributes(self, **kwargs):
|
||||
"""Set attributes from keyword arguments"""
|
||||
for key, value in kwargs.items():
|
||||
@@ -158,3 +175,38 @@ class Ticks:
|
||||
"""
|
||||
res = self._data.rename(columns=kwargs, inplace=inplace)
|
||||
return res if inplace else self.__class__(data=res)
|
||||
|
||||
def make_addplot(self, *, count: int = 50, columns: list = None, **kwargs) -> dict:
|
||||
"""
|
||||
Make subplots for adding to the main plot
|
||||
|
||||
Args:
|
||||
count (int): The numbers of candles to make the addplot for. Defaults to 50.
|
||||
columns (list[str]): The columns to make the plot from. Defaults to None.
|
||||
**kwargs: Valid arguments for the mplfinance make_addplot function
|
||||
"""
|
||||
columns = columns or []
|
||||
data = self._data[-count:]
|
||||
data.index = pd.to_datetime(data['time'], unit='s')
|
||||
return mplt.make_addplot(data[columns], **kwargs)
|
||||
|
||||
def visualize(self, *, count: int = 50, type='candle', savefig: str | dict = None, addplot: dict = None,
|
||||
style: str = 'charles', ylabel: str = 'Price', title: str = 'Chart', **kwargs):
|
||||
"""Visualize the candles using the mplfinance library.
|
||||
Args:
|
||||
count (int): The number of candles to visualize, counting from behind, i.e the most recent candles.
|
||||
Defaults to 50.
|
||||
type: Type of chart, defaults to candle
|
||||
savefig (str|dict): The path to save the figure or a dictionary of parameters to pass to the savefig method.
|
||||
addplot: Additional plots to add to the chart. Defaults to None. They should match the dimension of the
|
||||
original data which is specified via the count parameter.
|
||||
style (str): The style of the chart. Defaults to 'charles'.
|
||||
ylabel (str): The label of the y-axis. Defaults to 'Price'.
|
||||
title (str): The title of the chart. Defaults to 'Chart'.
|
||||
kwargs: valid kwargs for the plot function.
|
||||
"""
|
||||
kwargs |= {key: arg for key, arg in (('savefig', savefig), ('addplot', addplot), ('style', style),
|
||||
('ylabel', ylabel), ('title', title), ('type', type)) if arg}
|
||||
data = self._data[-count:]
|
||||
data.index = pd.to_datetime(data['time'], unit='s')
|
||||
mplt.plot(data, **kwargs)
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
"""This module contains the Records class, which is used to read and update trade records from csv files."""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
from pathlib import Path
|
||||
import csv
|
||||
import logging
|
||||
from typing import Iterable
|
||||
|
||||
from .core import Config, MetaTrader
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TradeRecords:
|
||||
"""This utility class read trade records from csv files, and update them based on their closing positions.
|
||||
|
||||
Attributes:
|
||||
config: Config object
|
||||
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 | 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): Absolute path to directory containing record of placed trades.
|
||||
"""
|
||||
self.config = Config()
|
||||
self.mt5 = MetaTrader()
|
||||
self.records_dir = records_dir or self.config.records_dir
|
||||
|
||||
async def get_csv_records(self):
|
||||
"""Get trade records saved as csv 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 get_json_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('.json'):
|
||||
yield file
|
||||
|
||||
async def read_update_csv(self, *, file: Path):
|
||||
"""Read and update csv trade records
|
||||
|
||||
Args:
|
||||
file: Trade record file in csv format
|
||||
"""
|
||||
try:
|
||||
fr = open(file, mode='r', newline='')
|
||||
reader: Iterable[dict] | csv.DictReader = csv.DictReader(fr)
|
||||
rows = [row for row in reader]
|
||||
rows = await self.update_rows(rows=rows)
|
||||
fr.close()
|
||||
fw = open(file, mode='w', newline='')
|
||||
writer = csv.DictWriter(fw, fieldnames=reader.fieldnames, extrasaction='ignore', restval=None)
|
||||
writer.writeheader()
|
||||
writer.writerows(rows)
|
||||
fw.close()
|
||||
except Exception as err:
|
||||
logger.error(f'Error: {err}. Unable to read and update csv trade records')
|
||||
|
||||
async def read_update_json(self, *, file: Path):
|
||||
"""Read and update json trade records
|
||||
Args:
|
||||
file: Trade record file in csv format
|
||||
"""
|
||||
try:
|
||||
fh = open(file, mode='r')
|
||||
data = json.load(fh)
|
||||
rows = [row for row in data]
|
||||
rows = await self.update_rows(rows=rows)
|
||||
fh.close()
|
||||
fh = open(file, mode='w')
|
||||
json.dump(rows, fh, indent=2)
|
||||
fh.close()
|
||||
except Exception as err:
|
||||
logger.error(f'Error: {err}. Unable to read and update json trade records')
|
||||
|
||||
async def update_row(self, *, row: dict) -> dict:
|
||||
"""Update a single row of entered trade in the csv or json file with the actual profit.
|
||||
|
||||
Args:
|
||||
row: A dictionary from the dictionary writer object of the csv file.
|
||||
|
||||
Returns:
|
||||
dict: A dictionary with the actual profit and win status.
|
||||
"""
|
||||
try:
|
||||
order = int(row['order'])
|
||||
deals = await self.mt5.history_deals_get(position=order)
|
||||
if not deals or len(deals) <= 1:
|
||||
return row
|
||||
deals = [deal for deal in deals if (deal.order != deal.position_id and deal.position_id == order
|
||||
and deal.entry == 1)]
|
||||
deals.sort(key=lambda x: x.time_msc)
|
||||
deal = deals[-1]
|
||||
row.update(actual_profit=deal.profit, win=deal.profit > 0, closed=True)
|
||||
return row
|
||||
except Exception as err:
|
||||
logging.error(f'Error: {err}. Unable to update trade record')
|
||||
return row
|
||||
|
||||
async def update_rows(self, *, rows: list[dict]) -> list[dict]:
|
||||
"""Update the rows of entered trades in the csv or json file with the actual profit.
|
||||
|
||||
Args:
|
||||
rows: A list of dictionaries.
|
||||
|
||||
Returns:
|
||||
list[dict]: A list of dictionaries with the actual profit and win status.
|
||||
"""
|
||||
closed, unclosed = [], []
|
||||
for row in rows:
|
||||
closed_ = row.get('closed', False)
|
||||
closed_ = closed_.title() == 'True' if isinstance(closed_, str) else closed_
|
||||
if closed_:
|
||||
closed.append(row)
|
||||
else:
|
||||
unclosed.append(row)
|
||||
unclosed = await asyncio.gather(*[self.update_row(row=row) for row in unclosed])
|
||||
return closed + list(unclosed)
|
||||
|
||||
async def update_csv_records(self):
|
||||
"""Update csv trade records in the records_dir folder."""
|
||||
records = [self.read_update_csv(file=record) async for record in self.get_csv_records()]
|
||||
await asyncio.gather(*records)
|
||||
|
||||
async def update_json_records(self):
|
||||
"""Update json trade records in the records_dir folder."""
|
||||
records = [self.read_update_json(file=record) async for record in self.get_json_records()]
|
||||
await asyncio.gather(*records)
|
||||
|
||||
async def update_csv_record(self, *, file: Path | str):
|
||||
"""Update a single trade record csv file."""
|
||||
await self.read_update_csv(file=file)
|
||||
|
||||
async def update_json_record(self, *, file: Path | str):
|
||||
"""Update a single json trade record file"""
|
||||
await self.read_update_json(file=file)
|
||||
@@ -107,17 +107,18 @@ class Trader(ABC):
|
||||
await self.record_trade(result, parameters=self.parameters.copy())
|
||||
return result
|
||||
|
||||
async def record_trade(self, result: OrderSendResult, parameters: dict = None, name: str = ''):
|
||||
"""Record the trade in a csv file.
|
||||
|
||||
async def record_trade(self, result: OrderSendResult, parameters: dict = None, name: str = '', exclude: set = None):
|
||||
"""Record the trade in csv or json.
|
||||
Args:
|
||||
result (OrderSendResult): Result of the order send
|
||||
parameters: parameters of the trading strategy used to place the trade
|
||||
name: Name of the trading strategy
|
||||
exclude: Exclude these fields from the recorded trade
|
||||
"""
|
||||
if result.retcode != 10009 or not self.config.record_trades:
|
||||
return
|
||||
params = parameters or self.parameters.copy()
|
||||
params = {k: v for k, v in params.items() if k not in (exclude or set())}
|
||||
profit = await self.order.calc_profit()
|
||||
params["expected_profit"] = profit
|
||||
date = datetime.utcnow()
|
||||
@@ -125,7 +126,7 @@ class Trader(ABC):
|
||||
params["date"] = str(date.date())
|
||||
params["time"] = str(date.time())
|
||||
res = Result(result=result, parameters=params, name=name)
|
||||
self.config.task_queue.add_task(res.to_csv)
|
||||
self.config.task_queue.add_task(res.save)
|
||||
|
||||
@abstractmethod
|
||||
async def place_trade(self, *args, **kwargs):
|
||||
|
||||
Reference in New Issue
Block a user