mirror of
https://github.com/Ichinga-Samuel/aiomql.git
synced 2026-08-23 00:38:07 +00:00
v3.16
This commit is contained in:
@@ -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
@@ -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 = [
|
||||||
|
|||||||
@@ -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 *
|
||||||
@@ -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:
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
@@ -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."""
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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.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()]
|
||||||
@@ -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
|
|
||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
@@ -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)
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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))))
|
||||||
Reference in New Issue
Block a user