This commit is contained in:
Ichinga Samuel
2024-11-11 16:54:51 +01:00
parent 72e902c4c5
commit 91a09a8ab9
12 changed files with 190 additions and 48 deletions
+60 -41
View File
@@ -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)
<a id="aiomql.contrib.symbols.forex_symbol"></a>
# aiomql.contrib.symbols.forex\_symbol
<a id="aiomql.contrib.symbols.forex_symbol.ForexSymbol"></a>
## ForexSymbol Objects
<a id="forex_symbol.forex_symbol"></a>
### 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.
<a id="aiomql.contrib.symbols.forex_symbol.ForexSymbol.pip"></a>
#### pip
<a id="forex_symbol.pip"></a>
### 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.
<a id="aiomql.contrib.symbols.forex_symbol.ForexSymbol.compute_points"></a>
#### compute\_points
#### Returns:
|Type|Description|
|----|-----------|
|float|The pip value of the symbol.|
<a id="forex_symbol.compute_points"></a>
### 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
<a id="aiomql.contrib.symbols.forex_symbol.ForexSymbol.compute_volume_points"></a>
#### compute\_volume\_points
#### Returns:
|Type|Description|
|----|-----------|
|float|The number of points required for the trade.|
<a id="forex_symbol.compute_volume_points"></a>
### 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.
<a id="forex_symbol.compute_volume_sl"></a>
### 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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -36,7 +36,7 @@ The base class for creating strategies.
<a id="strategy.__init__"></a>
### \_\_init\_\_
### \__init\__
```python
def __init__(*, symbol: Symbol, params: dict = None, sessions: Sessions, name: str = "")
```
@@ -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)
+3 -1
View File
@@ -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
+2 -1
View File
@@ -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
+1
View File
@@ -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:
@@ -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__]
@@ -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()
@@ -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