mirror of
https://github.com/Ichinga-Samuel/aiomql.git
synced 2026-08-26 01:58:05 +00:00
v4.0.5
This commit is contained in:
@@ -2,6 +2,7 @@
|
|||||||
Utils is a collection of utility functions that are used throughout the codebase. It is a collection of functions.
|
Utils is a collection of utility functions that are used throughout the codebase. It is a collection of functions.
|
||||||
|
|
||||||
## Table of Contents
|
## Table of Contents
|
||||||
|
- [backtest_sleep](#_utils.backtest_sleep)
|
||||||
- [round_off](#_utiils.round_off)
|
- [round_off](#_utiils.round_off)
|
||||||
- [dict_to_string](#_utils.dict_to_string)
|
- [dict_to_string](#_utils.dict_to_string)
|
||||||
- [round_down](#_utils.round_down)
|
- [round_down](#_utils.round_down)
|
||||||
@@ -126,3 +127,15 @@ A decorator to handle exceptions in an async function.
|
|||||||
def error_handler_sync(func=None, *, msg="", exe=Exception, response=None, log_error_msg=True) -> Callable:
|
def error_handler_sync(func=None, *, msg="", exe=Exception, response=None, log_error_msg=True) -> Callable:
|
||||||
```
|
```
|
||||||
A decorator to handle exceptions in a sync function.
|
A decorator to handle exceptions in a sync function.
|
||||||
|
|
||||||
|
<a id="_utils.backtest_sleep"></a>
|
||||||
|
### backtest_sleep
|
||||||
|
```python
|
||||||
|
def backtest_sleep(seconds: float) -> None:
|
||||||
|
```
|
||||||
|
Sleeps for a given number of seconds in backtest mode.
|
||||||
|
|
||||||
|
#### Parameters:
|
||||||
|
| Name | Type | Description |
|
||||||
|
|---------|-------|----------------------------------|
|
||||||
|
| seconds | float | The number of seconds to sleep. |
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
- [calc_profit](#order.calc_profit)
|
- [calc_profit](#order.calc_profit)
|
||||||
- [calc_loss](#order.calc_loss)
|
- [calc_loss](#order.calc_loss)
|
||||||
- [request](#order.request)
|
- [request](#order.request)
|
||||||
|
- [modify](#order.modify)
|
||||||
|
|
||||||
<a id="order.order"></a>
|
<a id="order.order"></a>
|
||||||
### Order
|
### Order
|
||||||
@@ -160,3 +161,11 @@ Return the trade request object as a dict
|
|||||||
| Type | Description |
|
| Type | Description |
|
||||||
|--------|----------------------------------|
|
|--------|----------------------------------|
|
||||||
| `dict` | Returns the trade request object |
|
| `dict` | Returns the trade request object |
|
||||||
|
|
||||||
|
|
||||||
|
<a id="order.modify"></a>
|
||||||
|
### modify
|
||||||
|
```python
|
||||||
|
def modify(**kwargs)
|
||||||
|
```
|
||||||
|
Modify the order object with keyword arguments.
|
||||||
|
|||||||
+30
-7
@@ -10,6 +10,7 @@
|
|||||||
- [close_position_by_ticket](#positions.close_position_by_ticket)
|
- [close_position_by_ticket](#positions.close_position_by_ticket)
|
||||||
- [close_position](#positions.close_position)
|
- [close_position](#positions.close_position)
|
||||||
- [close_all](#positions.close_all)
|
- [close_all](#positions.close_all)
|
||||||
|
- [get_total_positions](#positions.get_total_positions)
|
||||||
|
|
||||||
<a id="positions.positions"></a>
|
<a id="positions.positions"></a>
|
||||||
### Positions
|
### Positions
|
||||||
@@ -19,10 +20,11 @@ class Positions
|
|||||||
Get and handle Open positions.
|
Get and handle Open positions.
|
||||||
|
|
||||||
#### Attributes
|
#### Attributes
|
||||||
| Name | Type | Description |
|
| Name | Type | Description |
|
||||||
|-------------|-----------------------------|----------------------------|
|
|-------------|-----------------------------|-------------------------------------------------------------------------------------|
|
||||||
| `positions` | `tuple[TradePosition, ...]` | Financial instrument name. |
|
| `positions` | `tuple[TradePosition, ...]` | Financial instrument name. |
|
||||||
| `mt5` | `MetaTrader` | MetaTrader instance. |
|
| `mt5` | `MetaTrader` | MetaTrader instance. |
|
||||||
|
|`total_positions`| `int` | Total number of open positions. Can be set in `get_positions` or `get_total_positions`. |
|
||||||
|
|
||||||
<a id="positions.__init__"></a>
|
<a id="positions.__init__"></a>
|
||||||
### \_\_init\_\_
|
### \_\_init\_\_
|
||||||
@@ -35,11 +37,19 @@ Initialize a position instance
|
|||||||
<a id="positions.get_position"></a>
|
<a id="positions.get_position"></a>
|
||||||
### get_positions
|
### get_positions
|
||||||
```python
|
```python
|
||||||
async def get_positions(self) -> tuple[TradePosition, ...]:
|
async def get_positions(*, symbol: str = None, ticket: int = None, group: str = None) -> tuple[TradePosition, ...]:
|
||||||
```
|
```
|
||||||
Get open positions
|
Get open positions, with the ability to filter by symbol, ticket, or group.
|
||||||
|
|
||||||
#### Returns
|
#### Parameters:
|
||||||
|
| Name | Type | Description |
|
||||||
|
|----------|-------|-----------------|
|
||||||
|
| `symbol` | `str` | Symbol |
|
||||||
|
| `ticket` | `int` | Position ticket |
|
||||||
|
| `group` | `str` | Group name |
|
||||||
|
|
||||||
|
|
||||||
|
#### Returns:
|
||||||
| Type | Description |
|
| Type | Description |
|
||||||
|-----------------------------|--------------------------------|
|
|-----------------------------|--------------------------------|
|
||||||
| `tuple[TradePosition, ...]` | A list of open trade positions |
|
| `tuple[TradePosition, ...]` | A list of open trade positions |
|
||||||
@@ -150,3 +160,16 @@ Close all open positions for the trading account.
|
|||||||
| Type | Description |
|
| Type | Description |
|
||||||
|-------|--------------------------------------|
|
|-------|--------------------------------------|
|
||||||
| `int` | Return total number of closed trades |
|
| `int` | Return total number of closed trades |
|
||||||
|
|
||||||
|
|
||||||
|
<a id="positions.get_total_positions"></a>
|
||||||
|
### get_total_positions
|
||||||
|
```python
|
||||||
|
async def get_total_positions() -> int
|
||||||
|
```
|
||||||
|
Get the total number of open positions and set the `total_positions` attribute.
|
||||||
|
|
||||||
|
#### Returns:
|
||||||
|
| Type | Description |
|
||||||
|
|-------|--------------------------------------|
|
||||||
|
| `int` | Return total number of open trades |
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
- [get_amount](#ram.get_amount)
|
- [get_amount](#ram.get_amount)
|
||||||
- [check_losing_positions](#ram.check_losing_positions)
|
- [check_losing_positions](#ram.check_losing_positions)
|
||||||
- [check_open_positions](#ram.check_open_positions)
|
- [check_open_positions](#ram.check_open_positions)
|
||||||
|
- [modify_ram](#ram.modify_ram)
|
||||||
|
|
||||||
<a id="ram.ram"></a>
|
<a id="ram.ram"></a>
|
||||||
### RAM
|
### RAM
|
||||||
@@ -75,3 +76,11 @@ Check if the number of open positions is less than or equal the loss limit.
|
|||||||
| Type | Description |
|
| Type | Description |
|
||||||
|--------|---------------------------------------------------------------------------------------|
|
|--------|---------------------------------------------------------------------------------------|
|
||||||
| `bool` | True if the number of open losing trades is more than the loss limit, False otherwise |
|
| `bool` | True if the number of open losing trades is more than the loss limit, False otherwise |
|
||||||
|
|
||||||
|
|
||||||
|
<a id="ram.modify_ram"></a>
|
||||||
|
### modify_ram
|
||||||
|
```python
|
||||||
|
def modify_ram(**kwargs):
|
||||||
|
```
|
||||||
|
Modify the RAM attributes. All provided keyword arguments are set as attributes.
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"balance": 504.26,
|
||||||
|
"profit": 0,
|
||||||
|
"equity": 504.26,
|
||||||
|
"margin": 0.0,
|
||||||
|
"margin_free": 504.26,
|
||||||
|
"margin_level": 0,
|
||||||
|
"wins": 12,
|
||||||
|
"losses": 15,
|
||||||
|
"total": 27,
|
||||||
|
"win_percentage": 44.44,
|
||||||
|
"win": 326.55,
|
||||||
|
"loss": -172.29,
|
||||||
|
"net_profit": 154.26,
|
||||||
|
"profit_factor": 1.9,
|
||||||
|
"profitability": 44.07
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@ from datetime import datetime, UTC
|
|||||||
|
|
||||||
from aiomql.lib.backtester import BackTester
|
from aiomql.lib.backtester import BackTester
|
||||||
from aiomql.core import Config
|
from aiomql.core import Config
|
||||||
from aiomql.contrib.strategies import FingerTrap
|
from aiomql.contrib.strategies import FingerTrap, Chaos
|
||||||
from aiomql.contrib.symbols import ForexSymbol
|
from aiomql.contrib.symbols import ForexSymbol
|
||||||
from aiomql.core.backtesting import BackTestEngine
|
from aiomql.core.backtesting import BackTestEngine
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@ def back_tester():
|
|||||||
start = datetime(2024, 5, 1, tzinfo=UTC)
|
start = datetime(2024, 5, 1, tzinfo=UTC)
|
||||||
stop_time = datetime(2024, 5, 2, tzinfo=UTC)
|
stop_time = datetime(2024, 5, 2, tzinfo=UTC)
|
||||||
end = datetime(2024, 5, 7, tzinfo=UTC)
|
end = datetime(2024, 5, 7, tzinfo=UTC)
|
||||||
back_test_engine = BackTestEngine(start=start, end=end, speed=3600,
|
back_test_engine = BackTestEngine(start=start, end=end, speed=7200,
|
||||||
close_open_positions_on_exit=True, assign_to_config=True, preload=True,
|
close_open_positions_on_exit=True, assign_to_config=True, preload=True,
|
||||||
account_info={"balance": 350})
|
account_info={"balance": 350})
|
||||||
backtester = BackTester(backtest_engine=back_test_engine)
|
backtester = BackTester(backtest_engine=back_test_engine)
|
||||||
|
|||||||
+2
-2
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "aiomql"
|
name = "aiomql"
|
||||||
version = "4.0.4"
|
version = "4.0.5"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
classifiers = [
|
classifiers = [
|
||||||
@@ -14,7 +14,7 @@ classifiers = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
keywords = ["MetaTrader5", "Asynchronous", "Algorithmic Trading", "Trading Bot", "Backtesting",
|
keywords = ["MetaTrader5", "Asynchronous", "Algorithmic Trading", "Trading Bot", "Backtesting",
|
||||||
"Technical Analysis", "Forex", "Stocks", "Cryptocurrency", "Futures", "Options"]
|
"Technical Analysis", "Forex", "Stocks", "Cryptocurrency", "Futures", "Options", "Crypto", "Algo Trading"]
|
||||||
|
|
||||||
dependencies = ["MetaTrader5>=5.0.37", "pandas>=1.5.0", "pandas-ta>=0.3.14b0"]
|
dependencies = ["MetaTrader5>=5.0.37", "pandas>=1.5.0", "pandas-ta>=0.3.14b0"]
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
-e git+https://github.com/Ichinga-Samuel/aiomql.git@3143ca9936e2f791798b4944bf0df7225c4c6a78#egg=aiomql
|
|
||||||
anyio==4.3.0
|
anyio==4.3.0
|
||||||
argon2-cffi==23.1.0
|
argon2-cffi==23.1.0
|
||||||
argon2-cffi-bindings==21.2.0
|
argon2-cffi-bindings==21.2.0
|
||||||
|
|||||||
+74
-5
@@ -10,7 +10,35 @@ from .core.config import Config
|
|||||||
|
|
||||||
logger = getLogger(__name__)
|
logger = getLogger(__name__)
|
||||||
|
|
||||||
config = Config()
|
|
||||||
|
async def backtest_sleep(secs: float):
|
||||||
|
config = Config()
|
||||||
|
btc = config.backtest_controller
|
||||||
|
try:
|
||||||
|
if btc.parties == 2:
|
||||||
|
steps = int(secs) // config.backtest_engine.speed
|
||||||
|
steps = max(steps, 1)
|
||||||
|
config.backtest_engine.fast_forward(steps=steps)
|
||||||
|
btc.wait()
|
||||||
|
|
||||||
|
elif btc.parties > 2:
|
||||||
|
_time = config.backtest_engine.cursor.time + secs
|
||||||
|
while _time > config.backtest_engine.cursor.time:
|
||||||
|
btc.wait()
|
||||||
|
else:
|
||||||
|
btc.wait()
|
||||||
|
except Exception as err:
|
||||||
|
btc.wait()
|
||||||
|
logger.error("Error: %s in backtest_sleep", err)
|
||||||
|
|
||||||
|
|
||||||
|
# async def backtest_sleep(secs):
|
||||||
|
# """An async sleep function for use during backtesting."""
|
||||||
|
# btc = BackTestController()
|
||||||
|
# config = Config()
|
||||||
|
# sleep = config.backtest_engine.cursor.time + secs
|
||||||
|
# while sleep > config.backtest_engine.cursor.time:
|
||||||
|
# btc.wait()
|
||||||
|
|
||||||
|
|
||||||
def dict_to_string(data: dict, multi=False) -> str:
|
def dict_to_string(data: dict, multi=False) -> str:
|
||||||
@@ -28,6 +56,13 @@ def dict_to_string(data: dict, multi=False) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def backoff_decorator(func=None, *, max_retries: int = 2, retries: int = 0, error="") -> callable:
|
def backoff_decorator(func=None, *, max_retries: int = 2, retries: int = 0, error="") -> callable:
|
||||||
|
"""A decorator to retry a function a number of times before giving up.
|
||||||
|
Args:
|
||||||
|
func (callable, optional): The function to decorate. Defaults to None.
|
||||||
|
max_retries (int, optional): The maximum number of retries. Defaults to 2.
|
||||||
|
retries (int, optional): The number of retries. Defaults to 0.
|
||||||
|
error (Any, optional): The error to raise when the maximum number of retries is reached. Defaults to "".
|
||||||
|
"""
|
||||||
if func is None:
|
if func is None:
|
||||||
return partial(backoff_decorator, max_retries=max_retries, retries=retries, error=error)
|
return partial(backoff_decorator, max_retries=max_retries, retries=retries, error=error)
|
||||||
|
|
||||||
@@ -47,7 +82,7 @@ def backoff_decorator(func=None, *, max_retries: int = 2, retries: int = 0, erro
|
|||||||
return res
|
return res
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
logger.error("Error in %s: %s", func.__name__, err)
|
logger.error("Error in %s: %s", func.__name__, err)
|
||||||
if config.mode != "backtest":
|
if Config().mode != "backtest":
|
||||||
await asyncio.sleep(2**retries + random.uniform(0, max_retries))
|
await asyncio.sleep(2**retries + random.uniform(0, max_retries))
|
||||||
await wrapper(*args, **kwargs)
|
await wrapper(*args, **kwargs)
|
||||||
|
|
||||||
@@ -55,6 +90,14 @@ def backoff_decorator(func=None, *, max_retries: int = 2, retries: int = 0, erro
|
|||||||
|
|
||||||
|
|
||||||
def error_handler(func=None, *, msg="", exe=Exception, response=None, log_error_msg=True):
|
def error_handler(func=None, *, msg="", exe=Exception, response=None, log_error_msg=True):
|
||||||
|
"""A decorator to handle errors in an async function.
|
||||||
|
Args:
|
||||||
|
func (callable, optional): The function to decorate. Defaults to None.
|
||||||
|
msg (str, optional): The error message to log. Defaults to "".
|
||||||
|
exe (Exception, optional): The exception to catch. Defaults to Exception.
|
||||||
|
response (Any, optional): The response to return when an error occurs. Defaults to None.
|
||||||
|
log_error_msg (bool, optional): If True, log the error message. Defaults to True.
|
||||||
|
"""
|
||||||
if func is None:
|
if func is None:
|
||||||
return partial(error_handler, msg=msg, exe=exe, response=response, log_error_msg=log_error_msg)
|
return partial(error_handler, msg=msg, exe=exe, response=response, log_error_msg=log_error_msg)
|
||||||
|
|
||||||
@@ -72,6 +115,15 @@ def error_handler(func=None, *, msg="", exe=Exception, response=None, log_error_
|
|||||||
|
|
||||||
|
|
||||||
def error_handler_sync(func=None, *, msg="", exe=Exception, response=None, log_error_msg=True):
|
def error_handler_sync(func=None, *, msg="", exe=Exception, response=None, log_error_msg=True):
|
||||||
|
"""A decorator to handle errors in a synchronous function.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
func (callable, optional): The function to decorate. Defaults to None.
|
||||||
|
msg (str, optional): The error message to log. Defaults to "".
|
||||||
|
exe (Exception, optional): The exception to catch. Defaults to Exception.
|
||||||
|
response (Any, optional): The response to return when an error occurs. Defaults to None.
|
||||||
|
log_error_msg (bool, optional): If True, log the error message. Defaults to True.
|
||||||
|
"""
|
||||||
if func is None:
|
if func is None:
|
||||||
return partial(error_handler, msg=msg, exe=exe, response=response, log_error_msg=log_error_msg)
|
return partial(error_handler, msg=msg, exe=exe, response=response, log_error_msg=log_error_msg)
|
||||||
|
|
||||||
@@ -88,7 +140,15 @@ def error_handler_sync(func=None, *, msg="", exe=Exception, response=None, log_e
|
|||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
def round_down(value: int | float, base: int) -> int:
|
def round_down(value: int | float, base: int) -> int | float:
|
||||||
|
"""Round down a number to the nearest base.
|
||||||
|
Args:
|
||||||
|
value (int | float): The number to round down.
|
||||||
|
base (int): The base to round down to.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(int | float): The rounded down number.
|
||||||
|
"""
|
||||||
return int(value) if value % base == 0 else int(value - (value % base))
|
return int(value) if value % base == 0 else int(value - (value % base))
|
||||||
|
|
||||||
|
|
||||||
@@ -98,19 +158,28 @@ def round_up(value: int | float, base: int) -> int:
|
|||||||
|
|
||||||
# noinspection PyShadowingNames
|
# noinspection PyShadowingNames
|
||||||
def round_off(value: float, step: float, round_down: bool = False) -> float:
|
def round_off(value: float, step: float, round_down: bool = False) -> float:
|
||||||
"""Round off a number to the nearest step."""
|
"""Round off a number to the nearest step.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
value (float): The number to round off.
|
||||||
|
step (float): The step to round off to.
|
||||||
|
round_down (bool, optional): If True, the number is rounded down otherwise it is rounded up. Defaults to False.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
float: The rounded off number.
|
||||||
|
"""
|
||||||
with decimal.localcontext() as ctx:
|
with decimal.localcontext() as ctx:
|
||||||
ctx.rounding = decimal.ROUND_DOWN if round_down else decimal.ROUND_UP
|
ctx.rounding = decimal.ROUND_DOWN if round_down else decimal.ROUND_UP
|
||||||
return float(decimal.Decimal(str(value)).quantize(decimal.Decimal(str(step))))
|
return float(decimal.Decimal(str(value)).quantize(decimal.Decimal(str(step))))
|
||||||
|
|
||||||
|
|
||||||
def async_cache(fun):
|
def async_cache(fun):
|
||||||
|
"""A decorator to cache the result of an async function."""
|
||||||
@wraps(fun)
|
@wraps(fun)
|
||||||
async def wrapper(*args, **kwargs):
|
async def wrapper(*args, **kwargs):
|
||||||
key = (args, frozenset(kwargs.items()))
|
key = (args, frozenset(kwargs.items()))
|
||||||
with wrapper.lock:
|
with wrapper.lock:
|
||||||
if key not in wrapper.cache:
|
if key not in wrapper.cache:
|
||||||
# print(key)
|
|
||||||
wrapper.cache[key] = await fun(*args, **kwargs)
|
wrapper.cache[key] = await fun(*args, **kwargs)
|
||||||
return wrapper.cache[key]
|
return wrapper.cache[key]
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ logger = getLogger(__name__)
|
|||||||
|
|
||||||
class BackTestController:
|
class BackTestController:
|
||||||
"""The controller for the backtesting engine.
|
"""The controller for the backtesting engine.
|
||||||
It also act's as a synchronizier for running multiple strategies (tasks) using a threading.Barrier primitive.
|
It also acts as a synchronizer 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 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.
|
It handles the iterator for the backtesting engine and handles it movement in time by moving it to the next time step.
|
||||||
|
|
||||||
@@ -33,6 +33,7 @@ class BackTestController:
|
|||||||
if not hasattr(cls, "_instance"):
|
if not hasattr(cls, "_instance"):
|
||||||
cls._instance = super().__new__(cls)
|
cls._instance = super().__new__(cls)
|
||||||
cls._instance.config = Config()
|
cls._instance.config = Config()
|
||||||
|
cls._instance.config.backtest_controller = cls._instance
|
||||||
cls._instance.barrier = Barrier(1)
|
cls._instance.barrier = Barrier(1)
|
||||||
cls._instance.tasks = []
|
cls._instance.tasks = []
|
||||||
return cls._instance
|
return cls._instance
|
||||||
@@ -79,8 +80,7 @@ class BackTestController:
|
|||||||
if pending == 0:
|
if pending == 0:
|
||||||
await self.backtest_engine.tracker()
|
await self.backtest_engine.tracker()
|
||||||
self.backtest_engine.next()
|
self.backtest_engine.next()
|
||||||
# gives an output every 6 hours
|
if self.backtest_engine.cursor.time % (3600 * 12) == 0:
|
||||||
if self.backtest_engine.cursor.time % (3600 * 6) == 0:
|
|
||||||
logger.info(
|
logger.info(
|
||||||
datetime.strftime(
|
datetime.strftime(
|
||||||
datetime.fromtimestamp(self.backtest_engine.cursor.time), "%Y-%m-%d %H:%M:%S"
|
datetime.fromtimestamp(self.backtest_engine.cursor.time), "%Y-%m-%d %H:%M:%S"
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ from .task_queue import TaskQueue
|
|||||||
logger = getLogger(__name__)
|
logger = getLogger(__name__)
|
||||||
Bot = TypeVar("Bot")
|
Bot = TypeVar("Bot")
|
||||||
BackTestEngine = TypeVar("BackTestEngine")
|
BackTestEngine = TypeVar("BackTestEngine")
|
||||||
|
BackTestController = TypeVar("BackTestController")
|
||||||
|
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
@@ -28,6 +29,7 @@ class Config:
|
|||||||
task_queue: TaskQueue
|
task_queue: TaskQueue
|
||||||
_backtest_engine: BackTestEngine
|
_backtest_engine: BackTestEngine
|
||||||
bot: Bot
|
bot: Bot
|
||||||
|
backtest_controller: BackTestController
|
||||||
_instance: Self
|
_instance: Self
|
||||||
mode: Literal["backtest", "live"]
|
mode: Literal["backtest", "live"]
|
||||||
use_terminal_for_backtesting: bool
|
use_terminal_for_backtesting: bool
|
||||||
@@ -54,9 +56,11 @@ class Config:
|
|||||||
if not hasattr(cls, "_instance"):
|
if not hasattr(cls, "_instance"):
|
||||||
cls._instance = super().__new__(cls)
|
cls._instance = super().__new__(cls)
|
||||||
cls._instance.state = {}
|
cls._instance.state = {}
|
||||||
cls._instance.task_queue = TaskQueue()
|
cls._instance.task_queue = TaskQueue(mode='infinite', workers=10)
|
||||||
cls._instance.set_attributes(**cls._defaults)
|
cls._instance.set_attributes(**cls._defaults)
|
||||||
cls._instance._backtest_engine = None
|
cls._instance._backtest_engine = None
|
||||||
|
cls._instance.bot = None
|
||||||
|
cls._instance.backtest_controller = None
|
||||||
# cls._instance.load_config(**kwargs)
|
# cls._instance.load_config(**kwargs)
|
||||||
return cls._instance
|
return cls._instance
|
||||||
|
|
||||||
|
|||||||
@@ -123,7 +123,8 @@ class OrderType(Repr, IntEnum):
|
|||||||
Returns:
|
Returns:
|
||||||
int: integer value of opposite order type
|
int: integer value of opposite order type
|
||||||
"""
|
"""
|
||||||
return {0: 1, 1: 0, 2: 3, 3: 2, 4: 5, 5: 4, 6: 7, 7: 6, 8: 8}[self]
|
_type = {0: 1, 1: 0, 2: 3, 3: 2, 4: 5, 5: 4, 6: 7, 7: 6, 8: 8}[self]
|
||||||
|
return OrderType(_type)
|
||||||
|
|
||||||
|
|
||||||
class BookType(Repr, IntEnum):
|
class BookType(Repr, IntEnum):
|
||||||
|
|||||||
@@ -87,6 +87,10 @@ class TaskQueue:
|
|||||||
"""Worker function to run tasks in the queue."""
|
"""Worker function to run tasks in the queue."""
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
|
if self.mode == 'infinite' and self.queue.qsize() <= 1:
|
||||||
|
dummy = QueueItem(self.dummy_task)
|
||||||
|
self.add(item=dummy)
|
||||||
|
|
||||||
if isinstance(self.queue, asyncio.PriorityQueue):
|
if isinstance(self.queue, asyncio.PriorityQueue):
|
||||||
_, item = self.queue.get_nowait()
|
_, item = self.queue.get_nowait()
|
||||||
else:
|
else:
|
||||||
@@ -96,6 +100,7 @@ class TaskQueue:
|
|||||||
await item.run()
|
await item.run()
|
||||||
|
|
||||||
self.queue.task_done()
|
self.queue.task_done()
|
||||||
|
|
||||||
self.priority_tasks.discard(item)
|
self.priority_tasks.discard(item)
|
||||||
|
|
||||||
if self.stop and (self.on_exit == 'cancel' or len(self.priority_tasks) == 0):
|
if self.stop and (self.on_exit == 'cancel' or len(self.priority_tasks) == 0):
|
||||||
@@ -107,16 +112,15 @@ class TaskQueue:
|
|||||||
break
|
break
|
||||||
|
|
||||||
if self.mode == 'finite':
|
if self.mode == 'finite':
|
||||||
|
self.stop = True
|
||||||
break
|
break
|
||||||
|
|
||||||
# add dummy task to prevent worker from exiting
|
|
||||||
sleep = QueueItem(asyncio.sleep, 1)
|
|
||||||
self.add(item=sleep)
|
|
||||||
await asyncio.sleep(self.worker_timeout)
|
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
logger.error("%s: Error occurred in worker", err)
|
logger.error("%s: Error occurred in worker", err)
|
||||||
break
|
break
|
||||||
|
|
||||||
|
async def dummy_task(self):
|
||||||
|
await asyncio.sleep(self.worker_timeout)
|
||||||
|
|
||||||
async def run(self, timeout: int = 0):
|
async def run(self, timeout: int = 0):
|
||||||
"""Run the queue until all tasks are completed or the timeout is reached.
|
"""Run the queue until all tasks are completed or the timeout is reached.
|
||||||
|
|
||||||
@@ -128,13 +132,13 @@ class TaskQueue:
|
|||||||
"""
|
"""
|
||||||
start = time.perf_counter()
|
start = time.perf_counter()
|
||||||
try:
|
try:
|
||||||
self.worker_tasks.extend([asyncio.create_task(self.worker()) for _ in range(self.workers)])
|
self.worker_tasks.extend(asyncio.create_task(self.worker()) for _ in range(self.workers))
|
||||||
timeout = timeout or self.timeout
|
timeout = timeout or self.timeout
|
||||||
self.queue_task = asyncio.create_task(self.queue.join())
|
self.queue_task = asyncio.create_task(self.queue.join())
|
||||||
|
|
||||||
if timeout:
|
if timeout:
|
||||||
await asyncio.wait_for(self.queue_task, timeout=timeout)
|
await asyncio.wait_for(self.queue_task, timeout=timeout)
|
||||||
self.stop = True
|
|
||||||
else:
|
else:
|
||||||
await self.queue_task
|
await self.queue_task
|
||||||
|
|
||||||
@@ -142,22 +146,23 @@ class TaskQueue:
|
|||||||
logger.warning("Timed out after %d seconds, %d tasks remaining",
|
logger.warning("Timed out after %d seconds, %d tasks remaining",
|
||||||
time.perf_counter() - start, self.queue.qsize())
|
time.perf_counter() - start, self.queue.qsize())
|
||||||
self.stop = True
|
self.stop = True
|
||||||
|
await self.clean_up()
|
||||||
|
|
||||||
except asyncio.CancelledError:
|
except asyncio.CancelledError:
|
||||||
self.stop = True
|
self.stop = True
|
||||||
|
await self.clean_up()
|
||||||
|
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
logger.warning("%s: An error occurred in %s.run", err, self.__class__.__name__)
|
logger.warning("%s: An error occurred in %s.run", err, self.__class__.__name__)
|
||||||
self.stop = True
|
self.stop = True
|
||||||
|
|
||||||
finally:
|
|
||||||
await self.clean_up()
|
await self.clean_up()
|
||||||
|
|
||||||
|
|
||||||
async def clean_up(self):
|
async def clean_up(self):
|
||||||
"""Clean up tasks in the queue, completing priority tasks if `on_exit` is `complete_priority`"""
|
"""Clean up tasks in the queue, completing priority tasks if `on_exit` is `complete_priority`"""
|
||||||
|
self.stop = True
|
||||||
try:
|
try:
|
||||||
logger.info('cleaning up tasks...')
|
logger.info('cleaning up tasks...')
|
||||||
|
|
||||||
if self.on_exit == 'complete_priority' and (pt := len(self.priority_tasks)) > 0:
|
if self.on_exit == 'complete_priority' and (pt := len(self.priority_tasks)) > 0:
|
||||||
logger.info('Completing %d priority tasks...', pt)
|
logger.info('Completing %d priority tasks...', pt)
|
||||||
self.queue_task = asyncio.create_task(self.queue.join())
|
self.queue_task = asyncio.create_task(self.queue.join())
|
||||||
@@ -166,20 +171,16 @@ class TaskQueue:
|
|||||||
self.cancel()
|
self.cancel()
|
||||||
|
|
||||||
except asyncio.CancelledError:
|
except asyncio.CancelledError:
|
||||||
self.stop = True
|
self.cancel()
|
||||||
|
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
logger.error(f"%s: Error occurred in %s", err, self.__class__.__name__)
|
logger.error(f"%s: Error occurred in %s", err, self.__class__.__name__)
|
||||||
|
|
||||||
finally:
|
|
||||||
self.cancel()
|
self.cancel()
|
||||||
|
|
||||||
def cancel(self):
|
def cancel(self):
|
||||||
"""Cancel all tasks in the queue"""
|
"""Cancel all tasks in the queue"""
|
||||||
try:
|
try:
|
||||||
|
|
||||||
self.queue_task.cancel()
|
self.queue_task.cancel()
|
||||||
|
|
||||||
except asyncio.CancelledError:
|
except asyncio.CancelledError:
|
||||||
...
|
...
|
||||||
|
|
||||||
@@ -188,8 +189,11 @@ class TaskQueue:
|
|||||||
|
|
||||||
def sigint_handle(self, sig, frame):
|
def sigint_handle(self, sig, frame):
|
||||||
logger.info('SIGINT received, cleaning up...')
|
logger.info('SIGINT received, cleaning up...')
|
||||||
self.stop = True
|
if self.stop is False:
|
||||||
self.cancel()
|
self.stop = True
|
||||||
|
else:
|
||||||
|
self.stop = True
|
||||||
|
self.cancel()
|
||||||
|
|
||||||
|
|
||||||
TaskQueue.__doc__ = """TaskQueue is a class that allows you to queue tasks and run them concurrently with a specified number of workers.
|
TaskQueue.__doc__ = """TaskQueue is a class that allows you to queue tasks and run them concurrently with a specified number of workers.
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ class Account(_Base, AccountInfo):
|
|||||||
await self.mt5.initialize()
|
await self.mt5.initialize()
|
||||||
self.connected = await self.mt5.login()
|
self.connected = await self.mt5.login()
|
||||||
if not self.connected:
|
if not self.connected:
|
||||||
raise LoginError("Login failed")
|
raise LoginError(f"Login failed: {self.mt5.error}")
|
||||||
await self.refresh()
|
await self.refresh()
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
|||||||
@@ -183,6 +183,7 @@ class BackTester:
|
|||||||
self.executor.add_strategy(strategy=strategy)
|
self.executor.add_strategy(strategy=strategy)
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
|
self.mt._symbol_select(strategy.symbol.name, True)
|
||||||
self.mt._market_book_add(strategy.symbol.name)
|
self.mt._market_book_add(strategy.symbol.name)
|
||||||
info = self.mt._symbol_info(strategy.symbol.name)
|
info = self.mt._symbol_info(strategy.symbol.name)
|
||||||
time = datetime.fromtimestamp(self.backtest_engine.cursor.time, tz=UTC)
|
time = datetime.fromtimestamp(self.backtest_engine.cursor.time, tz=UTC)
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ class Bot:
|
|||||||
|
|
||||||
if len(self.executor.strategy_runners) == 0:
|
if len(self.executor.strategy_runners) == 0:
|
||||||
logger.warning("No strategies were added to the bot. Exiting in five seconds")
|
logger.warning("No strategies were added to the bot. Exiting in five seconds")
|
||||||
await asyncio.sleep(5)
|
await asyncio.sleep(1)
|
||||||
self.config.shutdown = True
|
self.config.shutdown = True
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
logger.error("%s: Bot initialization failed", err)
|
logger.error("%s: Bot initialization failed", err)
|
||||||
@@ -94,7 +94,7 @@ class Bot:
|
|||||||
|
|
||||||
if len(self.executor.strategy_runners) == 0:
|
if len(self.executor.strategy_runners) == 0:
|
||||||
logger.warning("No strategies were added to the bot. Exiting in 5 seconds")
|
logger.warning("No strategies were added to the bot. Exiting in 5 seconds")
|
||||||
time.sleep(5)
|
time.sleep(1)
|
||||||
self.config.shutdown = True
|
self.config.shutdown = True
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
logger.error("%s: Bot initialization failed", err)
|
logger.error("%s: Bot initialization failed", err)
|
||||||
|
|||||||
+21
-19
@@ -1,7 +1,8 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
from concurrent.futures import ThreadPoolExecutor
|
|
||||||
from typing import Coroutine, Callable
|
|
||||||
import os
|
import os
|
||||||
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
|
from signal import signal, SIGINT
|
||||||
|
from typing import Coroutine, Callable
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
|
|
||||||
from ..core.config import Config
|
from ..core.config import Config
|
||||||
@@ -21,7 +22,7 @@ class Executor:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
executor: ThreadPoolExecutor
|
executor: ThreadPoolExecutor
|
||||||
tasks: list[asyncio.Task]
|
tasks: list[asyncio.Task | asyncio.Future]
|
||||||
config: Config
|
config: Config
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
@@ -32,6 +33,7 @@ class Executor:
|
|||||||
self.tasks = []
|
self.tasks = []
|
||||||
self.config = Config()
|
self.config = Config()
|
||||||
self.timeout = None # Timeout for the executor. For testing purposes only
|
self.timeout = None # Timeout for the executor. For testing purposes only
|
||||||
|
signal(SIGINT, self.sigint_handle)
|
||||||
|
|
||||||
def add_function(self, *, function: Callable, kwargs: dict = None):
|
def add_function(self, *, function: Callable, kwargs: dict = None):
|
||||||
kwargs = kwargs or {}
|
kwargs = kwargs or {}
|
||||||
@@ -78,16 +80,11 @@ class Executor:
|
|||||||
|
|
||||||
async def create_coroutines_task(self):
|
async def create_coroutines_task(self):
|
||||||
""""""
|
""""""
|
||||||
coros = [asyncio.create_task(coroutine) for coroutine in self.coroutines]
|
tasks = [asyncio.create_task(coroutine) for coroutine in self.coroutines]
|
||||||
# task = asyncio.create_task(asyncio.gather(*coros, return_exceptions=False))
|
self.tasks.extend(tasks)
|
||||||
self.tasks.extend(coros)
|
task = asyncio.gather(*tasks, return_exceptions=True)
|
||||||
# loop = asyncio.get_running_loop()
|
|
||||||
# loop.run_in_executor()
|
|
||||||
task = asyncio.gather(*coros, return_exceptions=True)
|
|
||||||
self.tasks.append(task)
|
self.tasks.append(task)
|
||||||
await task
|
await task
|
||||||
# await task
|
|
||||||
# return task
|
|
||||||
|
|
||||||
def run_coroutine_tasks(self):
|
def run_coroutine_tasks(self):
|
||||||
"""Run all coroutines in the executor"""
|
"""Run all coroutines in the executor"""
|
||||||
@@ -109,6 +106,9 @@ class Executor:
|
|||||||
except Exception as err:
|
except Exception as err:
|
||||||
logger.error(f"Error: {err}. Unable to run function: {function.__name__}")
|
logger.error(f"Error: {err}. Unable to run function: {function.__name__}")
|
||||||
|
|
||||||
|
def sigint_handle(self, signum, frame):
|
||||||
|
self.config.shutdown = True
|
||||||
|
|
||||||
async def exit(self):
|
async def exit(self):
|
||||||
"""Shutdown the executor"""
|
"""Shutdown the executor"""
|
||||||
start = asyncio.get_event_loop().time()
|
start = asyncio.get_event_loop().time()
|
||||||
@@ -116,23 +116,25 @@ class Executor:
|
|||||||
while self.config.shutdown is False and self.config.force_shutdown is False:
|
while self.config.shutdown is False and self.config.force_shutdown is False:
|
||||||
if self.timeout is not None and self.timeout < (asyncio.get_event_loop().time() - start):
|
if self.timeout is not None and self.timeout < (asyncio.get_event_loop().time() - start):
|
||||||
self.config.shutdown = True
|
self.config.shutdown = True
|
||||||
timeout = self.timeout or 120
|
break
|
||||||
|
timeout = self.timeout or 30
|
||||||
await asyncio.sleep(timeout)
|
await asyncio.sleep(timeout)
|
||||||
|
|
||||||
print("Shutting down executor")
|
|
||||||
for strategy in self.strategy_runners:
|
for strategy in self.strategy_runners:
|
||||||
strategy.running = False
|
strategy.running = False
|
||||||
self.executor.shutdown(wait=False, cancel_futures=True)
|
|
||||||
|
if self.config.backtest_engine is not None:
|
||||||
|
self.config.backtest_engine.stop_testing = True
|
||||||
|
|
||||||
|
self.executor.shutdown(wait=False, cancel_futures=False)
|
||||||
|
|
||||||
for task in self.tasks:
|
for task in self.tasks:
|
||||||
task.cancel()
|
task.cancel()
|
||||||
|
|
||||||
|
|
||||||
# self.executor.shutdown(wait=False, cancel_futures=True)
|
|
||||||
if self.config.force_shutdown:
|
if self.config.force_shutdown:
|
||||||
os._exit(1)
|
os._exit(1)
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
logger.error(f"Error: {err}. Unable to shutdown executor")
|
logger.error("%s: Unable to shutdown executor", err)
|
||||||
|
os._exit(1)
|
||||||
|
|
||||||
def execute(self, *, workers: int = 5):
|
def execute(self, *, workers: int = 5):
|
||||||
"""Run the strategies with a threadpool executor.
|
"""Run the strategies with a threadpool executor.
|
||||||
@@ -143,7 +145,7 @@ class Executor:
|
|||||||
Notes:
|
Notes:
|
||||||
No matter the number specified, the executor will always use a minimum of 5 workers.
|
No matter the number specified, the executor will always use a minimum of 5 workers.
|
||||||
"""
|
"""
|
||||||
workers_ = len(self.strategy_runners) + len(self.functions) + len(self.coroutine_threads) + 2
|
workers_ = len(self.strategy_runners) + len(self.functions) + len(self.coroutine_threads) + 3
|
||||||
workers = max(workers, workers_)
|
workers = max(workers, workers_)
|
||||||
with ThreadPoolExecutor(max_workers=workers) as executor:
|
with ThreadPoolExecutor(max_workers=workers) as executor:
|
||||||
self.executor = executor
|
self.executor = executor
|
||||||
|
|||||||
@@ -28,6 +28,15 @@ class Order(_Base, TradeRequest):
|
|||||||
kwargs = {"action": TradeAction.DEAL, "type_time": OrderTime.DAY, "type_filling": OrderFilling.FOK, **kwargs}
|
kwargs = {"action": TradeAction.DEAL, "type_time": OrderTime.DAY, "type_filling": OrderFilling.FOK, **kwargs}
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
|
def modify(self, **kwargs):
|
||||||
|
"""Modify the order object with keyword arguments.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
**kwargs: Keyword arguments must match the attributes of TradeRequest as well as the attributes of
|
||||||
|
Order class as specified in the annotations in the class definition.
|
||||||
|
"""
|
||||||
|
self.set_attributes(**kwargs)
|
||||||
|
|
||||||
async def orders_total(self):
|
async def orders_total(self):
|
||||||
"""Get the number of active pending orders.
|
"""Get the number of active pending orders.
|
||||||
|
|
||||||
|
|||||||
@@ -24,23 +24,38 @@ class Positions:
|
|||||||
|
|
||||||
mt5: MetaTrader | MetaBackTester
|
mt5: MetaTrader | MetaBackTester
|
||||||
positions: tuple[TradePosition, ...]
|
positions: tuple[TradePosition, ...]
|
||||||
|
total_positions: int
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
"""Get Open Positions"""
|
"""Get Open Positions"""
|
||||||
self.config = Config()
|
self.config = Config()
|
||||||
self.mt5 = MetaTrader() if self.config.mode != "backtest" else MetaBackTester()
|
self.mt5 = MetaTrader() if self.config.mode != "backtest" else MetaBackTester()
|
||||||
self.positions = ()
|
self.positions = ()
|
||||||
|
self.total_positions = 0
|
||||||
|
|
||||||
@backoff_decorator
|
@backoff_decorator
|
||||||
async def get_positions(self) -> tuple[TradePosition, ...]:
|
async def get_positions(self, *, symbol: str = None, ticket: int = None, group: str = None) -> tuple[TradePosition, ...]:
|
||||||
"""Get open positions with the ability to filter by symbol or ticket.
|
"""Get open positions with the ability to filter by symbol, ticket or group of symbols.
|
||||||
|
Args:
|
||||||
|
symbol (Optional[str]): Financial instrument name. If symbol is provided, ticket is ignored.
|
||||||
|
ticket (Optional[int]): Position ticket.
|
||||||
|
group (Optional[str]): Group of symbols.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
tuple[TradePosition, ...]: A tuple of open trade positions
|
tuple[TradePosition, ...]: A tuple of open trade positions
|
||||||
"""
|
"""
|
||||||
positions = await self.mt5.positions_get()
|
kwargs = {}
|
||||||
|
if symbol is not None:
|
||||||
|
kwargs["symbol"] = symbol
|
||||||
|
ticket = None
|
||||||
|
if ticket is not None:
|
||||||
|
kwargs["ticket"] = ticket
|
||||||
|
if group is not None:
|
||||||
|
kwargs["group"] = group
|
||||||
|
positions = await self.mt5.positions_get(**kwargs)
|
||||||
if positions is not None:
|
if positions is not None:
|
||||||
self.positions = tuple(TradePosition(**pos._asdict()) for pos in positions)
|
self.positions = tuple(TradePosition(**pos._asdict()) for pos in positions)
|
||||||
|
self.total_positions = len(self.positions)
|
||||||
return self.positions
|
return self.positions
|
||||||
logger.warning("Failed to get open positions")
|
logger.warning("Failed to get open positions")
|
||||||
return ()
|
return ()
|
||||||
@@ -130,3 +145,9 @@ class Positions:
|
|||||||
*(self.close_position(position=position) for position in positions), return_exceptions=True
|
*(self.close_position(position=position) for position in positions), return_exceptions=True
|
||||||
)
|
)
|
||||||
return len([res for res in results if (isinstance(res, OrderSendResult) and res.retcode == 10009)])
|
return len([res for res in results if (isinstance(res, OrderSendResult) and res.retcode == 10009)])
|
||||||
|
|
||||||
|
async def get_total_positions(self) -> int:
|
||||||
|
"""Get total number of open positions."""
|
||||||
|
total = await self.mt5.positions_total()
|
||||||
|
self.total_positions = total or self.total_positions
|
||||||
|
return self.total_positions
|
||||||
|
|||||||
@@ -36,6 +36,11 @@ class RAM:
|
|||||||
self.open_limit = kwargs.get("open_limit", 3)
|
self.open_limit = kwargs.get("open_limit", 3)
|
||||||
self.fixed_amount = kwargs.get("fixed_amount", None)
|
self.fixed_amount = kwargs.get("fixed_amount", None)
|
||||||
|
|
||||||
|
def modify_ram(self, **kwargs):
|
||||||
|
"""Modify the Risk Assessment and Management with the provided keyword arguments.
|
||||||
|
"""
|
||||||
|
[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 margin_free.
|
"""Calculate the amount to risk per trade as a percentage of margin_free.
|
||||||
|
|
||||||
|
|||||||
@@ -78,7 +78,8 @@ class Result:
|
|||||||
"""Serialize the trade records and strategy parameters"""
|
"""Serialize the trade records and strategy parameters"""
|
||||||
try:
|
try:
|
||||||
return str(value)
|
return str(value)
|
||||||
except (ValueError, TypeError) as _:
|
except Exception as err:
|
||||||
|
logger.error("%s: Unable to serialize value", err)
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
async def to_json(self):
|
async def to_json(self):
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ from logging import getLogger
|
|||||||
|
|
||||||
from ..core.models import OrderSendResult, TradePosition
|
from ..core.models import OrderSendResult, TradePosition
|
||||||
from ..core.config import Config
|
from ..core.config import Config
|
||||||
from ..core.backtesting.backtest_controller import BackTestController
|
|
||||||
from .positions import Positions
|
from .positions import Positions
|
||||||
|
from .._utils import backtest_sleep
|
||||||
|
|
||||||
logger = getLogger(__name__)
|
logger = getLogger(__name__)
|
||||||
|
|
||||||
@@ -26,13 +26,13 @@ def delta(obj: time) -> timedelta:
|
|||||||
return timedelta(hours=obj.hour, minutes=obj.minute, seconds=obj.second, microseconds=obj.microsecond)
|
return timedelta(hours=obj.hour, minutes=obj.minute, seconds=obj.second, microseconds=obj.microsecond)
|
||||||
|
|
||||||
|
|
||||||
async def backtest_sleep(secs):
|
# async def backtest_sleep(secs):
|
||||||
"""An async sleep function for use during backtesting."""
|
# """An async sleep function for use during backtesting."""
|
||||||
btc = BackTestController()
|
# btc = BackTestController()
|
||||||
config = Config()
|
# config = Config()
|
||||||
sleep = config.backtest_engine.cursor.time + secs
|
# sleep = config.backtest_engine.cursor.time + secs
|
||||||
while sleep > config.backtest_engine.cursor.time:
|
# while sleep > config.backtest_engine.cursor.time:
|
||||||
btc.wait()
|
# btc.wait()
|
||||||
|
|
||||||
|
|
||||||
class Session:
|
class Session:
|
||||||
|
|||||||
@@ -53,14 +53,19 @@ class Symbol(_Base, SymbolInfo):
|
|||||||
None: If request was unsuccessful
|
None: If request was unsuccessful
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
tick = await self.mt5.symbol_info_tick(name or self.name)
|
if not name:
|
||||||
|
tick = await self.mt5.symbol_info_tick(self.name)
|
||||||
|
else:
|
||||||
|
await self.mt5.symbol_select(name, True)
|
||||||
|
await self.mt5.market_book_add(name)
|
||||||
|
tick = await self.mt5.symbol_info_tick(name)
|
||||||
if tick is not None:
|
if tick is not None:
|
||||||
tick = Tick(**tick._asdict())
|
tick = Tick(**tick._asdict())
|
||||||
setattr(self, "tick", tick) if not name else ...
|
setattr(self, "tick", tick) if not name else ...
|
||||||
return tick
|
return tick
|
||||||
return None
|
return None
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
logger.warning(f"{err}: Unable to get tick for {self.name}")
|
logger.warning("%s: Unable to get tick for %s", err, self.name)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
async def symbol_select(self, *, enable: bool = True) -> bool:
|
async def symbol_select(self, *, enable: bool = True) -> bool:
|
||||||
|
|||||||
@@ -11,8 +11,7 @@ async def test_bot():
|
|||||||
symbols = [ForexSymbol(name=sym) for sym in syms]
|
symbols = [ForexSymbol(name=sym) for sym in syms]
|
||||||
strategies = [Chaos(symbol=symbol, name="test_chaos") for symbol in symbols]
|
strategies = [Chaos(symbol=symbol, name="test_chaos") for symbol in symbols]
|
||||||
bot = Bot()
|
bot = Bot()
|
||||||
bot.config.task_queue.worker_timeout = 2
|
bot.executor.timeout = 5
|
||||||
bot.executor.timeout = 10
|
|
||||||
bot.add_strategies(strategies=strategies)
|
bot.add_strategies(strategies=strategies)
|
||||||
await bot.initialize()
|
await bot.initialize()
|
||||||
bot.executor.execute()
|
bot.executor.execute()
|
||||||
|
|||||||
Reference in New Issue
Block a user