mirror of
https://github.com/Ichinga-Samuel/aiomql.git
synced 2026-08-04 07:47:44 +00:00
v3.16
This commit is contained in:
@@ -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
@@ -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 = [
|
||||
|
||||
@@ -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 *
|
||||
@@ -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:
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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."""
|
||||
|
||||
@@ -24,7 +24,7 @@ class AccountInfo(Base):
|
||||
leverage: float
|
||||
profit: float
|
||||
point: float
|
||||
amount: float = 0
|
||||
amount: float
|
||||
equity: float
|
||||
credit: float
|
||||
margin: float
|
||||
|
||||
@@ -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()
|
||||
@@ -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()]
|
||||
@@ -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)
|
||||
@@ -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:
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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))))
|
||||
Reference in New Issue
Block a user