From 91a09a8ab96d72b6b1164a474ea852ace0bcbbfe Mon Sep 17 00:00:00 2001 From: Ichinga Samuel Date: Mon, 11 Nov 2024 16:54:51 +0100 Subject: [PATCH] v4 --- docs/contrib/symbols/forex_symbol.md | 101 +++++++++++------- docs/contrib/traders/scalp_trader.md | Bin 0 -> 2106 bytes docs/contrib/traders/simple_trader.md | Bin 0 -> 1684 bytes docs/contrib/utils/tracker.md | Bin 0 -> 760 bytes docs/lib/strategy.md | 2 +- src/aiomql/contrib/symbols/forex_symbol.py | 12 +++ src/aiomql/contrib/traders/scalp_trader.py | 4 +- src/aiomql/contrib/traders/simple_trader.py | 3 +- src/aiomql/contrib/utils/tracker.py | 1 + .../core/backtesting/backtest_account.py | 9 ++ .../core/backtesting/backtest_controller.py | 36 ++++++- .../core/backtesting/backtest_engine.py | 70 ++++++++++++ 12 files changed, 190 insertions(+), 48 deletions(-) create mode 100644 docs/contrib/traders/scalp_trader.md create mode 100644 docs/contrib/traders/simple_trader.md create mode 100644 docs/contrib/utils/tracker.md diff --git a/docs/contrib/symbols/forex_symbol.md b/docs/contrib/symbols/forex_symbol.md index dee71a4..b753146 100644 --- a/docs/contrib/symbols/forex_symbol.md +++ b/docs/contrib/symbols/forex_symbol.md @@ -1,71 +1,90 @@ -# Table of Contents +# ForexSymbol -* [aiomql.contrib.symbols.forex\_symbol](#aiomql.contrib.symbols.forex_symbol) - * [ForexSymbol](#aiomql.contrib.symbols.forex_symbol.ForexSymbol) - * [pip](#aiomql.contrib.symbols.forex_symbol.ForexSymbol.pip) - * [compute\_points](#aiomql.contrib.symbols.forex_symbol.ForexSymbol.compute_points) - * [compute\_volume\_points](#aiomql.contrib.symbols.forex_symbol.ForexSymbol.compute_volume_points) +## Table of Contents +- [ForexSymbol](#forex_symbol.forex_symbol) +- [pip](#forex_symbol.pip) +- [compute_points](#forex_symbol.compute_points) +- [compute_volume_points](#forex_symbol.compute_volume_points) +- [compute_volume_sl](#forex_symbol.compute_volume_sl) - - -# aiomql.contrib.symbols.forex\_symbol - - - -## ForexSymbol Objects + +### ForexSymbol ```python class ForexSymbol(Symbol) ``` +Subclass of Symbol for Forex Symbols. Handles the computation of stop loss, take profit and volume. -Subclass of Symbol for Forex Symbols. Handles the conversion of currency and the computation of stop loss, -take profit and volume. - - - -#### pip - + +### pip ```python @property def pip() ``` - Returns the pip value of the symbol. This is ten times the point value for forex symbols. -**Returns**: - -- `float` - The pip value of the symbol. - - - -#### compute\_points +#### Returns: +|Type|Description| +|----|-----------| +|float|The pip value of the symbol.| + +### compute_points ```python def compute_points(*, amount: float, volume: float) -> float ``` - Compute the number of points required for a trade. Given the amount and the volume of the trade. -**Arguments**: +#### Parameters: +|Name|Type|Description| +|----|----|-----------| +|amount|float|Amount to trade| +|volume|float|Volume to trade| -- `amount` _float_ - Amount to trade -- `volume` _float_ - Volume to trade - - - -#### compute\_volume\_points +#### Returns: +|Type|Description| +|----|-----------| +|float|The number of points required for the trade.| + +### compute_volume_points ```python async def compute_volume_points(*, amount: float, points: float, round_down: bool = False) -> float ``` - Compute the volume required for a trade. Given the amount and the number of points. -**Arguments**: +#### Parameters: +|Name|Type|Description| +|----|----|-----------| +|amount|float|Amount to trade| +|points|float|Number of points| +|round_down|bool|round down the computed volume to the nearest step default True| -- `amount` _float_ - Amount to trade -- `points` _float_ - Number of points -- `round_down` - round down the computed volume to the nearest step default True +#### Returns: +|Type|Description| +|----|-----------| +|float|The volume required for the trade. + + + +### compute_volume_sl +```python +async def compute_volume_sl(*, amount: float, price: float, sl: float, round_down: bool = False) -> float +``` +Compute the volume required for a trade. Given the amount, the price and the stop loss. + +#### Parameters: +|Name|Type|Description| +|----|----|-----------| +|amount|float|Amount to trade| +|price|float|Price of the trade| +|sl|float|Stop loss| +|round_down|bool|round down the computed volume to the nearest step default True| + +#### Returns: +|Type|Description| +|----|-----------| +|float|The volume required for the trade. \ No newline at end of file diff --git a/docs/contrib/traders/scalp_trader.md b/docs/contrib/traders/scalp_trader.md new file mode 100644 index 0000000000000000000000000000000000000000..3a4c3fb6fc192790b4e1060f4bdae45038aa5656 GIT binary patch literal 2106 zcmb7_L5~ta5QXb(;(wSd7c?&3jqXJ+Ue-J3>R| zmGxX`HTszGY~g&$d2M*p*^RBRFW6U%-t*YeQN$ci>2H8nI!z}ejaBvoFR$x?XGJ|b z&fL)LB~PzUIVh^O@8FHtZP^q44s=zc`?8)6h%PDWn0ZZ+Pg2=Go?7lsP3C)env%Ds zQ}b8G>Dx=%INlP0_qk-sYT~$*HuxfCMhe=Ug{VgqH?=*rZc65 z9c$t1ne`C$rr33DF2ia9 zas`H08*}yyb2Ymqbz00ED_mcm|5tfp>GZsOQoS4bjH=EQbAC#rqLcFPipr7SPinY5 zXsQZtQJwzw0lxIV(ZYS|pY~$(wfW`#<+o@}o-ukH`sQ z+?lV4n$MU{iD+Hb>g&~=k{q`*ixEG~qtHv-Yl8GeyocEZYfW>murzvz+2M%_UqL!uGuQ)l7td?IG5T6-o?imiiqtwrP&cbX@BKSvy;==67?0tO0!aB8l!Iq_Jz|5wFPynq9xv? z9bx~mwhvcUdDpP7(;MEEvn1=Bl{9-#tkl22PhMkGQ#g|+>@IwUrTssT%e8jJva}YN z&iKOAQwf%8o%!j6eY$eR&)ioyx8#4qS5Y;n6_s9j<yTLs>xr{Fd-M*Zbyc!G zvpTKt)>p1-E1nCF`pfDWO!YlsEu^5@9Q=Y9eW&6`{XB9y1?85tRHsUQu5r#dRp)w& z*^uqKcbF4XwyZ)?hw5H`fv8XYtHS^4lGDv8sk#^PUT~@`neUpJhut^qHMr_=PL?Sh zi_;hHP0@#|EBL2(jWxX6(IfCSRXAGJvZ=(7rI}qSwu>&+HfT|~U6f6IbTLvIZ!n|I c#YKu|=~8p7U~$U2QNS}hO+L=Qt^L678;N)uy#N3J literal 0 HcmV?d00001 diff --git a/docs/contrib/utils/tracker.md b/docs/contrib/utils/tracker.md new file mode 100644 index 0000000000000000000000000000000000000000..84ed7fcbb8e9df7ecab4222cfb7b0735309de147 GIT binary patch literal 760 zcma)4%MJlS5UjJrKWsQy68r!nadhP*63cppAjTtp9@W!}wwB0bAJbi3U0pM`YY_o@ z*dxLOD@^&9N6tS*XfDDSYi0s$G2^?#?*<1(? zkJurmYSEEhYZ2P_F=XF&5^7C;XGK)*%;>Mf`N*=5JNIv3O#R*P5`_BvO zVYU -### \_\_init\_\_ +### \__init\__ ```python def __init__(*, symbol: Symbol, params: dict = None, sessions: Sessions, name: str = "") ``` diff --git a/src/aiomql/contrib/symbols/forex_symbol.py b/src/aiomql/contrib/symbols/forex_symbol.py index 05a2c5d..b4c2ac7 100644 --- a/src/aiomql/contrib/symbols/forex_symbol.py +++ b/src/aiomql/contrib/symbols/forex_symbol.py @@ -17,6 +17,7 @@ class ForexSymbol(Symbol): def compute_points(self, *, amount: float, volume: float) -> 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 @@ -36,5 +37,16 @@ class ForexSymbol(Symbol): return self.round_off_volume(volume=volume, round_down=round_down) async def compute_volume_sl(self, *, amount: float, price: float, sl: float, round_down: bool = False) -> float: + """Compute the volume required for a trade. Given the amount, the price and the stop loss. + + Args: + amount (float): Amount to trade + price (float): The price of the trade + sl (float): The stop loss of the trade + round_down (bool): round down the computed volume to the nearest step default to False + + Returns: + float: The volume required for the trade + """ volume = amount / (abs(price - sl) * self.trade_contract_size) return self.round_off_volume(volume=volume, round_down=round_down) diff --git a/src/aiomql/contrib/traders/scalp_trader.py b/src/aiomql/contrib/traders/scalp_trader.py index d3a55c1..32f70df 100644 --- a/src/aiomql/contrib/traders/scalp_trader.py +++ b/src/aiomql/contrib/traders/scalp_trader.py @@ -8,7 +8,9 @@ logger = getLogger(__name__) class ScalpTrader(Trader): async def place_trade(self, *, order_type: OrderType, volume: float = None, parameters: dict = None): - """Places a trade based on the order_type and a given stop_loss + """Places a trade based on the order_type and volume. The volume is optional. If not provided, the minimum volume + for the symbol will be used. This trade is placed without a stop_loss or take_profit. The trade is recorded in the + trade_record file. Args: order_type (OrderType): The order_type diff --git a/src/aiomql/contrib/traders/simple_trader.py b/src/aiomql/contrib/traders/simple_trader.py index 0fa2030..d0308f1 100644 --- a/src/aiomql/contrib/traders/simple_trader.py +++ b/src/aiomql/contrib/traders/simple_trader.py @@ -8,7 +8,8 @@ logger = getLogger(__name__) class SimpleTrader(Trader): async def place_trade(self, *, order_type: OrderType, sl: float, parameters: dict = None): - """Places a trade based on the order_type and a given stop_loss + """Places a trade based on the order_type and a given stop_loss. The volume is based on the amount to risk which is + calculated using the Risk Assessment Management instance. Args: order_type (OrderType): The order_type diff --git a/src/aiomql/contrib/utils/tracker.py b/src/aiomql/contrib/utils/tracker.py index 74cf237..9c201a7 100644 --- a/src/aiomql/contrib/utils/tracker.py +++ b/src/aiomql/contrib/utils/tracker.py @@ -23,6 +23,7 @@ class Tracker: tp: float = 0 def update(self, **kwargs): + """Updates the tracker with the given kwargs""" fields = self.__dict__ for key in kwargs: if key in fields: diff --git a/src/aiomql/core/backtesting/backtest_account.py b/src/aiomql/core/backtesting/backtest_account.py index 0ec8098..f6734bc 100644 --- a/src/aiomql/core/backtesting/backtest_account.py +++ b/src/aiomql/core/backtesting/backtest_account.py @@ -6,6 +6,7 @@ from ..constants import AccountTradeMode, AccountMarginMode, AccountStopOutMode @dataclass class BackTestAccount: + """Account data for backtesting""" login: int = 0 trade_mode: AccountTradeMode = AccountTradeMode.DEMO leverage: float = 1 @@ -38,13 +39,21 @@ class BackTestAccount: __match_args__: ClassVar[tuple] def get_dict(self, exclude: set = None, include: set = None): + """Returns a dictionary of the account data. Using the exclude and include arguments, you can filter the data + + Args: + exclude (set): A set of keys to exclude + include (set): A set of keys to include + """ exclude, include = exclude or set(), include or set() filter_ = include or set(self.__match_args__).difference(exclude) return {key: value for key, value in self.asdict().items() if key in filter_} def asdict(self): + """Returns a dictionary of the account data""" res = {key: getattr(self, key) for key in self.__match_args__} return res def set_attrs(self, **kwargs): + """Se the attributes of the account data to the instance""" [setattr(self, k, v) for k, v in kwargs.items() if k in self.__match_args__] diff --git a/src/aiomql/core/backtesting/backtest_controller.py b/src/aiomql/core/backtesting/backtest_controller.py index 11f6026..995d80c 100644 --- a/src/aiomql/core/backtesting/backtest_controller.py +++ b/src/aiomql/core/backtesting/backtest_controller.py @@ -12,17 +12,28 @@ logger = getLogger(__name__) class BackTestController: + """The controller for the backtesting engine. + It also act's as a synchronizier for running multiple strategies (tasks) using a threading.Barrier primitive. + It handles the updating of open positions and close them when necessary. + It handles the iterator for the backtesting engine and handles it movement in time by moving it to the next time step. + + Attributes: + _instance (Self): The instance of the controller + config (Config): The configuration for the backtesting engine + tasks (list[Task]): The tasks that are being run + barrier (Barrier): The barrier for synchronizing the tasks + """ _instance: Self - task_tracker: int config: Config tasks: list[Task] + barrier: Barrier + def __new__(cls, *args, **kwargs): if not hasattr(cls, "_instance"): cls._instance = super().__new__(cls) cls._instance.config = Config() cls._instance.barrier = Barrier(1) - cls._instance.task_tracker = 0 cls._instance.tasks = [] return cls._instance @@ -31,16 +42,25 @@ class BackTestController: @property def backtest_engine(self): + """Returns the backtest engine""" return self.config.backtest_engine def add_tasks(self, *tasks: Task): + """Adds tasks to the tasks list""" self.tasks.extend(tasks) def set_parties(self, *, parties: int): + """Sets the number of parties for the barrier. The barrier will wait for the number of parties to reach the barrier. + This has to be done here as it can be impossible to know the eventual number of parties to set the barrier to during initialization. + + Args: + parties (int): The number of parties to set the barrier to + """ self.barrier._parties = parties @property def parties(self): + """Returns the number of parties for the barrier""" return self.barrier.parties def sigint_handler(self, sig, frame): @@ -48,14 +68,19 @@ class BackTestController: self.backtest_engine.stop_testing = True async def control(self): + """The backtest controller. It controls the backtesting engine and the tasks that are being run. + It acts as a synchronizer for the tasks and the backtesting engine. + """ try: self.backtest_engine.next() while True: pending = self.wait() - if pending == 0: # all main tasks have been completed in the current cycle + # all main tasks have been completed in the current cycle + if pending == 0: await self.backtest_engine.tracker() self.backtest_engine.next() - if self.backtest_engine.cursor.time % 3600 == 0: + # gives an output every 6 hours + if self.backtest_engine.cursor.time % (3600 * 6) == 0: logger.info(datetime.strftime(datetime.fromtimestamp(self.backtest_engine.cursor.time), "%Y-%m-%d %H:%M:%S")) if self.backtest_engine.stop_testing: logger.info( @@ -74,10 +99,12 @@ class BackTestController: return def stop_backtesting(self): + """Stop the backtester, and shutdown the executor""" self.abort() self.config.shutdown = True def wait(self): + """Called by individual tasks to indicate completion of their cycle""" try: pending = self.barrier.wait() return pending @@ -87,4 +114,5 @@ class BackTestController: logger.error("Error: %s in wait", err) def abort(self): + """Aborts the barrier""" self.barrier.abort() diff --git a/src/aiomql/core/backtesting/backtest_engine.py b/src/aiomql/core/backtesting/backtest_engine.py index 7bbfec3..6ee47ff 100644 --- a/src/aiomql/core/backtesting/backtest_engine.py +++ b/src/aiomql/core/backtesting/backtest_engine.py @@ -76,6 +76,68 @@ class BackTestEngine: assign_to_config: bool = True, account_info: dict = None, ): + """The BackTestEngine class is used to simulate trading strategies on historical data. + It can accept already saved data or create new data for backtesting on the fly. Ideally only one instance of this class should be created per session. + By default it is automatically assigned to the global config instance during instantiation, + replacing any existing backtest engine instance. But this is a configurable behaviour. + The start and end time can still be specified even when test data is provided. In that case it will be used to set the range of the backtest. + + Args: + data (BackTestData, optional): The data to use for backtesting. Defaults to None. + + speed (int, optional): The speed of the backtest. Defaults to 60 seconds. + + start (float | datetime, optional): The start time of the backtest. Defaults to 0. If a float is passed, it is assumed to be a timestamp. + + end (float | datetime, optional): The end time of the backtest. Defaults to 0. If a float is passed, it is assumed to be a timestamp. + + restart (bool, optional): Whether to restart the backtest from the beginning. Defaults to True. This is useful when resuming a backtest + using a saved BackTestData instance. + + use_terminal (bool, optional): Whether to use the terminal for backtesting. Defaults to None. If None, it uses the global config setting. + If use terminal is true, the backtest engine will use the terminal to get price data, compute margins, profit and check order viability. + If false, it will use the data provided in the BackTestData instance and default algorithm for the calculations + + name (str, optional): The name of the backtest. Defaults to "". If not provided, it is generated from the start and end times. + + stop_time (float | datetime, optional): The time to stop the backtest. Defaults to None. If a float is passed, it is assumed to be a timestamp. + If not given it is asummed to be the end of the backtest range. + + close_open_positions_on_exit (bool, optional): Whether to close all open positions when the backtest is stopped. Defaults to True. + + preload (bool, optional): Whether to preload the ticks for the backtest. Defaults to True. + + assign_to_config (bool, optional): Whether to assign the backtest engine to the global config instance. Defaults to True. + + account_info (dict, optional): A dictionary of account information to use for the backtest. Defaults to None. Use this to set + the account information for the backtest. + + Attributes: + _data (BackTestData): The data used for backtesting. This is the data that is saved to disk when the backtest is stopped. + + mt5 (MetaTrader): The MetaTrader instance for the backtest engine. + + config (Config): The global configuration instance. + + name (str): The name of the backtest. + + stop_testing (bool): Whether to stop the backtest. + + use_terminal (bool): Whether to use the terminal for backtesting. + + close_open_positions_on_exit (bool): Whether to close all open positions when the backtest is stopped. + + stop_time (int): The time to stop the backtest. + + preload (bool): Whether to preload the ticks for the backtest. + + preloaded_ticks (dict): A dictionary of preloaded ticks for the backtest. + + account_lock (RLock): A reentrant lock for the account data. + + account_info (dict): A dictionary of account information for the backtest. + + """ self._data = data or BackTestData() self.mt5 = MetaTrader() self.config = self.mt5.config @@ -113,6 +175,14 @@ class BackTestEngine: return f"{self.__class__.__name__}()" def setup_test_range(self, *, start: float | datetime = None, end: float | datetime = None, speed: int = 60, restart: bool = True): + """Setup the test range for the backtest engine. This is used to set the range of the backtest and the speed at which it runs. + + Args: + start (float | datetime, optional): The start time of the backtest. Defaults to None. If a float is passed, it is assumed to be a timestamp. + end (float | datetime, optional): The end time of the backtest. Defaults to None. If a float is passed, it is assumed to be a timestamp. + speed (int, optional): The speed of the backtest. Defaults to 60. + restart (bool, optional): Whether to restart the backtest. Defaults to True. This is useful when resuming a backtest using a saved BackTestData. + """ if self._data.span and self._data.range: start = start or self._data.span[0] end = end or self._data.span[-1] + speed