This commit is contained in:
Ichinga Samuel
2024-01-22 22:48:35 +01:00
parent d1ad46d203
commit 00e5592fd0
23 changed files with 285 additions and 165 deletions
+1 -1
View File
@@ -4,7 +4,7 @@ import asyncio
from datetime import datetime
from aiomql import ForexSymbol, Account, Positions, History, SimpleTrader as Trader, OrderType, RAM
logging.basicConfig(level=logging.INFO, filemode='w', filename='example.log', format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logging.basicConfig(level=logging.INFO)
async def main():
# Account details are in the aiomql.json file
+1 -1
View File
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "aiomql"
version = "3.15"
version = "3.16"
readme = "README.md"
requires-python = ">=3.10"
classifiers = [
+1 -1
View File
@@ -15,5 +15,5 @@ from .history import History
from .trader import Trader
from .terminal import Terminal
from .sessions import Session, Sessions
from .utils import dict_to_string
from .utils import dict_to_string, round_off
from .lib import *
+3 -1
View File
@@ -37,6 +37,7 @@ class Bot:
async def initialize(self):
"""Prepares the bot by signing in to the trading account and initializing the symbols for the trading session.
Starts the global task queue.
Raises:
SystemExit if sign in was not successful
@@ -48,6 +49,7 @@ class Bot:
logger.info("Login Successful")
await self.init_symbols()
self.executor.remove_workers()
self.add_coroutine(self.config.task_queue.start)
def add_function(self, func: Callable, **kwargs: dict):
"""Add a function to the executor.
@@ -58,7 +60,7 @@ class Bot:
"""
self.executor.add_function(func, kwargs)
def add_coroutine(self, coro: Coroutine, **kwargs):
def add_coroutine(self, coro: Coroutine | Callable, **kwargs):
"""Add a coroutine to the executor.
Args:
+1 -1
View File
@@ -183,7 +183,7 @@ class Candles(Generic[_Candle]):
elif isinstance(index, int):
index = index if index >= 0 else len(self) + index
return self.Candle(**self._data.iloc[index])
return self.Candle(**self._data.iloc[index], Index=index)
raise TypeError(f"Expected int, slice or str got {type(index)}")
def __setitem__(self, index, value: Series):
+2 -1
View File
@@ -4,4 +4,5 @@ from .models import *
from .constants import *
from .base import Base
from .errors import Error
from .exceptions import *
from .exceptions import *
from .task_queue import TaskQueue
+5 -3
View File
@@ -13,14 +13,16 @@ class Base:
This class provides a set of common methods and attributes for all data model classes.
For the data model classes attributes are annotated on the class body and are set as object attributes when the
class is instantiated.
Keyword Args:
**kwargs: Object attributes and values as keyword arguments. Only added if they are annotated on the class body.
"""
mt5: MetaTrader
config: Config
def __init__(self, **kwargs):
"""
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.
"""
self.config = Config()
self.mt5 = MetaTrader()
self.exclude = {'mt5', "config", 'exclude', 'include', 'annotations', 'class_vars', 'dict'}
+29 -26
View File
@@ -1,20 +1,17 @@
import os
from pathlib import Path
from sys import _getframe
from typing import Iterator
import json
from logging import getLogger
from .task_queue import TaskQueue
logger = getLogger(__name__)
class Config:
"""A class for handling configuration settings for the aiomql package.
Keyword Args:
**kwargs: Configuration settings as keyword arguments.
Variables set this way supersede those set in the config file.
Attributes:
record_trades (bool): Whether to keep record of trades or not.
filename (str): Name of the config file
@@ -26,9 +23,12 @@ class Config:
path (str): Path to terminal file
timeout (int): Timeout for terminal connection
_initialize (bool): First time initialization flag
state (dict): A global state dictionary for storing data across the framework
root_dir (str): The root directory of the project
Notes:
By default, the config class looks for a file named aiomql.json.
You can change this by passing the filename keyword argument to the constructor.
You can change this by passing the filename and/or the config_dir keyword argument(s) to the constructor
or the load_config method.
By passing reload=True to the load_config method, you can reload and search again for the config file.
"""
@@ -38,11 +38,16 @@ class Config:
path: str = ""
timeout: int = 60000
record_trades: bool = True
filename: str
filename: str = "aiomql.json"
win_percentage: float = 0.85
records_dir = Path.home() / "Documents" / "Aiomql" / "Trade Records"
config_dir: str = ''
_initialize = True
state: dict = {}
root_dir: Path = Path('.').absolute().resolve()
task_queue: TaskQueue = TaskQueue()
_instance: 'Config'
def __new__(cls, *args, **kwargs):
if not hasattr(cls, "_instance"):
@@ -50,13 +55,17 @@ class Config:
return cls._instance
def __init__(self, **kwargs):
self.filename = kwargs.pop('filename', "aiomql.json")
self.config_dir = kwargs.pop('config_dir', '')
self.load_config(reload=kwargs.pop('reload', False))
reload = kwargs.pop('reload', False)
[setattr(self, key, value) for key, value in kwargs.items()]
self.load_config(reload=reload)
def __setattr__(self, key, value):
if key == 'root_dir':
value = Path(value).absolute().resolve()
super().__setattr__(key, value)
@staticmethod
def walk_to_root(path: str) -> Iterator[str]:
def walk_to_root(path: str | Path) -> Iterator[str]:
if not os.path.exists(path):
raise IOError("Starting path not found")
@@ -71,21 +80,15 @@ class Config:
last_dir, current_dir = current_dir, parent_dir
def find_config(self):
current_file = __file__
frame = _getframe()
while frame.f_code.co_filename == current_file:
if frame.f_back is None:
return None
frame = frame.f_back
frame_filename = frame.f_code.co_filename
path = os.path.dirname(os.path.abspath(frame_filename))
path = os.path.join(path, self.config_dir) if self.config_dir else path
for dirname in self.walk_to_root(path):
check_path = os.path.join(dirname, self.filename)
if os.path.isfile(check_path):
return check_path
return None
try:
path = self.root_dir / self.config_dir
for dirname in self.walk_to_root(path):
check_path = os.path.join(dirname, self.filename)
if os.path.isfile(check_path):
return check_path
return None
except Exception as _:
return
def load_config(self, file: str = None, reload: bool = True, filename: str = None, config_dir: str = ''):
"""Load configuration settings from a file."""
+1 -1
View File
@@ -24,7 +24,7 @@ class AccountInfo(Base):
leverage: float
profit: float
point: float
amount: float = 0
amount: float
equity: float
credit: float
margin: float
+48
View File
@@ -0,0 +1,48 @@
import asyncio
from typing import Coroutine, Callable, Awaitable
from logging import getLogger
logger = getLogger(__name__)
class QueueItem:
def __init__(self, task: Callable | Awaitable | Coroutine, *args, **kwargs):
self.task = task
self.args = args
self.kwargs = kwargs
async def run(self):
try:
if asyncio.iscoroutinefunction(self.task):
return await self.task(*self.args, **self.kwargs)
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}')
class TaskQueue:
def __init__(self):
self.queue = asyncio.Queue()
def add(self, item: QueueItem):
try:
self.queue.put_nowait(item)
except asyncio.QueueFull:
return
async def worker(self):
while True:
try:
item: QueueItem = self.queue.get_nowait()
await item.run()
self.queue.task_done()
except asyncio.QueueEmpty:
return
def add_task(self, item: Callable | Awaitable | Coroutine, *args, **kwargs):
self.add(QueueItem(item, *args, **kwargs))
asyncio.create_task(self.worker())
async def start(self):
await self.queue.join()
+2 -2
View File
@@ -20,7 +20,7 @@ class Executor:
self.workers: list[type(Strategy)] = []
self.coroutines: dict[Coroutine | Callable: dict] = {}
self.functions: dict[Callable: dict] = {}
self.bot = bot
self.bot: 'Bot' = bot
def add_function(self, func: Callable, kwargs: dict):
self.functions[func] = kwargs | {'bot': self.bot}
@@ -82,4 +82,4 @@ class Executor:
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()]
+44 -48
View File
@@ -14,78 +14,74 @@ logger = logging.getLogger(__name__)
class FingerTrap(Strategy):
trend_time_frame: TimeFrame
entry_time_frame: TimeFrame
ttf: TimeFrame
etf: TimeFrame
trend: int
fast_period: int
slow_period: int
entry_period: int
fast_ema: int
slow_ema: int
entry_ema: int
parameters: dict
entry_candles_count: int
trend_candles_count: int
ecc: int
tcc: int
trader: Trader
tracker: Tracker
parameters = {"trend": 3, "fast_period": 8, "slow_period": 34, "entry_time_frame": TimeFrame.M5,
"trend_time_frame": TimeFrame.H1, "entry_period": 8,
"trend_candles_count": 48, "entry_candles_count": 50}
parameters = {"trend": 3, "fast_ema": 8, "slow_ema": 20, "etf": TimeFrame.M5,
"ttf": TimeFrame.H1, "entry_ema": 5, "tcc": 50, "ecc": 600}
def __init__(self, *, symbol: Symbol, params: dict | None = None, trader: Trader = None, sessions: Sessions = None,
name: str = 'FingerTrap'):
super().__init__(symbol=symbol, params=params, sessions=sessions, name=name)
self.trader = trader or SimpleTrader(symbol=self.symbol)
self.tracker: Tracker = Tracker(snooze=self.trend_time_frame.time)
self.tracker: Tracker = Tracker(snooze=self.ttf.time)
async def check_trend(self):
try:
candles: Candles = await self.symbol.copy_rates_from_pos(timeframe=self.trend_time_frame,
count=self.trend_candles_count)
candles: Candles = await self.symbol.copy_rates_from_pos(timeframe=self.ttf, count=self.tcc)
if not ((current := candles[-1].time) >= self.tracker.trend_time):
self.tracker.new = False
self.tracker.update(new=False, order_type=None)
return
self.tracker.update(new=True, trend_time=current)
candles.ta.ema(length=self.slow_period, append=True, fillna=0)
candles.ta.ema(length=self.fast_period, append=True, fillna=0)
candles.rename(inplace=True, **{f"EMA_{self.fast_period}": "fast", f"EMA_{self.slow_period}": "slow"})
# Compute
candles["fast_A_slow"] = candles.ta_lib.above(candles.fast, candles.slow)
candles["fast_B_slow"] = candles.ta_lib.below(candles.fast, candles.slow)
candles["close_A_fast"] = candles.ta_lib.above(candles.close, candles.fast)
candles["close_B_fast"] = candles.ta_lib.below(candles.close, candles.fast)
candles.ta.ema(length=self.slow_ema, append=True, fillna=0)
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"})
trend = candles[-self.trend: -1]
if all((c.is_bullish() and c.fast_A_slow and c.close_A_fast) for c in trend):
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
current = candles[-2]
if fas.iloc[-1] and caf.iloc[-1] and current.is_bullish():
self.tracker.update(trend="bullish")
elif all(c.is_bearish() and c.fast_B_slow and c.close_B_fast for c in trend):
elif fbs.iloc[-1] and cbf.iloc[-1] and current.is_bearish():
self.tracker.update(trend="bearish")
else:
self.tracker.update(trend="ranging", snooze=self.trend_time_frame.time)
except Exception as exe:
logger.error(f"{exe}. Error in {self.__class__.__name__}.check_trend")
self.tracker.update(trend="ranging", snooze=self.ttf.time, order_type=None)
except Exception as err:
logger.error(f"{err} for {self.symbol} in {self.__class__.__name__}.check_trend")
self.tracker.update(snooze=self.ttf.time, order_type=None)
async def confirm_trend(self):
try:
candles = await self.symbol.copy_rates_from_pos(timeframe=self.entry_time_frame,
count=self.entry_candles_count)
candles = await self.symbol.copy_rates_from_pos(timeframe=self.etf, count=self.ecc)
if not ((current := candles[-1].time) >= self.tracker.entry_time):
self.tracker.new = False
self.tracker.update(new=False, order_type=None)
return
self.tracker.update(new=True, entry_time=current)
candles.ta.ema(length=self.entry_period, append=True, fillna=0)
candles.rename(**{f"EMA_{self.entry_period}": "ema"})
candles["close_A_ema"] = candles.ta_lib.above(candles.close, candles.ema)
candles["close_B_ema"] = candles.ta_lib.below(candles.close, candles.ema)
candles["close_XA_ema"] = candles.ta_lib.cross(candles.close, candles.ema)
candles["close_XB_ema"] = candles.ta_lib.cross(candles.close, candles.ema, above=False)
current = candles[-2]
if self.tracker.bullish and current.close_XA_ema:
self.tracker.update(snooze=self.entry_time_frame.time, order_type=OrderType.BUY)
elif self.tracker.bearish and current.close_XB_ema:
self.tracker.update(snooze=self.entry_time_frame.time, order_type=OrderType.SELL)
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)
else:
self.tracker.update(snooze=self.entry_time_frame.time, order_type=None)
except Exception as exe:
logger.error(f"{exe} Error in {self.name}.confirm_trend")
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")
self.tracker.update(snooze=self.etf.time, order_type=None)
async def watch_market(self):
await self.check_trend()
@@ -95,6 +91,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)
while True:
await sess.check()
try:
@@ -108,6 +105,5 @@ class FingerTrap(Strategy):
await self.trader.place_trade(order_type=self.tracker.order_type, parameters=self.parameters)
await self.sleep(self.tracker.snooze)
except Exception as err:
logger.error(f"Error: {err}\t Symbol: {self.symbol} in {self.__class__.__name__}.trade")
await self.sleep(self.trend_time_frame.time)
continue
logger.error(f"{err} For {self.symbol} in {self.__class__.__name__}.trade\n")
await self.sleep(self.ttf.time)
+56 -5
View File
@@ -1,19 +1,71 @@
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.
"""
def compute_points(self, *, amount: float, volume) -> float:
"""Compute the number of points required for a trade. Given the amount and the volume of the trade.
Args:
amount (float): Amount to trade
volume (float): Volume to trade
"""
points = amount / (volume * self.point * self.trade_contract_size)
return points
async def compute_volume_points(self, *, amount: float, points: float, use_limits=False, round_down: bool = True,
adjust: float = False) -> tuple[float, float]:
"""Compute the volume and points required for a trade. Given the amount and the number of points.
async def compute_volume(self, *, amount: float, points, use_limits=False) -> float:
Args:
amount (float): Amount to trade
points (float): Number of points
round_down: round down the computed volume to the nearest step default True
adjust: Adjust the points if the computed volume is outside the range of permitted volumes
use_limits: Adjust the computed volume to the nearest permitted volume if the computed volume is outside
"""
amount = await self.check_amount(amount)
volume = amount / (self.point * points * self.trade_contract_size)
volume = self.round_off_volume(volume, round_down=round_down)
if (chk_vol := self.check_volume(volume))[0]:
if adjust:
points = self.compute_points(amount=amount, volume=volume)
return volume, points
if use_limits:
vol = chk_vol[1]
if adjust:
points = self.compute_points(amount=amount, volume=vol)
return vol, points
raise VolumeError(f"Incorrect Volume. Computed Volume outside the range of permitted volumes")
async def compute_volume_sl(self, *, amount: float, price: float, sl: float,
use_limits=False, adjust: bool = False, round_down: bool = True) -> tuple[float, float]:
amount = await self.check_amount(amount)
volume = amount / ((price - sl) * self.trade_contract_size)
volume = self.round_off_volume(volume, round_down=round_down)
sign = volume / abs(volume) if volume else 1
if (chk_vol := self.check_volume(abs(volume)))[0]:
if adjust:
sl = price - (amount / (volume * self.trade_contract_size))
return abs(volume), sl
return abs(volume), sl
if use_limits:
vol = chk_vol[1] * sign
if adjust:
sl = price - (amount / (vol * self.trade_contract_size))
return abs(vol), sl
raise VolumeError(f"Incorrect Volume. Computed Volume outside the range of permitted volumes")
async def compute_volume(self, *, amount: float, points, use_limits=False, round_down=True) -> float:
"""Compute volume given an amount to risk and target points. Round the computed volume to the nearest step.
Args:
amount (float): Amount to risk. Given in terms of the account currency.
points (float): Target points.
use_limits (bool): If True, the computed volume checked against the maximum and minimum volume.
round_down: round down the computed volume to the nearest step default True
Returns:
float: volume
@@ -21,10 +73,9 @@ class ForexSymbol(Symbol):
Raises:
VolumeError: If the computed volume is less than the minimum volume or greater than the maximum volume.
"""
if self.currency_profit != self.account.currency:
amount = await self.convert_currency(amount=amount, base=self.currency_profit, quote=self.account.currency)
amount = await self.check_amount(amount)
volume = amount / (self.point * points * self.trade_contract_size)
volume = self.round_off_volume(volume)
volume = self.round_off_volume(volume, round_down=round_down)
if self.check_volume(volume)[0]:
return volume
if use_limits:
+11 -15
View File
@@ -3,26 +3,22 @@ from logging import getLogger
from ..symbols import ForexSymbol
from ...ram import RAM
from ...core.models import OrderType
from ...positions import Positions
from ...trader import Trader
logger = getLogger(__name__)
class SimpleTrader(Trader):
"""A simple trader class. Limits the number of loosing trades per symbol"""
def __init__(self, *, symbol: ForexSymbol, ram: RAM = None, loss_limit: int = 3):
"""A simple trader class"""
def __init__(self, *, symbol: ForexSymbol, ram: RAM = None):
"""Initializes the order object and RAM instance
The default risk to reward ratio is 1:1.
Args:
symbol (Symbol): Financial instrument
ram (RAM): Risk Assessment and Management instance
loss_limit (int): Maximum number of losing trades allowed at a time.
"""
ram = ram or RAM(risk_to_reward=1, points=100)
ram = ram or RAM(risk_to_reward=2)
super().__init__(symbol=symbol, ram=ram)
self.loss_limit = loss_limit
async def create_order(self, *, order_type: OrderType):
"""Complete the order object with the required values. Creates a simple order.
@@ -30,16 +26,16 @@ class SimpleTrader(Trader):
Args:
order_type (OrderType): Type of order
"""
positions = await Positions().positions_get()
loosing = [trade for trade in positions if trade.profit < 0]
if (losses := len(loosing)) > self.loss_limit:
raise RuntimeError(f"Last {losses} trades in a losing position")
points = self.ram.points or self.symbol.trade_stops_level * 3
losing = await self.ram.check_losing_positions()
if losing:
raise RuntimeError(f"More than {self.ram.loss_limit} losing positions")
amount = await self.ram.get_amount()
self.order.volume = await self.symbol.compute_volume(amount=amount, points=points)
points = self.symbol.compute_points(amount=amount, volume=self.symbol.volume_min)
self.order.volume = self.symbol.volume_min
self.order.type = order_type
self.order.comment = self.parameters.get('name', '')
await self.set_trade_stop_levels(points=points)
self.order.comment = self.parameters.get('name', 'SimpleTrader')
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):
"""Places a trade based on the order_type."""
+3 -8
View File
@@ -1,7 +1,7 @@
"""Order Class"""
from logging import getLogger
from .core.models import TradeRequest, OrderSendResult, OrderCheckResult, TradeOrder
from .core.models import TradeRequest, OrderSendResult, OrderCheckResult, TradeOrder, SymbolInfo
from .core.constants import TradeAction, OrderTime, OrderFilling
from .core.exceptions import SymbolError, OrderError
from .symbol import Symbol
@@ -23,14 +23,9 @@ class Order(TradeRequest):
action (TradeAction.DEAL): Trade action
type_time (OrderTime.DAY): Order time
type_filling (OrderFilling.FOK): Order filling
Raises:
SymbolError: If symbol is not provided
"""
if 'symbol' not in kwargs:
raise SymbolError('symbol is required')
sym = kwargs.pop('symbol')
self.symbol = sym.name if isinstance(sym, Symbol) else sym
if 'symbol' in kwargs:
kwargs['symbol'] = str(kwargs['symbol'])
self.action = kwargs.pop('action', TradeAction.DEAL)
self.type_time = kwargs.pop('type_time', OrderTime.DAY)
self.type_filling = kwargs.pop('type_filling', OrderFilling.FOK)
+8 -5
View File
@@ -69,8 +69,13 @@ class Positions:
type=order_type.opposite)
return await order.send()
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)
return await order.send()
async def close_all(self, symbol: str = '', group: str = '') -> int:
"""Close all open positions for the trading account.
"""Close all open positions for the trading account. Specify a symbol or group to filter positions.
Keyword Args:
symbol (str): Financial instrument name.
@@ -82,8 +87,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(price=pos.price_current, ticket=pos.ticket, order_type=pos.type, volume=pos.volume,
symbol=pos.symbol) for pos in positions]
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.retcode == 10009])
return len([res for res in results if (res and res.retcode) == 10009])
+17 -6
View File
@@ -1,37 +1,48 @@
"""Risk Assessment and Management"""
from .account import Account
from .positions import Positions
class RAM:
account: Account
risk_to_reward: float
risk: float
amount: float
points: float
pips: float
min_amount: float
max_amount: float
balance_level: float = 50
loss_limit: int = 3
def __init__(self, *, risk_to_reward: float = 1, risk: float = 0.01, amount: float = 0, **kwargs):
def __init__(self, *, risk_to_reward: float = 1, risk: float = 0.01, **kwargs):
"""Initialize Risk Assessment and Management with the provided keyword arguments.
Keyword Args:
risk_to_reward (float): Risk to reward ratio. Defaults to 1
risk (float): Percentage of account balance to risk per trade 0.01 # 1%
amount (float): Amount to risk per trade in terms of account currency 0
kwargs: extra keyword arguments are set as object attributes
"""
self.risk_to_reward = risk_to_reward
self.risk = risk
self.amount = amount
self.account = Account()
[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 equity.
"""Calculate the amount to risk per trade as a percentage of balance.
Returns:
float: Amount to risk per trade
"""
await self.account.refresh()
return self.account.equity * self.risk
return self.account.balance * self.risk
async def check_losing_positions(self) -> bool:
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
async def check_balance_level(self) -> bool:
await self.account.refresh()
balance_level = (self.account.margin / self.account.balance) * 100
return balance_level >= self.balance_level
+1 -1
View File
@@ -102,7 +102,7 @@ class Records:
else:
unclosed.append(row)
unclosed = await asyncio.gather(*[self.update_row(row) for row in unclosed])
return closed + unclosed
return closed + list(unclosed)
async def update_records(self):
"""Update trade records in the records_dir folder."""
+10 -12
View File
@@ -1,5 +1,7 @@
import asyncio
import csv
from logging import getLogger
from threading import RLock
from .core import Config
from .core.models import OrderSendResult
@@ -34,22 +36,18 @@ class Result:
return (self.parameters | self.result.get_dict(exclude={'retcode', 'comment', 'retcode_external', 'request_id', 'request'})
| {'actual_profit': 0, 'closed': False, 'win': False})
def to_csv(self):
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 open(file, 'a', newline='') as fh:
writer = csv.DictWriter(fh, fieldnames=sorted(list(data.keys())), extrasaction='ignore', restval=None)
if not exists:
writer.writeheader()
writer.writerow(data)
with RLock():
with open(file, 'a', newline='') as fh:
writer = csv.DictWriter(fh, fieldnames=sorted(list(data.keys())), 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')
async def save_csv(self):
"""Save trade results and associated parameters as a csv file in a separate thread
"""
self.to_csv()
logger.error(f'Error: {err}. Unable to save trade results')
+11 -3
View File
@@ -2,6 +2,7 @@
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
@@ -9,6 +10,8 @@ from .ticks import Tick
from .account import Account
from .candle import Candles
from .ticks import Ticks
from .utils import round_off
logger = getLogger(__name__)
@@ -165,17 +168,22 @@ class Symbol(SymbolInfo):
else:
return check, self.volume_max
def round_off_volume(self, volume) -> float:
def round_off_volume(self, volume: float, round_down: bool = True) -> float:
"""Round off the volume to the nearest volume step.
Args:
volume (float): Volume to round off
down (bool): If True, round down. If False, round up. Optional unnamed parameter. Defaults to True.
Returns:
float: Rounded off volume
"""
step = ceil(abs(log10(self.volume_step)))
return round(volume, step)
return round_off(value=volume, step=self.volume_step, round_down=round_down)
async def check_amount(self, amount: float) -> float:
if self.currency_profit != self.account.currency:
amount = await self.convert_currency(amount=amount, base=self.currency_profit, quote=self.account.currency)
return amount
async def compute_volume(self, *args, **kwargs) -> float:
"""Computes the volume required for a trade usually based on the amount and any other keyword arguments.
+1 -1
View File
@@ -120,7 +120,7 @@ class Ticks:
return self._data[index]
item = self._data.iloc[index]
return Tick(Index=index, **item)
return Tick(**item, Index=index)
def __setitem__(self, index, value: Series):
if isinstance(value, Series):
+16 -19
View File
@@ -7,6 +7,7 @@ from zoneinfo import ZoneInfo
from .order import Order
from .symbol import Symbol as _Symbol
from .ticks import Tick
from .ram import RAM
from .core.models import OrderType, OrderSendResult
from .core.config import Config
@@ -43,19 +44,15 @@ class Trader(ABC):
self.ram = ram or RAM()
self.parameters = {}
@abstractmethod
async def create_order(self, *args, **kwargs):
"""Complete the order object with the required values. Creates a simple order."""
async def set_order_limits(self, *, pips: float):
def set_order_limits(self, *, pips: float, tick: Tick):
"""Sets the stop loss and take profit for the order. This method uses pips as defined for forex instruments.
Args:
pips: Target pips
tick: Tick object
"""
pips = pips * self.symbol.pip
sl, tp = pips, pips * self.ram.risk_to_reward
tick = await self.symbol.info_tick()
if self.order.type == OrderType.BUY:
self.order.sl, self.order.tp = round(tick.ask - sl, self.symbol.digits), round(tick.ask + tp,
self.symbol.digits)
@@ -67,15 +64,15 @@ class Trader(ABC):
else:
raise ValueError(f"Invalid order type: {self.order.type}")
async def set_trade_stop_levels(self, *, points):
"""Set the stop loss and take profit levels of the order based on the points.
def set_trade_stop_levels(self, *, points: float, tick: Tick):
"""Set the stop loss and take profit levels of the order based on the points and price tick.
Args:
points: Target points
tick: Tick object
"""
points = points * self.symbol.point
sl, tp = points, points * self.ram.risk_to_reward
tick = await self.symbol.info_tick()
if self.order.type == OrderType.BUY:
self.order.sl, self.order.tp = round(tick.ask - sl, self.symbol.digits), round(tick.ask + tp,
self.symbol.digits)
@@ -93,22 +90,22 @@ class Trader(ABC):
"""
check = await self.order.check()
if check.retcode != 0:
logger.warning(f"""Unable to place order for {self.symbol}\n
{dict_to_string(check.get_dict(include={'comment', 'retcode'}) | check.request._asdict(), multi=True)}""")
logger.warning(f"""Invalid order for {self.symbol}
\r\r{dict_to_string(check.request._asdict() | check.get_dict(include={'comment', 'retcode'}))}""")
return False
return True
async def send_order(self):
async def send_order(self) -> OrderSendResult:
"""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}\n
{dict_to_string(result.get_dict(include={'comment', 'retcode'}) | result.request._asdict(),
multi=True)}\n""")
return
logger.info(f"""Placed Trade for {self.symbol}\n{dict_to_string(
result.get_dict(exclude={'request', 'retcode_external', 'retcode', 'request_id'}), multi=True)}\n""")
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""")
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""")
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.
@@ -128,7 +125,7 @@ class Trader(ABC):
params["date"] = str(date.date())
params["time"] = str(date.time())
res = Result(result=result, parameters=params, name=name)
await res.save_csv()
self.config.task_queue.add_task(res.to_csv)
@abstractmethod
async def place_trade(self, *args, **kwargs):
+13 -4
View File
@@ -1,15 +1,24 @@
"""Utility functions for aiomql."""
import decimal
def dict_to_string(data: dict, multi=False) -> str:
"""Convert a dict to a string. Use for logging.
def dict_to_string(data: dict, multi=True) -> 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 False.
multi (bool, optional): If True, each key-value pair will be on a new line. Defaults to True.
Returns:
str: The string representation of the dict.
"""
sep = '\n' if multi else ', '
return f"{sep}".join(f"{key}: {value}" for key, value in data.items())
return f"{sep}".join(f"{key}: {value}" for key, value in data.items())
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))))