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