mirror of
https://github.com/Ichinga-Samuel/aiomql.git
synced 2026-08-17 22:08:06 +00:00
v4
This commit is contained in:
@@ -43,6 +43,7 @@ htmlcov/
|
|||||||
.coverage
|
.coverage
|
||||||
.coverage.*
|
.coverage.*
|
||||||
.cache
|
.cache
|
||||||
|
.ruff_cache
|
||||||
.pytest_cache
|
.pytest_cache
|
||||||
nosetests.xml
|
nosetests.xml
|
||||||
coverage.xml
|
coverage.xml
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
import asyncio
|
|
||||||
|
|
||||||
|
|
||||||
def run():
|
|
||||||
for i in range(10):
|
|
||||||
print('running')
|
|
||||||
|
|
||||||
|
|
||||||
async def run_async():
|
|
||||||
for i in range(10):
|
|
||||||
await asyncio.sleep(1)
|
|
||||||
print('running async')
|
|
||||||
|
|
||||||
|
|
||||||
async def main():
|
|
||||||
await run_async()
|
|
||||||
run()
|
|
||||||
|
|
||||||
|
|
||||||
asyncio.run(main())
|
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
# Table of Contents
|
||||||
|
|
||||||
|
* [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)
|
||||||
|
|
||||||
|
<a id="aiomql.contrib.symbols.forex_symbol"></a>
|
||||||
|
|
||||||
|
# aiomql.contrib.symbols.forex\_symbol
|
||||||
|
|
||||||
|
<a id="aiomql.contrib.symbols.forex_symbol.ForexSymbol"></a>
|
||||||
|
|
||||||
|
## ForexSymbol Objects
|
||||||
|
|
||||||
|
```python
|
||||||
|
class ForexSymbol(Symbol)
|
||||||
|
```
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
```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
|
||||||
|
|
||||||
|
```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**:
|
||||||
|
|
||||||
|
- `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
|
||||||
|
|
||||||
|
```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**:
|
||||||
|
|
||||||
|
- `amount` _float_ - Amount to trade
|
||||||
|
- `points` _float_ - Number of points
|
||||||
|
- `round_down` - round down the computed volume to the nearest step default True
|
||||||
@@ -38,14 +38,15 @@ The MetaTrader Class provides an asynchronous wrapper around the MetaTrader5 API
|
|||||||
- [history\_deals\_total](#history_deals_total)
|
- [history\_deals\_total](#history_deals_total)
|
||||||
- [history\_deals\_get](#history_deals_get)
|
- [history\_deals\_get](#history_deals_get)
|
||||||
|
|
||||||
<a id="MetaTrader"></a>
|
<a id="meta_trader.meta_trader"></a>
|
||||||
### MetaTrader
|
### MetaTrader
|
||||||
```python
|
```python
|
||||||
class MetaTrader(metaclass=BaseMeta)
|
class MetaTrader(MetaCore)
|
||||||
```
|
```
|
||||||
The MetaTrader class is a wrapper around the MetaTrader terminal.
|
The MetaTrader class is a wrapper around the MetaTrader terminal.
|
||||||
It provides methods for connecting to the MetaTrader terminal and retrieving data from it.
|
It provides methods for connecting to the MetaTrader terminal and retrieving data from it.
|
||||||
#### Attributes
|
|
||||||
|
#### Attributes:
|
||||||
| Name | Type | Description | Default |
|
| Name | Type | Description | Default |
|
||||||
|-------|-------|--------------------------------------------------------|------------------------|
|
|-------|-------|--------------------------------------------------------|------------------------|
|
||||||
| error | Error | The last error encountered by the MetaTrader terminal. | Error(1, 'Successful') |
|
| error | Error | The last error encountered by the MetaTrader terminal. | Error(1, 'Successful') |
|
||||||
@@ -62,7 +63,7 @@ async def __aenter__() -> 'MetaTrader'
|
|||||||
Async context manager entry point.
|
Async context manager entry point.
|
||||||
Initializes the connection to the MetaTrader terminal.
|
Initializes the connection to the MetaTrader terminal.
|
||||||
|
|
||||||
#### Returns
|
#### Returns:
|
||||||
| Type | Description |
|
| Type | Description |
|
||||||
|--------------|-------------------------------------|
|
|--------------|-------------------------------------|
|
||||||
| `MetaTrader` | An instance of the MetaTrader class |
|
| `MetaTrader` | An instance of the MetaTrader class |
|
||||||
@@ -621,4 +622,4 @@ Call without parameters. Return closed deals on all symbols
|
|||||||
#### Returns
|
#### Returns
|
||||||
| Type | Description |
|
| Type | Description |
|
||||||
|--------------------|----------------------------------------------------|
|
|--------------------|----------------------------------------------------|
|
||||||
| `tuple[TradeDeal]` | A tuple of closed trade deals as TradeDeal objects |
|
| `tuple[TradeDeal]` | A tuple of closed trade deals as TradeDeal objects |
|
||||||
|
|||||||
@@ -1,4 +1,2 @@
|
|||||||
line-length = 150
|
line-length = 150
|
||||||
target-version = "py311"
|
target-version = "py311"
|
||||||
[format]
|
|
||||||
skip-magic-trailing-comma=true
|
|
||||||
|
|||||||
+3
-11
@@ -12,16 +12,8 @@ from aiomql.core.backtesting import BackTestEngine
|
|||||||
async def back_tester():
|
async def back_tester():
|
||||||
config = Config()
|
config = Config()
|
||||||
config.mode = "backtest"
|
config.mode = "backtest"
|
||||||
logging.basicConfig(
|
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s")
|
||||||
level=logging.INFO,
|
syms = ["Volatility 75 Index", "Volatility 100 Index", "Volatility 25 Index", "Volatility 10 Index"]
|
||||||
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
|
||||||
)
|
|
||||||
syms = [
|
|
||||||
"Volatility 75 Index",
|
|
||||||
"Volatility 100 Index",
|
|
||||||
"Volatility 25 Index",
|
|
||||||
"Volatility 10 Index",
|
|
||||||
]
|
|
||||||
symbols = [ForexSymbol(name=sym) for sym in syms]
|
symbols = [ForexSymbol(name=sym) for sym in syms]
|
||||||
strategies = [Chaos(symbol=symbol) for symbol in symbols]
|
strategies = [Chaos(symbol=symbol) for symbol in symbols]
|
||||||
start = datetime(2024, 5, 1, tzinfo=UTC)
|
start = datetime(2024, 5, 1, tzinfo=UTC)
|
||||||
@@ -35,7 +27,7 @@ async def back_tester():
|
|||||||
close_open_positions_on_exit=True,
|
close_open_positions_on_exit=True,
|
||||||
assign_to_config=True,
|
assign_to_config=True,
|
||||||
preload=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)
|
||||||
backtester.add_strategies(strategies=strategies)
|
backtester.add_strategies(strategies=strategies)
|
||||||
|
|||||||
+1
-4
@@ -6,10 +6,7 @@ from aiomql.contrib.symbols import ForexSymbol
|
|||||||
|
|
||||||
|
|
||||||
def chaos_bot():
|
def chaos_bot():
|
||||||
logging.basicConfig(
|
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s")
|
||||||
level=logging.INFO,
|
|
||||||
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
|
||||||
)
|
|
||||||
syms = ["Volatility 75 Index", "Volatility 100 Index", "Volatility 50 Index"]
|
syms = ["Volatility 75 Index", "Volatility 100 Index", "Volatility 50 Index"]
|
||||||
symbols = [ForexSymbol(name=sym) for sym in syms]
|
symbols = [ForexSymbol(name=sym) for sym in syms]
|
||||||
strategies = [Chaos(symbol=symbol) for symbol in symbols]
|
strategies = [Chaos(symbol=symbol) for symbol in symbols]
|
||||||
|
|||||||
+7
-26
@@ -1,4 +1,5 @@
|
|||||||
"""Utility functions for aiomql."""
|
"""Utility functions for aiomql."""
|
||||||
|
|
||||||
import decimal
|
import decimal
|
||||||
import random
|
import random
|
||||||
from functools import wraps, partial
|
from functools import wraps, partial
|
||||||
@@ -26,13 +27,9 @@ def dict_to_string(data: dict, multi=False) -> str:
|
|||||||
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 backoff_decorator(
|
def backoff_decorator(func=None, *, max_retries: int = 2, retries: int = 0, error="") -> callable:
|
||||||
func=None, *, max_retries: int = 2, retries: int = 0, error=""
|
|
||||||
) -> callable:
|
|
||||||
if func is None:
|
if func is None:
|
||||||
return partial(
|
return partial(backoff_decorator, max_retries=max_retries, retries=retries, error=error)
|
||||||
backoff_decorator, max_retries=max_retries, retries=retries, error=error
|
|
||||||
)
|
|
||||||
|
|
||||||
@wraps(func)
|
@wraps(func)
|
||||||
async def wrapper(*args, **kwargs):
|
async def wrapper(*args, **kwargs):
|
||||||
@@ -57,17 +54,9 @@ def backoff_decorator(
|
|||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
def error_handler(
|
def error_handler(func=None, *, msg="", exe=Exception, response=None, log_error_msg=True):
|
||||||
func=None, *, msg="", exe=Exception, response=None, log_error_msg=True
|
|
||||||
):
|
|
||||||
if func is None:
|
if func is None:
|
||||||
return partial(
|
return partial(error_handler, msg=msg, exe=exe, response=response, log_error_msg=log_error_msg)
|
||||||
error_handler,
|
|
||||||
msg=msg,
|
|
||||||
exe=exe,
|
|
||||||
response=response,
|
|
||||||
log_error_msg=log_error_msg,
|
|
||||||
)
|
|
||||||
|
|
||||||
@wraps(func)
|
@wraps(func)
|
||||||
async def wrapper(*args, **kwargs):
|
async def wrapper(*args, **kwargs):
|
||||||
@@ -82,17 +71,9 @@ def error_handler(
|
|||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
def error_handler_sync(
|
def error_handler_sync(func=None, *, msg="", exe=Exception, response=None, log_error_msg=True):
|
||||||
func=None, *, msg="", exe=Exception, response=None, log_error_msg=True
|
|
||||||
):
|
|
||||||
if func is None:
|
if func is None:
|
||||||
return partial(
|
return partial(error_handler, msg=msg, exe=exe, response=response, log_error_msg=log_error_msg)
|
||||||
error_handler,
|
|
||||||
msg=msg,
|
|
||||||
exe=exe,
|
|
||||||
response=response,
|
|
||||||
log_error_msg=log_error_msg,
|
|
||||||
)
|
|
||||||
|
|
||||||
@wraps(func)
|
@wraps(func)
|
||||||
def wrapper(*args, **kwargs):
|
def wrapper(*args, **kwargs):
|
||||||
|
|||||||
@@ -3,21 +3,11 @@ from ...lib.candle import Candle, Candles
|
|||||||
|
|
||||||
def find_bearish_fractal(candles: Candles) -> Candle | None:
|
def find_bearish_fractal(candles: Candles) -> Candle | None:
|
||||||
for i in range(len(candles) - 3, 1, -1):
|
for i in range(len(candles) - 3, 1, -1):
|
||||||
if candles[i].high > max(
|
if candles[i].high > max(candles[i - 1].high, candles[i + 1].high, candles[i - 2].high, candles[i + 2].high):
|
||||||
candles[i - 1].high,
|
|
||||||
candles[i + 1].high,
|
|
||||||
candles[i - 2].high,
|
|
||||||
candles[i + 2].high,
|
|
||||||
):
|
|
||||||
return candles[i]
|
return candles[i]
|
||||||
|
|
||||||
|
|
||||||
def find_bullish_fractal(candles: Candles) -> Candle | None:
|
def find_bullish_fractal(candles: Candles) -> Candle | None:
|
||||||
for i in range(len(candles) - 3, 1, -1):
|
for i in range(len(candles) - 3, 1, -1):
|
||||||
if candles[i].low < min(
|
if candles[i].low < min(candles[i - 1].low, candles[i + 1].low, candles[i - 2].low, candles[i + 2].low):
|
||||||
candles[i - 1].low,
|
|
||||||
candles[i + 1].low,
|
|
||||||
candles[i - 2].low,
|
|
||||||
candles[i + 2].low,
|
|
||||||
):
|
|
||||||
return candles[i]
|
return candles[i]
|
||||||
|
|||||||
@@ -20,70 +20,40 @@ class Chaos(Strategy):
|
|||||||
fast_ema: int
|
fast_ema: int
|
||||||
slow_ema: int
|
slow_ema: int
|
||||||
tracker: Tracker
|
tracker: Tracker
|
||||||
parameters = {
|
parameters = {"fast_ema": 8, "slow_ema": 20, "ltf": TimeFrame.M1, "htf": TimeFrame.M2, "lcc": 100, "hcc": 100}
|
||||||
"fast_ema": 8,
|
|
||||||
"slow_ema": 20,
|
|
||||||
"ltf": TimeFrame.M1,
|
|
||||||
"htf": TimeFrame.M2,
|
|
||||||
"lcc": 100,
|
|
||||||
"hcc": 100,
|
|
||||||
}
|
|
||||||
|
|
||||||
def __init__(
|
def __init__(self, *, symbol: ForexSymbol, params: dict = None, sessions=None, name="Chaos"):
|
||||||
self, *, symbol: ForexSymbol, params: dict = None, sessions=None, name="Chaos"
|
|
||||||
):
|
|
||||||
super().__init__(symbol=symbol, params=params, sessions=sessions, name=name)
|
super().__init__(symbol=symbol, params=params, sessions=sessions, name=name)
|
||||||
self.tracker = Tracker(snooze=self.ltf.seconds)
|
self.tracker = Tracker(snooze=self.ltf.seconds)
|
||||||
self.trader = ScalpTrader(symbol=self.symbol)
|
self.trader = ScalpTrader(symbol=self.symbol)
|
||||||
|
|
||||||
async def check_trend(self):
|
async def check_trend(self):
|
||||||
try:
|
try:
|
||||||
candles = await self.symbol.copy_rates_from_pos(
|
candles = await self.symbol.copy_rates_from_pos(timeframe=self.htf, count=self.hcc)
|
||||||
timeframe=self.htf, count=self.hcc
|
if (current := candles[-1]) and current.time < self.tracker.trend_time and current.close == self.tracker.last_trend_price:
|
||||||
)
|
|
||||||
if (
|
|
||||||
(current := candles[-1])
|
|
||||||
and current.time < self.tracker.trend_time
|
|
||||||
and current.close == self.tracker.last_trend_price
|
|
||||||
):
|
|
||||||
self.tracker.update(new=False, order_type=None, snooze=5)
|
self.tracker.update(new=False, order_type=None, snooze=5)
|
||||||
return
|
return
|
||||||
self.tracker.update(
|
self.tracker.update(new=True, trend_time=current.time, last_trend_price=current.close)
|
||||||
new=True, trend_time=current.time, last_trend_price=current.close
|
|
||||||
)
|
|
||||||
candles.ta.ema(length=self.slow_ema, append=True, fillna=0)
|
candles.ta.ema(length=self.slow_ema, append=True, fillna=0)
|
||||||
candles.ta.ema(length=self.fast_ema, append=True, fillna=0)
|
candles.ta.ema(length=self.fast_ema, append=True, fillna=0)
|
||||||
candles.rename(
|
candles.rename(inplace=True, **{f"EMA_{self.fast_ema}": "fast", f"EMA_{self.slow_ema}": "slow"})
|
||||||
inplace=True,
|
|
||||||
**{f"EMA_{self.fast_ema}": "fast", f"EMA_{self.slow_ema}": "slow"},
|
|
||||||
)
|
|
||||||
order_type = random.choice([OrderType.BUY, OrderType.SELL])
|
order_type = random.choice([OrderType.BUY, OrderType.SELL])
|
||||||
if order_type == OrderType.BUY:
|
if order_type == OrderType.BUY:
|
||||||
self.tracker.update(
|
self.tracker.update(trend="bullish", snooze=self.htf.seconds, order_type=OrderType.BUY)
|
||||||
trend="bullish", snooze=self.htf.seconds, order_type=OrderType.BUY
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
self.tracker.update(
|
self.tracker.update(trend="bearish", snooze=self.htf.seconds, order_type=OrderType.SELL)
|
||||||
trend="bearish", snooze=self.htf.seconds, order_type=OrderType.SELL
|
|
||||||
)
|
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
logger.error(f"{err}. Failed to check trend")
|
logger.error(f"{err}. Failed to check trend")
|
||||||
self.tracker.update(
|
self.tracker.update(trend="ranging", snooze=self.ltf.seconds, order_type=None)
|
||||||
trend="ranging", snooze=self.ltf.seconds, order_type=None
|
|
||||||
)
|
|
||||||
|
|
||||||
async def trade(self):
|
async def trade(self):
|
||||||
try:
|
try:
|
||||||
await self.check_trend()
|
await self.check_trend()
|
||||||
if self.tracker.order_type is not None:
|
if self.tracker.order_type is not None:
|
||||||
await self.trader.place_trade(
|
await self.trader.place_trade(order_type=self.tracker.order_type, parameters=self.parameters)
|
||||||
order_type=self.tracker.order_type, parameters=self.parameters
|
|
||||||
)
|
|
||||||
self.tracker.update(order_type=None)
|
self.tracker.update(order_type=None)
|
||||||
await self.sleep(secs=self.tracker.snooze)
|
await self.sleep(secs=self.tracker.snooze)
|
||||||
else:
|
else:
|
||||||
await self.sleep(secs=self.tracker.snooze)
|
await self.sleep(secs=self.tracker.snooze)
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
logger.error(
|
logger.error(f"{err}. Failed to trade {self.symbol.name} with {self.__class__.__name__}")
|
||||||
f"{err}. Failed to trade {self.symbol.name} with {self.__class__.__name__}"
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -24,46 +24,23 @@ class FingerTrap(Strategy):
|
|||||||
tcc: int
|
tcc: int
|
||||||
trader: Trader
|
trader: Trader
|
||||||
tracker: Tracker
|
tracker: Tracker
|
||||||
parameters = {
|
parameters = {"fast_ema": 8, "slow_ema": 20, "etf": TimeFrame.M5, "ttf": TimeFrame.H1, "entry_ema": 5, "tcc": 672, "ecc": 3360}
|
||||||
"fast_ema": 8,
|
|
||||||
"slow_ema": 20,
|
|
||||||
"etf": TimeFrame.M5,
|
|
||||||
"ttf": TimeFrame.H1,
|
|
||||||
"entry_ema": 5,
|
|
||||||
"tcc": 672,
|
|
||||||
"ecc": 3360,
|
|
||||||
}
|
|
||||||
|
|
||||||
def __init__(
|
def __init__(self, *, symbol: Symbol, params: dict | None = None, trader: Trader = None, sessions: Sessions = None, name: str = "FingerTrap"):
|
||||||
self,
|
|
||||||
*,
|
|
||||||
symbol: Symbol,
|
|
||||||
params: dict | None = None,
|
|
||||||
trader: Trader = None,
|
|
||||||
sessions: Sessions = None,
|
|
||||||
name: str = "FingerTrap",
|
|
||||||
):
|
|
||||||
super().__init__(symbol=symbol, params=params, sessions=sessions, name=name)
|
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.ttf.seconds)
|
self.tracker: Tracker = Tracker(snooze=self.ttf.seconds)
|
||||||
|
|
||||||
async def check_trend(self):
|
async def check_trend(self):
|
||||||
try:
|
try:
|
||||||
candles: Candles = await self.symbol.copy_rates_from_pos(
|
candles: Candles = await self.symbol.copy_rates_from_pos(timeframe=self.ttf, count=self.tcc)
|
||||||
timeframe=self.ttf, count=self.tcc
|
|
||||||
)
|
|
||||||
if (current := candles[-1]) and current.time < self.tracker.trend_time:
|
if (current := candles[-1]) and current.time < self.tracker.trend_time:
|
||||||
self.tracker.update(new=False, order_type=None)
|
self.tracker.update(new=False, order_type=None)
|
||||||
return
|
return
|
||||||
self.tracker.update(
|
self.tracker.update(new=True, trend_time=current.time, last_trend_price=current.close)
|
||||||
new=True, trend_time=current.time, last_trend_price=current.close
|
|
||||||
)
|
|
||||||
candles.ta.ema(length=self.slow_ema, append=True, fillna=0)
|
candles.ta.ema(length=self.slow_ema, append=True, fillna=0)
|
||||||
candles.ta.ema(length=self.fast_ema, append=True, fillna=0)
|
candles.ta.ema(length=self.fast_ema, append=True, fillna=0)
|
||||||
candles.rename(
|
candles.rename(inplace=True, **{f"EMA_{self.fast_ema}": "fast", f"EMA_{self.slow_ema}": "slow"})
|
||||||
inplace=True,
|
|
||||||
**{f"EMA_{self.fast_ema}": "fast", f"EMA_{self.slow_ema}": "slow"},
|
|
||||||
)
|
|
||||||
|
|
||||||
fas = candles.ta_lib.above(candles.fast, candles.slow)
|
fas = candles.ta_lib.above(candles.fast, candles.slow)
|
||||||
fbs = candles.ta_lib.below(candles.fast, candles.slow)
|
fbs = candles.ta_lib.below(candles.fast, candles.slow)
|
||||||
@@ -75,52 +52,34 @@ class FingerTrap(Strategy):
|
|||||||
elif fbs.iloc[-1] and cbf.iloc[-1] and current.is_bearish():
|
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(
|
self.tracker.update(trend="ranging", snooze=self.ttf.seconds, order_type=None)
|
||||||
trend="ranging", snooze=self.ttf.seconds, order_type=None
|
|
||||||
)
|
|
||||||
self.tracker.update(trend="bullish") # remove this line
|
self.tracker.update(trend="bullish") # remove this line
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
logger.error(
|
logger.error(f"{err} for {self.symbol} in {self.__class__.__name__}.check_trend")
|
||||||
f"{err} for {self.symbol} in {self.__class__.__name__}.check_trend"
|
|
||||||
)
|
|
||||||
self.tracker.update(snooze=self.ttf.seconds, order_type=None)
|
self.tracker.update(snooze=self.ttf.seconds, order_type=None)
|
||||||
|
|
||||||
async def confirm_trend(self):
|
async def confirm_trend(self):
|
||||||
try:
|
try:
|
||||||
candles = await self.symbol.copy_rates_from_pos(
|
candles = await self.symbol.copy_rates_from_pos(timeframe=self.etf, count=self.ecc)
|
||||||
timeframe=self.etf, count=self.ecc
|
|
||||||
)
|
|
||||||
if (current := candles[-1]) and current.time < self.tracker.entry_time:
|
if (current := candles[-1]) and current.time < self.tracker.entry_time:
|
||||||
self.tracker.update(new=False, order_type=None)
|
self.tracker.update(new=False, order_type=None)
|
||||||
return
|
return
|
||||||
self.tracker.update(
|
self.tracker.update(new=True, trend_time=current.time, last_entry_price=current.close)
|
||||||
new=True, trend_time=current.time, last_entry_price=current.close
|
|
||||||
)
|
|
||||||
candles.ta.ema(length=self.entry_ema, append=True)
|
candles.ta.ema(length=self.entry_ema, append=True)
|
||||||
candles.rename(**{f"EMA_{self.entry_ema}": "ema"})
|
candles.rename(**{f"EMA_{self.entry_ema}": "ema"})
|
||||||
candles["cae"] = candles.ta_lib.cross(candles.close, candles.ema)
|
candles["cae"] = candles.ta_lib.cross(candles.close, candles.ema)
|
||||||
candles["cbe"] = candles.ta_lib.cross(
|
candles["cbe"] = candles.ta_lib.cross(candles.close, candles.ema, above=False)
|
||||||
candles.close, candles.ema, above=False
|
|
||||||
)
|
|
||||||
current = candles[-1]
|
current = candles[-1]
|
||||||
if (
|
if self.tracker.bullish and True or current.cae: # change True to current.cae
|
||||||
self.tracker.bullish and True or current.cae
|
|
||||||
): # change True to current.cae
|
|
||||||
sl = find_bullish_fractal(candles).low
|
sl = find_bullish_fractal(candles).low
|
||||||
self.tracker.update(
|
self.tracker.update(snooze=self.ttf.seconds, order_type=OrderType.BUY, sl=sl)
|
||||||
snooze=self.ttf.seconds, order_type=OrderType.BUY, sl=sl
|
|
||||||
)
|
|
||||||
elif self.tracker.bearish and current.cbe:
|
elif self.tracker.bearish and current.cbe:
|
||||||
sl = find_bearish_fractal(candles).high
|
sl = find_bearish_fractal(candles).high
|
||||||
self.tracker.update(
|
self.tracker.update(snooze=self.ttf.seconds, order_type=OrderType.SELL, sl=sl)
|
||||||
snooze=self.ttf.seconds, order_type=OrderType.SELL, sl=sl
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
self.tracker.update(snooze=self.etf.seconds, order_type=None)
|
self.tracker.update(snooze=self.etf.seconds, order_type=None)
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
logger.error(
|
logger.error(f"{err} for {self.symbol} in {self.__class__.__name__}.confirm_trend")
|
||||||
f"{err} for {self.symbol} in {self.__class__.__name__}.confirm_trend"
|
|
||||||
)
|
|
||||||
self.tracker.update(snooze=self.etf.seconds, order_type=None)
|
self.tracker.update(snooze=self.etf.seconds, order_type=None)
|
||||||
|
|
||||||
async def watch_market(self):
|
async def watch_market(self):
|
||||||
@@ -138,11 +97,7 @@ class FingerTrap(Strategy):
|
|||||||
if self.tracker.order_type is None:
|
if self.tracker.order_type is None:
|
||||||
await self.sleep(secs=self.tracker.snooze)
|
await self.sleep(secs=self.tracker.snooze)
|
||||||
return
|
return
|
||||||
await self.trader.place_trade(
|
await self.trader.place_trade(order_type=self.tracker.order_type, parameters=self.parameters, sl=self.tracker.sl)
|
||||||
order_type=self.tracker.order_type,
|
|
||||||
parameters=self.parameters,
|
|
||||||
sl=self.tracker.sl,
|
|
||||||
)
|
|
||||||
await self.sleep(secs=self.tracker.snooze)
|
await self.sleep(secs=self.tracker.snooze)
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
logger.error(f"{err} For {self.symbol} in {self.__class__.__name__}.trade")
|
logger.error(f"{err} For {self.symbol} in {self.__class__.__name__}.trade")
|
||||||
|
|||||||
@@ -24,9 +24,7 @@ class ForexSymbol(Symbol):
|
|||||||
points = amount / (volume * self.point * self.trade_contract_size)
|
points = amount / (volume * self.point * self.trade_contract_size)
|
||||||
return points
|
return points
|
||||||
|
|
||||||
async def compute_volume_points(
|
async def compute_volume_points(self, *, amount: float, points: float, round_down: bool = False) -> float:
|
||||||
self, *, amount: float, points: float, round_down: bool = False
|
|
||||||
) -> float:
|
|
||||||
"""Compute the volume required for a trade. Given the amount and the number of points.
|
"""Compute the volume required for a trade. Given the amount and the number of points.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -37,8 +35,6 @@ class ForexSymbol(Symbol):
|
|||||||
volume = amount / (self.point * points * self.trade_contract_size)
|
volume = amount / (self.point * points * self.trade_contract_size)
|
||||||
return self.round_off_volume(volume=volume, round_down=round_down)
|
return self.round_off_volume(volume=volume, round_down=round_down)
|
||||||
|
|
||||||
async def compute_volume_sl(
|
async def compute_volume_sl(self, *, amount: float, price: float, sl: float, round_down: bool = False) -> float:
|
||||||
self, *, amount: float, price: float, sl: float, round_down: bool = False
|
|
||||||
) -> float:
|
|
||||||
volume = amount / (abs(price - sl) * self.trade_contract_size)
|
volume = amount / (abs(price - sl) * self.trade_contract_size)
|
||||||
return self.round_off_volume(volume=volume, round_down=round_down)
|
return self.round_off_volume(volume=volume, round_down=round_down)
|
||||||
|
|||||||
@@ -7,9 +7,7 @@ logger = getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
class ScalpTrader(Trader):
|
class ScalpTrader(Trader):
|
||||||
async def place_trade(
|
async def place_trade(self, *, order_type: OrderType, volume: float = None, parameters: dict = None):
|
||||||
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 a given stop_loss
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -28,6 +26,4 @@ class ScalpTrader(Trader):
|
|||||||
if res is not None:
|
if res is not None:
|
||||||
await self.record_trade(result=res, parameters=self.parameters)
|
await self.record_trade(result=res, parameters=self.parameters)
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
logger.error(
|
logger.error(f"{err} in {self.__class__.__name__}.place_trade for {self.symbol.name}")
|
||||||
f"{err} in {self.__class__.__name__}.place_trade for {self.symbol.name}"
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -7,9 +7,7 @@ logger = getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
class SimpleTrader(Trader):
|
class SimpleTrader(Trader):
|
||||||
async def place_trade(
|
async def place_trade(self, *, order_type: OrderType, sl: float, parameters: dict = None):
|
||||||
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
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -25,6 +23,4 @@ class SimpleTrader(Trader):
|
|||||||
self.order.comment = self.parameters.get("name", self.__class__.__name__)
|
self.order.comment = self.parameters.get("name", self.__class__.__name__)
|
||||||
await self.send_order()
|
await self.send_order()
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
logger.error(
|
logger.error(f"{err} in {self.__class__.__name__}.place_trade for {self.symbol.name}")
|
||||||
f"{err} in {self.__class__.__name__}.place_trade for {self.symbol.name}"
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -294,21 +294,9 @@ types = (
|
|||||||
class BaseMeta(type):
|
class BaseMeta(type):
|
||||||
def __new__(mcs, cls_name, bases, cls_dict):
|
def __new__(mcs, cls_name, bases, cls_dict):
|
||||||
defaults: dict = getattr(MetaTrader5, "__dict__", {})
|
defaults: dict = getattr(MetaTrader5, "__dict__", {})
|
||||||
callables = {
|
callables = {f"_{key}": value for key in core_mt5_functions if (value := defaults.get(key, None)) is not None}
|
||||||
f"_{key}": value
|
consts = {key: value for key in constants if (value := defaults.get(key, None)) is not None}
|
||||||
for key in core_mt5_functions
|
types_ = {key: value for key in types if (value := defaults.get(key, None)) is not None}
|
||||||
if (value := defaults.get(key, None)) is not None
|
|
||||||
}
|
|
||||||
consts = {
|
|
||||||
key: value
|
|
||||||
for key in constants
|
|
||||||
if (value := defaults.get(key, None)) is not None
|
|
||||||
}
|
|
||||||
types_ = {
|
|
||||||
key: value
|
|
||||||
for key in types
|
|
||||||
if (value := defaults.get(key, None)) is not None
|
|
||||||
}
|
|
||||||
cls_dict |= callables
|
cls_dict |= callables
|
||||||
cls_dict |= consts
|
cls_dict |= consts
|
||||||
cls_dict |= types_
|
cls_dict |= types_
|
||||||
|
|||||||
@@ -52,24 +52,14 @@ class BackTestController:
|
|||||||
self.backtest_engine.next()
|
self.backtest_engine.next()
|
||||||
while True:
|
while True:
|
||||||
pending = self.wait()
|
pending = self.wait()
|
||||||
if (
|
if pending == 0: # all main tasks have been completed in the current cycle
|
||||||
pending == 0
|
|
||||||
): # all main tasks have been completed in the current cycle
|
|
||||||
await self.backtest_engine.tracker()
|
await self.backtest_engine.tracker()
|
||||||
self.backtest_engine.next()
|
self.backtest_engine.next()
|
||||||
if self.backtest_engine.cursor.time % 3600 == 0:
|
if self.backtest_engine.cursor.time % 3600 == 0:
|
||||||
logger.info(
|
logger.info(datetime.strftime(datetime.fromtimestamp(self.backtest_engine.cursor.time), "%Y-%m-%d %H:%M:%S"))
|
||||||
datetime.strftime(
|
|
||||||
datetime.fromtimestamp(
|
|
||||||
self.backtest_engine.cursor.time
|
|
||||||
),
|
|
||||||
"%Y-%m-%d %H:%M:%S",
|
|
||||||
)
|
|
||||||
)
|
|
||||||
if self.backtest_engine.stop_testing:
|
if self.backtest_engine.stop_testing:
|
||||||
logger.info(
|
logger.info(
|
||||||
"Stop trading called in control at %s",
|
"Stop trading called in control at %s", datetime.fromtimestamp(self.backtest_engine.cursor.time).strftime("%Y-%m-%d %H:%M:%S")
|
||||||
datetime.fromtimestamp(self.backtest_engine.cursor.time).strftime("%Y-%m-%d %H:%M:%S"),
|
|
||||||
)
|
)
|
||||||
break
|
break
|
||||||
await self.backtest_engine.wrap_up()
|
await self.backtest_engine.wrap_up()
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -58,15 +58,7 @@ class BackTestData:
|
|||||||
class GetData:
|
class GetData:
|
||||||
data: BackTestData
|
data: BackTestData
|
||||||
|
|
||||||
def __init__(
|
def __init__(self, *, start: datetime, end: datetime, symbols: Sequence[str], timeframes: Sequence[TimeFrame], name: str = ""):
|
||||||
self,
|
|
||||||
*,
|
|
||||||
start: datetime,
|
|
||||||
end: datetime,
|
|
||||||
symbols: Sequence[str],
|
|
||||||
timeframes: Sequence[TimeFrame],
|
|
||||||
name: str = "",
|
|
||||||
):
|
|
||||||
""""""
|
""""""
|
||||||
self.config = Config()
|
self.config = Config()
|
||||||
self.start = start.astimezone(tz=UTC)
|
self.start = start.astimezone(tz=UTC)
|
||||||
@@ -102,14 +94,8 @@ class GetData:
|
|||||||
logger.error(f"Error: {err}")
|
logger.error(f"Error: {err}")
|
||||||
|
|
||||||
def save_data(self, *, name: str | Path = ""):
|
def save_data(self, *, name: str | Path = ""):
|
||||||
name = name or (
|
name = name or (self.name + ".pkl" if not self.name.endswith(".pkl") else self.name)
|
||||||
self.name + ".pkl" if not self.name.endswith(".pkl") else self.name
|
name = Path(self.config.backtest_dir) / name if not isinstance(name, Path) else name
|
||||||
)
|
|
||||||
name = (
|
|
||||||
Path(self.config.backtest_dir) / name
|
|
||||||
if not isinstance(name, Path)
|
|
||||||
else name
|
|
||||||
)
|
|
||||||
with open(name, "wb") as fo:
|
with open(name, "wb") as fo:
|
||||||
pickle.dump(self.data, fo, protocol=pickle.HIGHEST_PROTOCOL)
|
pickle.dump(self.data, fo, protocol=pickle.HIGHEST_PROTOCOL)
|
||||||
|
|
||||||
@@ -118,26 +104,15 @@ class GetData:
|
|||||||
if workers:
|
if workers:
|
||||||
self.task_queue.workers = workers
|
self.task_queue.workers = workers
|
||||||
|
|
||||||
q_items = [
|
q_items = [QueueItem(self.get_symbols_rates), QueueItem(self.get_symbols_ticks), QueueItem(self.get_symbols_info)]
|
||||||
QueueItem(self.get_symbols_rates),
|
|
||||||
QueueItem(self.get_symbols_ticks),
|
|
||||||
QueueItem(self.get_symbols_info),
|
|
||||||
]
|
|
||||||
|
|
||||||
[
|
[self.task_queue.add(item=item, priority=0, must_complete=True) for item in q_items]
|
||||||
self.task_queue.add(item=item, priority=0, must_complete=True)
|
|
||||||
for item in q_items
|
|
||||||
]
|
|
||||||
|
|
||||||
if not self.data.account:
|
if not self.data.account:
|
||||||
self.task_queue.add(
|
self.task_queue.add(item=QueueItem(self.get_account_info), must_complete=True)
|
||||||
item=QueueItem(self.get_account_info), must_complete=True
|
|
||||||
)
|
|
||||||
|
|
||||||
if not self.data.terminal:
|
if not self.data.terminal:
|
||||||
self.task_queue.add(
|
self.task_queue.add(item=QueueItem(self.get_terminal_info), must_complete=True)
|
||||||
item=QueueItem(self.get_terminal_info), must_complete=True
|
|
||||||
)
|
|
||||||
|
|
||||||
if not self.data.version:
|
if not self.data.version:
|
||||||
self.task_queue.add(item=QueueItem(self.get_version), must_complete=True)
|
self.task_queue.add(item=QueueItem(self.get_version), must_complete=True)
|
||||||
@@ -146,9 +121,7 @@ class GetData:
|
|||||||
|
|
||||||
if self.data.fully_loaded is False:
|
if self.data.fully_loaded is False:
|
||||||
logger.warning("Data not fully loaded")
|
logger.warning("Data not fully loaded")
|
||||||
self.data = BackTestData(
|
self.data = BackTestData(name=self.name, span=self.span, range=self.range, fully_loaded=False)
|
||||||
name=self.name, span=self.span, range=self.range, fully_loaded=False
|
|
||||||
)
|
|
||||||
|
|
||||||
async def get_terminal_info(self):
|
async def get_terminal_info(self):
|
||||||
""""""
|
""""""
|
||||||
@@ -179,29 +152,16 @@ class GetData:
|
|||||||
|
|
||||||
async def get_symbols_info(self):
|
async def get_symbols_info(self):
|
||||||
""""""
|
""""""
|
||||||
[
|
[self.task_queue.add(item=QueueItem(self.get_symbol_info, symbol=symbol)) for symbol in self.symbols if self.data.symbols.get(symbol) is None]
|
||||||
self.task_queue.add(item=QueueItem(self.get_symbol_info, symbol=symbol))
|
|
||||||
for symbol in self.symbols
|
|
||||||
if self.data.symbols.get(symbol) is None
|
|
||||||
]
|
|
||||||
|
|
||||||
async def get_symbols_ticks(self):
|
async def get_symbols_ticks(self):
|
||||||
""""""
|
""""""
|
||||||
[
|
[self.task_queue.add(item=QueueItem(self.get_symbol_ticks, symbol=symbol)) for symbol in self.symbols if self.data.ticks.get(symbol) is None]
|
||||||
self.task_queue.add(item=QueueItem(self.get_symbol_ticks, symbol=symbol))
|
|
||||||
for symbol in self.symbols
|
|
||||||
if self.data.ticks.get(symbol) is None
|
|
||||||
]
|
|
||||||
|
|
||||||
async def get_symbols_rates(self):
|
async def get_symbols_rates(self):
|
||||||
""""""
|
""""""
|
||||||
[
|
[
|
||||||
self.task_queue.add(
|
self.task_queue.add(item=QueueItem(self.get_symbol_rates, symbol=symbol, timeframe=timeframe), priority=4)
|
||||||
item=QueueItem(
|
|
||||||
self.get_symbol_rates, symbol=symbol, timeframe=timeframe
|
|
||||||
),
|
|
||||||
priority=4,
|
|
||||||
)
|
|
||||||
for symbol in self.symbols
|
for symbol in self.symbols
|
||||||
for timeframe in self.timeframes
|
for timeframe in self.timeframes
|
||||||
if self.data.rates.get(symbol, {}).get(timeframe) is None
|
if self.data.rates.get(symbol, {}).get(timeframe) is None
|
||||||
@@ -219,9 +179,7 @@ class GetData:
|
|||||||
@backoff_decorator
|
@backoff_decorator
|
||||||
async def get_symbol_ticks(self, *, symbol: str):
|
async def get_symbol_ticks(self, *, symbol: str):
|
||||||
""""""
|
""""""
|
||||||
res = await self.mt5.copy_ticks_range(
|
res = await self.mt5.copy_ticks_range(symbol, self.start, self.end, MetaTrader5.COPY_TICKS_ALL)
|
||||||
symbol, self.start, self.end, MetaTrader5.COPY_TICKS_ALL
|
|
||||||
)
|
|
||||||
if res is None:
|
if res is None:
|
||||||
self.data.fully_loaded = False
|
self.data.fully_loaded = False
|
||||||
self.task_queue.stop_queue()
|
self.task_queue.stop_queue()
|
||||||
|
|||||||
@@ -66,13 +66,9 @@ class PositionsManager(TradeManager):
|
|||||||
_open_positions: set[int]
|
_open_positions: set[int]
|
||||||
margins: dict[int, float]
|
margins: dict[int, float]
|
||||||
|
|
||||||
def __init__(
|
def __init__(self, *, data: dict = None, open_positions: set = None, margins: dict = None):
|
||||||
self, *, data: dict = None, open_positions: set = None, margins: dict = None
|
|
||||||
):
|
|
||||||
super().__init__(data=data)
|
super().__init__(data=data)
|
||||||
self._open_positions = open_positions or {
|
self._open_positions = open_positions or {trade.ticket for trade in self._data.values()}
|
||||||
trade.ticket for trade in self._data.values()
|
|
||||||
}
|
|
||||||
self.margins: dict[int, float] = margins or dict()
|
self.margins: dict[int, float] = margins or dict()
|
||||||
|
|
||||||
def __len__(self):
|
def __len__(self):
|
||||||
@@ -113,22 +109,12 @@ class PositionsManager(TradeManager):
|
|||||||
def set_margin(self, *, ticket: int, margin: float):
|
def set_margin(self, *, ticket: int, margin: float):
|
||||||
self.margins[ticket] = margin
|
self.margins[ticket] = margin
|
||||||
|
|
||||||
def positions_get(
|
def positions_get(self, *, ticket: int = None, symbol: str = None, group: None = None) -> tuple[TradePosition, ...]:
|
||||||
self, *, ticket: int = None, symbol: str = None, group: None = None
|
|
||||||
) -> tuple[TradePosition, ...]:
|
|
||||||
if ticket:
|
if ticket:
|
||||||
return tuple(
|
return tuple(position for position in self.open_positions if position.ticket == ticket)
|
||||||
position
|
|
||||||
for position in self.open_positions
|
|
||||||
if position.ticket == ticket
|
|
||||||
)
|
|
||||||
|
|
||||||
if symbol:
|
if symbol:
|
||||||
return tuple(
|
return tuple(position for position in self.open_positions if position.symbol == symbol)
|
||||||
position
|
|
||||||
for position in self.open_positions
|
|
||||||
if position.symbol == symbol
|
|
||||||
)
|
|
||||||
|
|
||||||
if group:
|
if group:
|
||||||
return self.open_positions
|
return self.open_positions
|
||||||
@@ -143,33 +129,19 @@ class PositionsManager(TradeManager):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def open_positions(self) -> tuple[TradePosition, ...]:
|
def open_positions(self) -> tuple[TradePosition, ...]:
|
||||||
return tuple(
|
return tuple(position for position in self.values() if position.ticket in self._open_positions)
|
||||||
position
|
|
||||||
for position in self.values()
|
|
||||||
if position.ticket in self._open_positions
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class OrdersManager(TradeManager):
|
class OrdersManager(TradeManager):
|
||||||
_data = dict[int, TradeOrder]
|
_data = dict[int, TradeOrder]
|
||||||
|
|
||||||
def get_orders_range(
|
def get_orders_range(self, *, date_from: float, date_to: float) -> tuple[TradeData, ...]:
|
||||||
self, *, date_from: float, date_to: float
|
|
||||||
) -> tuple[TradeData, ...]:
|
|
||||||
start = date_from.timestamp() if isinstance(date_from, datetime) else date_from
|
start = date_from.timestamp() if isinstance(date_from, datetime) else date_from
|
||||||
end = date_to.timestamp() if isinstance(date_to, datetime) else date_to
|
end = date_to.timestamp() if isinstance(date_to, datetime) else date_to
|
||||||
return tuple(
|
return tuple(order for order in self.values() if start <= order.time_setup <= end)
|
||||||
order for order in self.values() if start <= order.time_setup <= end
|
|
||||||
)
|
|
||||||
|
|
||||||
def history_orders_get(
|
def history_orders_get(
|
||||||
self,
|
self, *, date_from: float | datetime = None, date_to: float | datetime = None, group: str = "", ticket: int = None, position: int = None
|
||||||
*,
|
|
||||||
date_from: float | datetime = None,
|
|
||||||
date_to: float | datetime = None,
|
|
||||||
group: str = "",
|
|
||||||
ticket: int = None,
|
|
||||||
position: int = None,
|
|
||||||
) -> tuple[TradeOrder, ...]:
|
) -> tuple[TradeOrder, ...]:
|
||||||
if date_from and date_to:
|
if date_from and date_to:
|
||||||
orders = self.get_orders_range(date_from=date_from, date_to=date_to)
|
orders = self.get_orders_range(date_from=date_from, date_to=date_to)
|
||||||
@@ -181,36 +153,24 @@ class OrdersManager(TradeManager):
|
|||||||
return tuple(order for order in self.values() if order.ticket == ticket)
|
return tuple(order for order in self.values() if order.ticket == ticket)
|
||||||
|
|
||||||
if position:
|
if position:
|
||||||
return tuple(
|
return tuple(order for order in self.values() if order.position_id == position)
|
||||||
order for order in self.values() if order.position_id == position
|
|
||||||
)
|
|
||||||
|
|
||||||
return ()
|
return ()
|
||||||
|
|
||||||
def history_orders_total(
|
def history_orders_total(self, *, date_from: datetime | float, date_to: datetime | float) -> int:
|
||||||
self, *, date_from: datetime | float, date_to: datetime | float
|
|
||||||
) -> int:
|
|
||||||
return len(self.get_orders_range(date_from=date_from, date_to=date_to))
|
return len(self.get_orders_range(date_from=date_from, date_to=date_to))
|
||||||
|
|
||||||
|
|
||||||
class DealsManager(TradeManager):
|
class DealsManager(TradeManager):
|
||||||
_data = dict[int, TradeDeal]
|
_data = dict[int, TradeDeal]
|
||||||
|
|
||||||
def get_deals_range(
|
def get_deals_range(self, *, date_from: float, date_to: float) -> tuple[TradeData, ...]:
|
||||||
self, *, date_from: float, date_to: float
|
|
||||||
) -> tuple[TradeData, ...]:
|
|
||||||
start = date_from.timestamp() if isinstance(date_from, datetime) else date_from
|
start = date_from.timestamp() if isinstance(date_from, datetime) else date_from
|
||||||
end = date_to.timestamp() if isinstance(date_to, datetime) else date_to
|
end = date_to.timestamp() if isinstance(date_to, datetime) else date_to
|
||||||
return tuple(deal for deal in self.values() if start <= deal.time <= end)
|
return tuple(deal for deal in self.values() if start <= deal.time <= end)
|
||||||
|
|
||||||
def history_deals_get(
|
def history_deals_get(
|
||||||
self,
|
self, *, date_from: float | datetime = None, date_to: float | datetime = None, group: str = "", ticket: int = None, position: int = None
|
||||||
*,
|
|
||||||
date_from: float | datetime = None,
|
|
||||||
date_to: float | datetime = None,
|
|
||||||
group: str = "",
|
|
||||||
ticket: int = None,
|
|
||||||
position: int = None,
|
|
||||||
) -> tuple[TradeDeal, ...]:
|
) -> tuple[TradeDeal, ...]:
|
||||||
if date_from and date_to:
|
if date_from and date_to:
|
||||||
deals = self.get_deals_range(date_from=date_from, date_to=date_to)
|
deals = self.get_deals_range(date_from=date_from, date_to=date_to)
|
||||||
@@ -226,7 +186,5 @@ class DealsManager(TradeManager):
|
|||||||
|
|
||||||
return ()
|
return ()
|
||||||
|
|
||||||
def history_deals_total(
|
def history_deals_total(self, *, date_from: datetime | float, date_to: datetime | float) -> int:
|
||||||
self, *, date_from: datetime | float, date_to: datetime | float
|
|
||||||
) -> int:
|
|
||||||
return len(self.get_deals_range(date_from=date_from, date_to=date_to))
|
return len(self.get_deals_range(date_from=date_from, date_to=date_to))
|
||||||
|
|||||||
+9
-43
@@ -24,32 +24,14 @@ class Base:
|
|||||||
Args:
|
Args:
|
||||||
**kwargs: Set instance attributes with keyword arguments. Only if they are annotated on the class body.
|
**kwargs: Set instance attributes with keyword arguments. Only if they are annotated on the class body.
|
||||||
"""
|
"""
|
||||||
self.exclude = {
|
self.exclude = {"mt5", "config", "exclude", "include", "annotations", "class_vars", "dict", "_instance"}
|
||||||
"mt5",
|
|
||||||
"config",
|
|
||||||
"exclude",
|
|
||||||
"include",
|
|
||||||
"annotations",
|
|
||||||
"class_vars",
|
|
||||||
"dict",
|
|
||||||
"_instance",
|
|
||||||
}
|
|
||||||
self.include = set()
|
self.include = set()
|
||||||
self.set_attributes(**kwargs)
|
self.set_attributes(**kwargs)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
kv = [
|
kv = [(k, v) for k, v in self.__dict__.items() if not k.startswith("_") and (type(v) in (int, float, str) or isinstance(v, enum.Enum))]
|
||||||
(k, v)
|
|
||||||
for k, v in self.__dict__.items()
|
|
||||||
if not k.startswith("_")
|
|
||||||
and (type(v) in (int, float, str) or isinstance(v, enum.Enum))
|
|
||||||
]
|
|
||||||
args = ", ".join("%s=%s" % (i, j) for i, j in kv[:3])
|
args = ", ".join("%s=%s" % (i, j) for i, j in kv[:3])
|
||||||
args = (
|
args = args if len(kv) <= 3 else args + " ... " + ", ".join("%s=%s" % (i, j) for i, j in kv[-1:])
|
||||||
args
|
|
||||||
if len(kv) <= 3
|
|
||||||
else args + " ... " + ", ".join("%s=%s" % (i, j) for i, j in kv[-1:])
|
|
||||||
)
|
|
||||||
return "%(class)s(%(args)s)" % {"class": self.__class__.__name__, "args": args}
|
return "%(class)s(%(args)s)" % {"class": self.__class__.__name__, "args": args}
|
||||||
|
|
||||||
def set_attributes(self, **kwargs):
|
def set_attributes(self, **kwargs):
|
||||||
@@ -68,21 +50,15 @@ class Base:
|
|||||||
try:
|
try:
|
||||||
setattr(self, i, self.annotations[i](j))
|
setattr(self, i, self.annotations[i](j))
|
||||||
except KeyError:
|
except KeyError:
|
||||||
logger.debug(
|
logger.debug(f"Attribute {i} does not belong to class {self.__class__.__name__}")
|
||||||
f"Attribute {i} does not belong to class {self.__class__.__name__}"
|
|
||||||
)
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
logger.debug(
|
logger.debug(f"Cannot covert object of type {type(j)} to type {self.annotations[i]}")
|
||||||
f"Cannot covert object of type {type(j)} to type {self.annotations[i]}"
|
|
||||||
)
|
|
||||||
setattr(self, i, j)
|
setattr(self, i, j)
|
||||||
|
|
||||||
except Exception as exe:
|
except Exception as exe:
|
||||||
logger.debug(
|
logger.debug(f"Did not set attribute {i} on class {self.__class__.__name__} due to {exe}")
|
||||||
f"Did not set attribute {i} on class {self.__class__.__name__} due to {exe}"
|
|
||||||
)
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -113,11 +89,7 @@ class Base:
|
|||||||
"""
|
"""
|
||||||
exclude, include = exclude or set(), include or set()
|
exclude, include = exclude or set(), include or set()
|
||||||
filter_ = include or set(self.dict.keys()).difference(exclude)
|
filter_ = include or set(self.dict.keys()).difference(exclude)
|
||||||
return {
|
return {key: value for key, value in self.dict.items() if key in filter_ and value is not None}
|
||||||
key: value
|
|
||||||
for key, value in self.dict.items()
|
|
||||||
if key in filter_ and value is not None
|
|
||||||
}
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@cache
|
@cache
|
||||||
@@ -131,9 +103,7 @@ class Base:
|
|||||||
cls_dict = {}
|
cls_dict = {}
|
||||||
for cls in clss:
|
for cls in clss:
|
||||||
cls_dict |= cls.__dict__
|
cls_dict |= cls.__dict__
|
||||||
return {
|
return {key: value for key, value in cls_dict.items() if key in self.annotations}
|
||||||
key: value for key, value in cls_dict.items() if key in self.annotations
|
|
||||||
}
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def dict(self) -> dict:
|
def dict(self) -> dict:
|
||||||
@@ -144,11 +114,7 @@ class Base:
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
_filter = self.exclude.difference(self.include)
|
_filter = self.exclude.difference(self.include)
|
||||||
return {
|
return {key: value for key, value in (self.class_vars | self.__dict__).items() if key not in _filter and value is not None}
|
||||||
key: value
|
|
||||||
for key, value in (self.class_vars | self.__dict__).items()
|
|
||||||
if key not in _filter and value is not None
|
|
||||||
}
|
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
logger.warning(err)
|
logger.warning(err)
|
||||||
|
|
||||||
|
|||||||
@@ -114,9 +114,7 @@ class Config:
|
|||||||
**kwargs: Object attributes and values as keyword arguments
|
**kwargs: Object attributes and values as keyword arguments
|
||||||
"""
|
"""
|
||||||
if kwargs.pop("root", None) is not None:
|
if kwargs.pop("root", None) is not None:
|
||||||
logger.warning(
|
logger.warning("Tried setting root from set_attributes. Use load_config to change project root")
|
||||||
"Tried setting root from set_attributes. Use load_config to change project root"
|
|
||||||
)
|
|
||||||
[setattr(self, key, value) for key, value in kwargs.items()]
|
[setattr(self, key, value) for key, value in kwargs.items()]
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -145,14 +143,7 @@ class Config:
|
|||||||
logger.debug(f"Error finding config file: {err}")
|
logger.debug(f"Error finding config file: {err}")
|
||||||
return
|
return
|
||||||
|
|
||||||
def load_config(
|
def load_config(self, *, file: str | Path = None, filename: str = None, root: str | Path = None, **kwargs) -> Self:
|
||||||
self,
|
|
||||||
*,
|
|
||||||
file: str | Path = None,
|
|
||||||
filename: str = None,
|
|
||||||
root: str | Path = None,
|
|
||||||
**kwargs,
|
|
||||||
) -> Self:
|
|
||||||
"""Load configuration settings from a file.
|
"""Load configuration settings from a file.
|
||||||
|
|
||||||
Keyword Args:
|
Keyword Args:
|
||||||
@@ -190,17 +181,9 @@ class Config:
|
|||||||
self.set_attributes(**data)
|
self.set_attributes(**data)
|
||||||
|
|
||||||
if self.path:
|
if self.path:
|
||||||
self.path = (
|
self.path = self.root / self.path if not Path(self.path).resolve().exists() else self.path
|
||||||
self.root / self.path
|
|
||||||
if not Path(self.path).resolve().exists()
|
|
||||||
else self.path
|
|
||||||
)
|
|
||||||
|
|
||||||
if self.record_trades and (
|
if self.record_trades and (hasattr(self, "records_dir") is False or self.records_dir is None or root is not None):
|
||||||
hasattr(self, "records_dir") is False
|
|
||||||
or self.records_dir is None
|
|
||||||
or root is not None
|
|
||||||
):
|
|
||||||
self.records_dir = self.root / self.records_dir_name
|
self.records_dir = self.root / self.records_dir_name
|
||||||
self.records_dir.mkdir(parents=True, exist_ok=True)
|
self.records_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
|||||||
@@ -3,17 +3,7 @@ from logging import getLogger
|
|||||||
from typing import Literal, TypeVar
|
from typing import Literal, TypeVar
|
||||||
|
|
||||||
from numpy import ndarray
|
from numpy import ndarray
|
||||||
from MetaTrader5 import (
|
from MetaTrader5 import Tick, SymbolInfo, AccountInfo, TerminalInfo, TradeOrder, TradePosition, TradeDeal, OrderCheckResult, OrderSendResult
|
||||||
Tick,
|
|
||||||
SymbolInfo,
|
|
||||||
AccountInfo,
|
|
||||||
TerminalInfo,
|
|
||||||
TradeOrder,
|
|
||||||
TradePosition,
|
|
||||||
TradeDeal,
|
|
||||||
OrderCheckResult,
|
|
||||||
OrderSendResult,
|
|
||||||
)
|
|
||||||
|
|
||||||
from .meta_trader import MetaTrader
|
from .meta_trader import MetaTrader
|
||||||
from .constants import TimeFrame, CopyTicks, OrderType
|
from .constants import TimeFrame, CopyTicks, OrderType
|
||||||
@@ -49,73 +39,29 @@ class MetaBackTester(MetaTrader):
|
|||||||
return await super().last_error()
|
return await super().last_error()
|
||||||
|
|
||||||
async def initialize(
|
async def initialize(
|
||||||
self,
|
self, *, path: str = "", login: int = 0, password: str = "", server: str = "", timeout: int | None = None, portable=False
|
||||||
*,
|
|
||||||
path: str = "",
|
|
||||||
login: int = 0,
|
|
||||||
password: str = "",
|
|
||||||
server: str = "",
|
|
||||||
timeout: int | None = None,
|
|
||||||
portable=False,
|
|
||||||
) -> bool:
|
) -> bool:
|
||||||
if self.config.use_terminal_for_backtesting:
|
if self.config.use_terminal_for_backtesting:
|
||||||
return await super().initialize(
|
return await super().initialize(path=path, login=login, password=password, server=server, timeout=timeout)
|
||||||
path=path,
|
|
||||||
login=login,
|
|
||||||
password=password,
|
|
||||||
server=server,
|
|
||||||
timeout=timeout,
|
|
||||||
)
|
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def initialize_sync(
|
def initialize_sync(
|
||||||
self,
|
self, *, path: str = "", login: int = 0, password: str = "", server: str = "", timeout: int | None = None, portable=False
|
||||||
*,
|
|
||||||
path: str = "",
|
|
||||||
login: int = 0,
|
|
||||||
password: str = "",
|
|
||||||
server: str = "",
|
|
||||||
timeout: int | None = None,
|
|
||||||
portable=False,
|
|
||||||
) -> bool:
|
) -> bool:
|
||||||
if self.config.use_terminal_for_backtesting:
|
if self.config.use_terminal_for_backtesting:
|
||||||
return super().initialize_sync(
|
return super().initialize_sync(path=path, login=login, password=password, server=server, timeout=timeout)
|
||||||
path=path,
|
|
||||||
login=login,
|
|
||||||
password=password,
|
|
||||||
server=server,
|
|
||||||
timeout=timeout,
|
|
||||||
)
|
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def login_sync(
|
def login_sync(self, *, login: int = 0, password: str = "", server: str = "", timeout: int = 60000) -> bool:
|
||||||
self,
|
|
||||||
*,
|
|
||||||
login: int = 0,
|
|
||||||
password: str = "",
|
|
||||||
server: str = "",
|
|
||||||
timeout: int = 60000,
|
|
||||||
) -> bool:
|
|
||||||
if self.config.use_terminal_for_backtesting:
|
if self.config.use_terminal_for_backtesting:
|
||||||
return super().login_sync(
|
return super().login_sync(login=login, password=password, server=server, timeout=timeout)
|
||||||
login=login, password=password, server=server, timeout=timeout
|
|
||||||
)
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
async def login(
|
async def login(self, *, login: int = 0, password: str = "", server: str = "", timeout: int = 60000) -> bool:
|
||||||
self,
|
|
||||||
*,
|
|
||||||
login: int = 0,
|
|
||||||
password: str = "",
|
|
||||||
server: str = "",
|
|
||||||
timeout: int = 60000,
|
|
||||||
) -> bool:
|
|
||||||
if self.config.use_terminal_for_backtesting:
|
if self.config.use_terminal_for_backtesting:
|
||||||
return await super().login(
|
return await super().login(login=login, password=password, server=server, timeout=timeout)
|
||||||
login=login, password=password, server=server, timeout=timeout
|
|
||||||
)
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
async def shutdown(self) -> None:
|
async def shutdown(self) -> None:
|
||||||
@@ -153,56 +99,28 @@ class MetaBackTester(MetaTrader):
|
|||||||
return tick
|
return tick
|
||||||
|
|
||||||
@error_handler(msg="test data not available", exe=AttributeError)
|
@error_handler(msg="test data not available", exe=AttributeError)
|
||||||
async def copy_rates_from(
|
async def copy_rates_from(self, symbol: str, timeframe: TimeFrame, date_from: datetime | float, count: int) -> ndarray | None:
|
||||||
self, symbol: str, timeframe: TimeFrame, date_from: datetime | float, count: int
|
rates = await self.backtest_engine.get_rates_from(symbol=symbol, timeframe=timeframe, date_from=date_from, count=count)
|
||||||
) -> ndarray | None:
|
|
||||||
rates = await self.backtest_engine.get_rates_from(
|
|
||||||
symbol=symbol, timeframe=timeframe, date_from=date_from, count=count
|
|
||||||
)
|
|
||||||
return rates
|
return rates
|
||||||
|
|
||||||
@error_handler(msg="test data not available", exe=AttributeError)
|
@error_handler(msg="test data not available", exe=AttributeError)
|
||||||
async def copy_rates_from_pos(
|
async def copy_rates_from_pos(self, symbol: str, timeframe: TimeFrame, start_pos: int, count: int) -> ndarray | None:
|
||||||
self, symbol: str, timeframe: TimeFrame, start_pos: int, count: int
|
rates = await self.backtest_engine.get_rates_from_pos(symbol=symbol, timeframe=timeframe, start_pos=start_pos, count=count)
|
||||||
) -> ndarray | None:
|
|
||||||
rates = await self.backtest_engine.get_rates_from_pos(
|
|
||||||
symbol=symbol, timeframe=timeframe, start_pos=start_pos, count=count
|
|
||||||
)
|
|
||||||
return rates
|
return rates
|
||||||
|
|
||||||
@error_handler(msg="test data not available", exe=AttributeError)
|
@error_handler(msg="test data not available", exe=AttributeError)
|
||||||
async def copy_rates_range(
|
async def copy_rates_range(self, symbol: str, timeframe: TimeFrame, date_from: datetime | float, date_to: datetime | float) -> ndarray | None:
|
||||||
self,
|
rates = await self.backtest_engine.get_rates_range(symbol=symbol, timeframe=timeframe, date_from=date_from, date_to=date_to)
|
||||||
symbol: str,
|
|
||||||
timeframe: TimeFrame,
|
|
||||||
date_from: datetime | float,
|
|
||||||
date_to: datetime | float,
|
|
||||||
) -> ndarray | None:
|
|
||||||
rates = await self.backtest_engine.get_rates_range(
|
|
||||||
symbol=symbol, timeframe=timeframe, date_from=date_from, date_to=date_to
|
|
||||||
)
|
|
||||||
return rates
|
return rates
|
||||||
|
|
||||||
@error_handler(msg="test data not available", exe=AttributeError)
|
@error_handler(msg="test data not available", exe=AttributeError)
|
||||||
async def copy_ticks_from(
|
async def copy_ticks_from(self, symbol: str, date_from: datetime | float, count: int, flags: CopyTicks) -> ndarray | None:
|
||||||
self, symbol: str, date_from: datetime | float, count: int, flags: CopyTicks
|
ticks = await self.backtest_engine.get_ticks_from(symbol=symbol, date_from=date_from, count=count, flags=flags)
|
||||||
) -> ndarray | None:
|
|
||||||
ticks = await self.backtest_engine.get_ticks_from(
|
|
||||||
symbol=symbol, date_from=date_from, count=count, flags=flags
|
|
||||||
)
|
|
||||||
return ticks
|
return ticks
|
||||||
|
|
||||||
@error_handler(msg="test data not available", exe=AttributeError)
|
@error_handler(msg="test data not available", exe=AttributeError)
|
||||||
async def copy_ticks_range(
|
async def copy_ticks_range(self, symbol: str, date_from: datetime | float, date_to: datetime | float, flags: CopyTicks) -> ndarray | None:
|
||||||
self,
|
ticks = await self.backtest_engine.get_ticks_range(symbol=symbol, date_from=date_from, date_to=date_to, flags=flags)
|
||||||
symbol: str,
|
|
||||||
date_from: datetime | float,
|
|
||||||
date_to: datetime | float,
|
|
||||||
flags: CopyTicks,
|
|
||||||
) -> ndarray | None:
|
|
||||||
ticks = await self.backtest_engine.get_ticks_range(
|
|
||||||
symbol=symbol, date_from=date_from, date_to=date_to, flags=flags
|
|
||||||
)
|
|
||||||
return ticks
|
return ticks
|
||||||
|
|
||||||
@error_handler(msg="test data not available", exe=AttributeError)
|
@error_handler(msg="test data not available", exe=AttributeError)
|
||||||
@@ -210,40 +128,19 @@ class MetaBackTester(MetaTrader):
|
|||||||
return self.backtest_engine.get_orders_total()
|
return self.backtest_engine.get_orders_total()
|
||||||
|
|
||||||
@error_handler(msg="test data not available", exe=AttributeError)
|
@error_handler(msg="test data not available", exe=AttributeError)
|
||||||
async def orders_get(
|
async def orders_get(self, group: str = "", ticket: int = 0, symbol: str = "") -> tuple[TradeOrder, ...] | None:
|
||||||
self, group: str = "", ticket: int = 0, symbol: str = ""
|
kwargs = {key: value for key, value in (("group", group), ("ticket", ticket), ("symbol", symbol)) if value}
|
||||||
) -> tuple[TradeOrder, ...] | None:
|
|
||||||
kwargs = {
|
|
||||||
key: value
|
|
||||||
for key, value in (("group", group), ("ticket", ticket), ("symbol", symbol))
|
|
||||||
if value
|
|
||||||
}
|
|
||||||
return self.backtest_engine.get_orders(**kwargs)
|
return self.backtest_engine.get_orders(**kwargs)
|
||||||
|
|
||||||
@error_handler(msg="test data not available", exe=AttributeError)
|
@error_handler(msg="test data not available", exe=AttributeError)
|
||||||
async def order_calc_margin(
|
async def order_calc_margin(self, action: OrderType, symbol: str, volume: float, price: float) -> float | None:
|
||||||
self, action: OrderType, symbol: str, volume: float, price: float
|
res = await self.backtest_engine.order_calc_margin(action=action, symbol=symbol, volume=volume, price=price)
|
||||||
) -> float | None:
|
|
||||||
res = await self.backtest_engine.order_calc_margin(
|
|
||||||
action=action, symbol=symbol, volume=volume, price=price
|
|
||||||
)
|
|
||||||
return res
|
return res
|
||||||
|
|
||||||
@error_handler(msg="test data not available", exe=AttributeError)
|
@error_handler(msg="test data not available", exe=AttributeError)
|
||||||
async def order_calc_profit(
|
async def order_calc_profit(self, action: Literal[0, 1], symbol: str, volume: float, price_open: float, price_close: float) -> float | None:
|
||||||
self,
|
|
||||||
action: Literal[0, 1],
|
|
||||||
symbol: str,
|
|
||||||
volume: float,
|
|
||||||
price_open: float,
|
|
||||||
price_close: float,
|
|
||||||
) -> float | None:
|
|
||||||
profit = await self.backtest_engine.order_calc_profit(
|
profit = await self.backtest_engine.order_calc_profit(
|
||||||
action=action,
|
action=action, symbol=symbol, volume=volume, price_open=price_open, price_close=price_close
|
||||||
symbol=symbol,
|
|
||||||
volume=volume,
|
|
||||||
price_open=price_open,
|
|
||||||
price_close=price_close,
|
|
||||||
)
|
)
|
||||||
return profit
|
return profit
|
||||||
|
|
||||||
@@ -261,66 +158,30 @@ class MetaBackTester(MetaTrader):
|
|||||||
return self.backtest_engine.get_positions_total()
|
return self.backtest_engine.get_positions_total()
|
||||||
|
|
||||||
@error_handler(msg="test data not available", exe=AttributeError)
|
@error_handler(msg="test data not available", exe=AttributeError)
|
||||||
async def positions_get(
|
async def positions_get(self, group: str = "", ticket: int = None, symbol: str = "") -> tuple[TradePosition, ...] | None:
|
||||||
self, group: str = "", ticket: int = None, symbol: str = ""
|
kwargs = {key: value for key, value in (("group", group), ("ticket", ticket), ("symbol", symbol)) if value}
|
||||||
) -> tuple[TradePosition, ...] | None:
|
|
||||||
kwargs = {
|
|
||||||
key: value
|
|
||||||
for key, value in (("group", group), ("ticket", ticket), ("symbol", symbol))
|
|
||||||
if value
|
|
||||||
}
|
|
||||||
return self.backtest_engine.get_positions(**kwargs)
|
return self.backtest_engine.get_positions(**kwargs)
|
||||||
|
|
||||||
@error_handler(msg="test data not available", exe=AttributeError)
|
@error_handler(msg="test data not available", exe=AttributeError)
|
||||||
async def history_orders_total(
|
async def history_orders_total(self, date_from: datetime | float, date_to: datetime | float) -> int | None:
|
||||||
self, date_from: datetime | float, date_to: datetime | float
|
return self.backtest_engine.get_history_orders_total(date_from=date_from, date_to=date_to)
|
||||||
) -> int | None:
|
|
||||||
return self.backtest_engine.get_history_orders_total(
|
|
||||||
date_from=date_from, date_to=date_to
|
|
||||||
)
|
|
||||||
|
|
||||||
@error_handler(msg="test data not available", exe=AttributeError)
|
@error_handler(msg="test data not available", exe=AttributeError)
|
||||||
async def history_orders_get(
|
async def history_orders_get(
|
||||||
self,
|
self, date_from: datetime | float = None, date_to: datetime | float = None, group: str = "", ticket: int = None, position: int = None
|
||||||
date_from: datetime | float = None,
|
|
||||||
date_to: datetime | float = None,
|
|
||||||
group: str = "",
|
|
||||||
ticket: int = None,
|
|
||||||
position: int = None,
|
|
||||||
) -> tuple[TradeOrder, ...] | None:
|
) -> tuple[TradeOrder, ...] | None:
|
||||||
args = (
|
args = (("date_from", date_from), ("date_to", date_to), ("group", group), ("ticket", ticket), ("position", position))
|
||||||
("date_from", date_from),
|
|
||||||
("date_to", date_to),
|
|
||||||
("group", group),
|
|
||||||
("ticket", ticket),
|
|
||||||
("position", position),
|
|
||||||
)
|
|
||||||
kwargs = {key: value for key, value in args if value}
|
kwargs = {key: value for key, value in args if value}
|
||||||
return self.backtest_engine.get_history_orders(**kwargs)
|
return self.backtest_engine.get_history_orders(**kwargs)
|
||||||
|
|
||||||
@error_handler(msg="test data not available", exe=AttributeError)
|
@error_handler(msg="test data not available", exe=AttributeError)
|
||||||
async def history_deals_total(
|
async def history_deals_total(self, date_from: datetime | float, date_to: datetime | float) -> int | None:
|
||||||
self, date_from: datetime | float, date_to: datetime | float
|
return self.backtest_engine.get_history_deals_total(date_from=date_from, date_to=date_to)
|
||||||
) -> int | None:
|
|
||||||
return self.backtest_engine.get_history_deals_total(
|
|
||||||
date_from=date_from, date_to=date_to
|
|
||||||
)
|
|
||||||
|
|
||||||
@error_handler(msg="test data not available", exe=AttributeError)
|
@error_handler(msg="test data not available", exe=AttributeError)
|
||||||
async def history_deals_get(
|
async def history_deals_get(
|
||||||
self,
|
self, date_from: datetime | float = None, date_to: datetime | float = None, group: str = "", ticket: int = None, position: int = None
|
||||||
date_from: datetime | float = None,
|
|
||||||
date_to: datetime | float = None,
|
|
||||||
group: str = "",
|
|
||||||
ticket: int = None,
|
|
||||||
position: int = None,
|
|
||||||
) -> tuple[TradeDeal, ...] | None:
|
) -> tuple[TradeDeal, ...] | None:
|
||||||
args = (
|
args = (("date_from", date_from), ("date_to", date_to), ("group", group), ("ticket", ticket), ("position", position))
|
||||||
("date_from", date_from),
|
|
||||||
("date_to", date_to),
|
|
||||||
("group", group),
|
|
||||||
("ticket", ticket),
|
|
||||||
("position", position),
|
|
||||||
)
|
|
||||||
kwargs = {key: value for key, value in args if value}
|
kwargs = {key: value for key, value in args if value}
|
||||||
return self.backtest_engine.get_history_deals(**kwargs)
|
return self.backtest_engine.get_history_deals(**kwargs)
|
||||||
|
|||||||
+49
-255
@@ -5,18 +5,8 @@ from typing import Literal
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from MetaTrader5 import (
|
from MetaTrader5 import (BookInfo, SymbolInfo, AccountInfo, Tick, TerminalInfo, TradeOrder, TradeDeal, TradePosition,
|
||||||
BookInfo,
|
OrderSendResult, OrderCheckResult)
|
||||||
SymbolInfo,
|
|
||||||
AccountInfo,
|
|
||||||
Tick,
|
|
||||||
TerminalInfo,
|
|
||||||
TradeOrder,
|
|
||||||
TradeDeal,
|
|
||||||
TradePosition,
|
|
||||||
OrderSendResult,
|
|
||||||
OrderCheckResult,
|
|
||||||
)
|
|
||||||
import MetaTrader5 as mt5
|
import MetaTrader5 as mt5
|
||||||
|
|
||||||
from .constants import OrderType, CopyTicks
|
from .constants import OrderType, CopyTicks
|
||||||
@@ -75,14 +65,7 @@ class MetaTrader(MetaCore):
|
|||||||
logger.warning(f"{error_msg}:{self.error.description}")
|
logger.warning(f"{error_msg}:{self.error.description}")
|
||||||
return res
|
return res
|
||||||
|
|
||||||
async def login(
|
async def login(self, *, login: int = 0, password: str = "", server: str = "", timeout: int = 60000) -> bool:
|
||||||
self,
|
|
||||||
*,
|
|
||||||
login: int = 0,
|
|
||||||
password: str = "",
|
|
||||||
server: str = "",
|
|
||||||
timeout: int = 60000,
|
|
||||||
) -> bool:
|
|
||||||
"""
|
"""
|
||||||
Connects to the MetaTrader terminal using the specified login, password and server.
|
Connects to the MetaTrader terminal using the specified login, password and server.
|
||||||
|
|
||||||
@@ -99,18 +82,9 @@ class MetaTrader(MetaCore):
|
|||||||
login = login or acc_details.get("login", 0)
|
login = login or acc_details.get("login", 0)
|
||||||
password = password or acc_details.get("password", "")
|
password = password or acc_details.get("password", "")
|
||||||
server = server or acc_details.get("server", "")
|
server = server or acc_details.get("server", "")
|
||||||
return await asyncio.to_thread(
|
return await asyncio.to_thread(self._login, login, password=password, server=server, timeout=timeout)
|
||||||
self._login, login, password=password, server=server, timeout=timeout
|
|
||||||
)
|
|
||||||
|
|
||||||
def login_sync(
|
def login_sync(self, *, login: int = 0, password: str = "", server: str = "", timeout: int = 60000) -> bool:
|
||||||
self,
|
|
||||||
*,
|
|
||||||
login: int = 0,
|
|
||||||
password: str = "",
|
|
||||||
server: str = "",
|
|
||||||
timeout: int = 60000,
|
|
||||||
) -> bool:
|
|
||||||
"""
|
"""
|
||||||
Connects to the MetaTrader terminal using the specified login, password and server.
|
Connects to the MetaTrader terminal using the specified login, password and server.
|
||||||
|
|
||||||
@@ -131,13 +105,7 @@ class MetaTrader(MetaCore):
|
|||||||
return res
|
return res
|
||||||
|
|
||||||
async def initialize(
|
async def initialize(
|
||||||
self,
|
self, path: str = None, login: int = 0, password: str = "", server: str = "", timeout: int | None = None, portable=False
|
||||||
path: str = None,
|
|
||||||
login: int = 0,
|
|
||||||
password: str = "",
|
|
||||||
server: str = "",
|
|
||||||
timeout: int | None = None,
|
|
||||||
portable=False,
|
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""
|
"""
|
||||||
Initializes the connection to the MetaTrader terminal. All parameters are optional.
|
Initializes the connection to the MetaTrader terminal. All parameters are optional.
|
||||||
@@ -179,13 +147,7 @@ class MetaTrader(MetaCore):
|
|||||||
return res
|
return res
|
||||||
|
|
||||||
def initialize_sync(
|
def initialize_sync(
|
||||||
self,
|
self, path: str = None, login: int = 0, password: str = "", server: str = "", timeout: int | None = None, portable=False
|
||||||
path: str = None,
|
|
||||||
login: int = 0,
|
|
||||||
password: str = "",
|
|
||||||
server: str = "",
|
|
||||||
timeout: int | None = None,
|
|
||||||
portable=False,
|
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""
|
"""
|
||||||
Initializes the connection to the MetaTrader terminal. All parameters are optional.
|
Initializes the connection to the MetaTrader terminal. All parameters are optional.
|
||||||
@@ -244,107 +206,62 @@ class MetaTrader(MetaCore):
|
|||||||
|
|
||||||
async def account_info(self) -> AccountInfo | None:
|
async def account_info(self) -> AccountInfo | None:
|
||||||
""""""
|
""""""
|
||||||
api = {
|
api = {"func": self._account_info, "error_msg": "Error in obtaining account information"}
|
||||||
"func": self._account_info,
|
|
||||||
"error_msg": "Error in obtaining account information",
|
|
||||||
}
|
|
||||||
res = await self._handler(api)
|
res = await self._handler(api)
|
||||||
return res
|
return res
|
||||||
|
|
||||||
async def terminal_info(self) -> TerminalInfo | None:
|
async def terminal_info(self) -> TerminalInfo | None:
|
||||||
api = {
|
api = {"func": self._terminal_info, "error_msg": "Error in obtaining terminal information"}
|
||||||
"func": self._terminal_info,
|
|
||||||
"error_msg": "Error in obtaining terminal information",
|
|
||||||
}
|
|
||||||
res = await self._handler(api)
|
res = await self._handler(api)
|
||||||
return res
|
return res
|
||||||
|
|
||||||
async def symbols_total(self) -> int:
|
async def symbols_total(self) -> int:
|
||||||
api = {
|
api = {"func": self._symbols_total, "error_msg": "Error in obtaining total symbols."}
|
||||||
"func": self._symbols_total,
|
|
||||||
"error_msg": "Error in obtaining total symbols.",
|
|
||||||
}
|
|
||||||
res = await self._handler(api)
|
res = await self._handler(api)
|
||||||
return res
|
return res
|
||||||
|
|
||||||
async def symbols_get(self, group: str = "") -> tuple[SymbolInfo] | None:
|
async def symbols_get(self, group: str = "") -> tuple[SymbolInfo] | None:
|
||||||
kwargs = {"group": group} if group else {}
|
kwargs = {"group": group} if group else {}
|
||||||
api = {
|
api = {"func": self._symbols_get, "kwargs": kwargs, "error_msg": "Error in obtaining symbols."}
|
||||||
"func": self._symbols_get,
|
|
||||||
"kwargs": kwargs,
|
|
||||||
"error_msg": "Error in obtaining symbols.",
|
|
||||||
}
|
|
||||||
res = await self._handler(api)
|
res = await self._handler(api)
|
||||||
return res
|
return res
|
||||||
|
|
||||||
async def symbol_info(self, symbol: str) -> SymbolInfo | None:
|
async def symbol_info(self, symbol: str) -> SymbolInfo | None:
|
||||||
api = {
|
api = {"func": self._symbol_info, "args": (symbol,), "error_msg": f"Error in obtaining information for {symbol}"}
|
||||||
"func": self._symbol_info,
|
|
||||||
"args": (symbol,),
|
|
||||||
"error_msg": f"Error in obtaining information for {symbol}",
|
|
||||||
}
|
|
||||||
res = await self._handler(api)
|
res = await self._handler(api)
|
||||||
return res
|
return res
|
||||||
|
|
||||||
async def symbol_info_tick(self, symbol: str) -> Tick | None:
|
async def symbol_info_tick(self, symbol: str) -> Tick | None:
|
||||||
api = {
|
api = {"func": self._symbol_info_tick, "args": (symbol,), "error_msg": f"Error in obtaining tick for {symbol}"}
|
||||||
"func": self._symbol_info_tick,
|
|
||||||
"args": (symbol,),
|
|
||||||
"error_msg": f"Error in obtaining tick for {symbol}",
|
|
||||||
}
|
|
||||||
res = await self._handler(api)
|
res = await self._handler(api)
|
||||||
return res
|
return res
|
||||||
|
|
||||||
async def symbol_select(self, symbol: str, enable: bool) -> bool:
|
async def symbol_select(self, symbol: str, enable: bool) -> bool:
|
||||||
api = {
|
api = {"func": self._symbol_select, "args": (symbol, enable), "error_msg": f"Error in selecting {symbol}"}
|
||||||
"func": self._symbol_select,
|
|
||||||
"args": (symbol, enable),
|
|
||||||
"error_msg": f"Error in selecting {symbol}",
|
|
||||||
}
|
|
||||||
res = await self._handler(api)
|
res = await self._handler(api)
|
||||||
return res
|
return res
|
||||||
|
|
||||||
async def market_book_add(self, symbol: str) -> bool:
|
async def market_book_add(self, symbol: str) -> bool:
|
||||||
api = {
|
api = {"func": self._market_book_add, "args": (symbol,), "error_msg": f"Error in adding {symbol} to market book"}
|
||||||
"func": self._market_book_add,
|
|
||||||
"args": (symbol,),
|
|
||||||
"error_msg": f"Error in adding {symbol} to market book",
|
|
||||||
}
|
|
||||||
res = await self._handler(api)
|
res = await self._handler(api)
|
||||||
return res
|
return res
|
||||||
|
|
||||||
async def market_book_get(self, symbol: str) -> tuple[BookInfo] | None:
|
async def market_book_get(self, symbol: str) -> tuple[BookInfo] | None:
|
||||||
api = {
|
api = {"func": self._market_book_get, "args": (symbol,), "error_msg": f"Error in obtaining market depth for {symbol}"}
|
||||||
"func": self._market_book_get,
|
|
||||||
"args": (symbol,),
|
|
||||||
"error_msg": f"Error in obtaining market depth for {symbol}",
|
|
||||||
}
|
|
||||||
res = await self._handler(api)
|
res = await self._handler(api)
|
||||||
return res
|
return res
|
||||||
|
|
||||||
async def market_book_release(self, symbol: str) -> bool:
|
async def market_book_release(self, symbol: str) -> bool:
|
||||||
api = {
|
api = {"func": self._market_book_release, "args": (symbol,), "error_msg": f"Error in releasing market depth for {symbol}"}
|
||||||
"func": self._market_book_release,
|
|
||||||
"args": (symbol,),
|
|
||||||
"error_msg": f"Error in releasing market depth for {symbol}",
|
|
||||||
}
|
|
||||||
res = await self._handler(api)
|
res = await self._handler(api)
|
||||||
return res
|
return res
|
||||||
|
|
||||||
async def copy_rates_from(
|
async def copy_rates_from(self, symbol: str, timeframe: int, date_from: datetime | float, count: int) -> np.ndarray | None:
|
||||||
self, symbol: str, timeframe: int, date_from: datetime | float, count: int
|
api = {"func": self._copy_rates_from, "args": (symbol, timeframe, date_from, count), "error_msg": f"Error in obtaining rates for {symbol}"}
|
||||||
) -> np.ndarray | None:
|
|
||||||
api = {
|
|
||||||
"func": self._copy_rates_from,
|
|
||||||
"args": (symbol, timeframe, date_from, count),
|
|
||||||
"error_msg": f"Error in obtaining rates for {symbol}",
|
|
||||||
}
|
|
||||||
res = await self._handler(api)
|
res = await self._handler(api)
|
||||||
return res
|
return res
|
||||||
|
|
||||||
async def copy_rates_from_pos(
|
async def copy_rates_from_pos(self, symbol: str, timeframe: int, start_pos: int, count: int) -> np.ndarray | None:
|
||||||
self, symbol: str, timeframe: int, start_pos: int, count: int
|
|
||||||
) -> np.ndarray | None:
|
|
||||||
api = {
|
api = {
|
||||||
"func": self._copy_rates_from_pos,
|
"func": self._copy_rates_from_pos,
|
||||||
"args": (symbol, timeframe, start_pos, count),
|
"args": (symbol, timeframe, start_pos, count),
|
||||||
@@ -353,93 +270,39 @@ class MetaTrader(MetaCore):
|
|||||||
res = await self._handler(api)
|
res = await self._handler(api)
|
||||||
return res
|
return res
|
||||||
|
|
||||||
async def copy_rates_range(
|
async def copy_rates_range(self, symbol: str, timeframe: int, date_from: datetime | float, date_to: datetime | float) -> np.ndarray | None:
|
||||||
self,
|
api = {"func": self._copy_rates_range, "args": (symbol, timeframe, date_from, date_to), "error_msg": f"Error in obtaining rates for {symbol}"}
|
||||||
symbol: str,
|
|
||||||
timeframe: int,
|
|
||||||
date_from: datetime | float,
|
|
||||||
date_to: datetime | float,
|
|
||||||
) -> np.ndarray | None:
|
|
||||||
api = {
|
|
||||||
"func": self._copy_rates_range,
|
|
||||||
"args": (symbol, timeframe, date_from, date_to),
|
|
||||||
"error_msg": f"Error in obtaining rates for {symbol}",
|
|
||||||
}
|
|
||||||
res = await self._handler(api)
|
res = await self._handler(api)
|
||||||
return res
|
return res
|
||||||
|
|
||||||
async def copy_ticks_from(
|
async def copy_ticks_from(self, symbol: str, date_from: datetime | float, count: int, flags: CopyTicks) -> np.ndarray | None:
|
||||||
self, symbol: str, date_from: datetime | float, count: int, flags: CopyTicks
|
api = {"func": self._copy_ticks_from, "args": (symbol, date_from, count, flags), "error_msg": f"Error in obtaining ticks for {symbol}"}
|
||||||
) -> np.ndarray | None:
|
|
||||||
api = {
|
|
||||||
"func": self._copy_ticks_from,
|
|
||||||
"args": (symbol, date_from, count, flags),
|
|
||||||
"error_msg": f"Error in obtaining ticks for {symbol}",
|
|
||||||
}
|
|
||||||
res = await self._handler(api)
|
res = await self._handler(api)
|
||||||
return res
|
return res
|
||||||
|
|
||||||
async def copy_ticks_range(
|
async def copy_ticks_range(self, symbol: str, date_from: datetime | float, date_to: datetime | float, flags: CopyTicks) -> np.ndarray | None:
|
||||||
self,
|
api = {"func": self._copy_ticks_range, "args": (symbol, date_from, date_to, flags), "error_msg": f"Error in obtaining ticks for {symbol}"}
|
||||||
symbol: str,
|
|
||||||
date_from: datetime | float,
|
|
||||||
date_to: datetime | float,
|
|
||||||
flags: CopyTicks,
|
|
||||||
) -> np.ndarray | None:
|
|
||||||
api = {
|
|
||||||
"func": self._copy_ticks_range,
|
|
||||||
"args": (symbol, date_from, date_to, flags),
|
|
||||||
"error_msg": f"Error in obtaining ticks for {symbol}",
|
|
||||||
}
|
|
||||||
res = await self._handler(api)
|
res = await self._handler(api)
|
||||||
return res
|
return res
|
||||||
|
|
||||||
async def orders_total(self) -> int:
|
async def orders_total(self) -> int:
|
||||||
api = {
|
api = {"func": self._orders_total, "error_msg": "Error in obtaining total orders."}
|
||||||
"func": self._orders_total,
|
|
||||||
"error_msg": "Error in obtaining total orders.",
|
|
||||||
}
|
|
||||||
res = await self._handler(api)
|
res = await self._handler(api)
|
||||||
return res
|
return res
|
||||||
|
|
||||||
async def orders_get(
|
async def orders_get(self, group: str = "", ticket: int = 0, symbol: str = "") -> tuple[TradeOrder] | None:
|
||||||
self, group: str = "", ticket: int = 0, symbol: str = ""
|
kwargs = {key: value for key, value in (("group", group), ("ticket", ticket), ("symbol", symbol)) if value}
|
||||||
) -> tuple[TradeOrder] | None:
|
api = {"func": self._orders_get, "kwargs": kwargs, "error_msg": "Error in obtaining orders."}
|
||||||
kwargs = {
|
|
||||||
key: value
|
|
||||||
for key, value in (("group", group), ("ticket", ticket), ("symbol", symbol))
|
|
||||||
if value
|
|
||||||
}
|
|
||||||
api = {
|
|
||||||
"func": self._orders_get,
|
|
||||||
"kwargs": kwargs,
|
|
||||||
"error_msg": "Error in obtaining orders.",
|
|
||||||
}
|
|
||||||
res = await self._handler(api)
|
res = await self._handler(api)
|
||||||
return res
|
return res
|
||||||
|
|
||||||
async def order_calc_margin(
|
async def order_calc_margin(self, action: Literal[OrderType.BUY, OrderType.SELL], symbol: str, volume: float, price: float) -> float | None:
|
||||||
self,
|
api = {"func": self._order_calc_margin, "args": (action, symbol, volume, price), "error_msg": "Error in calculating margin."}
|
||||||
action: Literal[OrderType.BUY, OrderType.SELL],
|
|
||||||
symbol: str,
|
|
||||||
volume: float,
|
|
||||||
price: float,
|
|
||||||
) -> float | None:
|
|
||||||
api = {
|
|
||||||
"func": self._order_calc_margin,
|
|
||||||
"args": (action, symbol, volume, price),
|
|
||||||
"error_msg": "Error in calculating margin.",
|
|
||||||
}
|
|
||||||
res = await self._handler(api)
|
res = await self._handler(api)
|
||||||
return res
|
return res
|
||||||
|
|
||||||
async def order_calc_profit(
|
async def order_calc_profit(
|
||||||
self,
|
self, action: Literal[OrderType.BUY, OrderType.SELL], symbol: str, volume: float, price_open: float, price_close: float
|
||||||
action: Literal[OrderType.BUY, OrderType.SELL],
|
|
||||||
symbol: str,
|
|
||||||
volume: float,
|
|
||||||
price_open: float,
|
|
||||||
price_close: float,
|
|
||||||
) -> float | None:
|
) -> float | None:
|
||||||
api = {
|
api = {
|
||||||
"func": self._order_calc_profit,
|
"func": self._order_calc_profit,
|
||||||
@@ -450,119 +313,50 @@ class MetaTrader(MetaCore):
|
|||||||
return res
|
return res
|
||||||
|
|
||||||
async def order_check(self, request: dict) -> OrderCheckResult:
|
async def order_check(self, request: dict) -> OrderCheckResult:
|
||||||
api = {
|
api = {"func": self._order_check, "args": (request,), "error_msg": "Error in checking order."}
|
||||||
"func": self._order_check,
|
|
||||||
"args": (request,),
|
|
||||||
"error_msg": "Error in checking order.",
|
|
||||||
}
|
|
||||||
res = await self._handler(api)
|
res = await self._handler(api)
|
||||||
return res
|
return res
|
||||||
|
|
||||||
async def order_send(self, request: dict) -> OrderSendResult:
|
async def order_send(self, request: dict) -> OrderSendResult:
|
||||||
api = {
|
api = {"func": self._order_send, "args": (request,), "error_msg": "Error in sending order."}
|
||||||
"func": self._order_send,
|
|
||||||
"args": (request,),
|
|
||||||
"error_msg": "Error in sending order.",
|
|
||||||
}
|
|
||||||
res = await self._handler(api)
|
res = await self._handler(api)
|
||||||
return res
|
return res
|
||||||
|
|
||||||
async def positions_total(self) -> int:
|
async def positions_total(self) -> int:
|
||||||
api = {
|
api = {"func": self._positions_total, "error_msg": "Error in obtaining total positions."}
|
||||||
"func": self._positions_total,
|
|
||||||
"error_msg": "Error in obtaining total positions.",
|
|
||||||
}
|
|
||||||
res = await self._handler(api)
|
res = await self._handler(api)
|
||||||
return res
|
return res
|
||||||
|
|
||||||
async def positions_get(
|
async def positions_get(self, group: str = "", ticket: int = None, symbol: str = "") -> tuple[TradePosition] | None:
|
||||||
self, group: str = "", ticket: int = None, symbol: str = ""
|
kwargs = {key: value for key, value in (("group", group), ("ticket", ticket), ("symbol", symbol)) if value}
|
||||||
) -> tuple[TradePosition] | None:
|
api = {"func": self._positions_get, "kwargs": kwargs, "error_msg": "Error in obtaining open positions."}
|
||||||
kwargs = {
|
|
||||||
key: value
|
|
||||||
for key, value in (("group", group), ("ticket", ticket), ("symbol", symbol))
|
|
||||||
if value
|
|
||||||
}
|
|
||||||
api = {
|
|
||||||
"func": self._positions_get,
|
|
||||||
"kwargs": kwargs,
|
|
||||||
"error_msg": "Error in obtaining open positions.",
|
|
||||||
}
|
|
||||||
res = await self._handler(api)
|
res = await self._handler(api)
|
||||||
return res
|
return res
|
||||||
|
|
||||||
async def history_orders_total(
|
async def history_orders_total(self, date_from: datetime | float, date_to: datetime | float) -> int:
|
||||||
self, date_from: datetime | float, date_to: datetime | float
|
api = {"func": self._history_orders_total, "args": (date_from, date_to), "error_msg": "Error in obtaining total history orders."}
|
||||||
) -> int:
|
|
||||||
api = {
|
|
||||||
"func": self._history_orders_total,
|
|
||||||
"args": (date_from, date_to),
|
|
||||||
"error_msg": "Error in obtaining total history orders.",
|
|
||||||
}
|
|
||||||
res = await self._handler(api)
|
res = await self._handler(api)
|
||||||
return res
|
return res
|
||||||
|
|
||||||
async def history_orders_get(
|
async def history_orders_get(
|
||||||
self,
|
self, date_from: datetime | float = None, date_to: datetime | float = None, group: str = "", ticket: int = None, position: int = None
|
||||||
date_from: datetime | float = None,
|
|
||||||
date_to: datetime | float = None,
|
|
||||||
group: str = "",
|
|
||||||
ticket: int = None,
|
|
||||||
position: int = None,
|
|
||||||
) -> tuple[TradeOrder] | None:
|
) -> tuple[TradeOrder] | None:
|
||||||
kwargs = {
|
kwargs = {key: value for key, value in (("group", group), ("ticket", ticket), ("position", position)) if value}
|
||||||
key: value
|
|
||||||
for key, value in (
|
|
||||||
("group", group),
|
|
||||||
("ticket", ticket),
|
|
||||||
("position", position),
|
|
||||||
)
|
|
||||||
if value
|
|
||||||
}
|
|
||||||
args = tuple(arg for arg in (date_from, date_to) if arg)
|
args = tuple(arg for arg in (date_from, date_to) if arg)
|
||||||
api = {
|
api = {"func": self._history_orders_get, "args": args, "kwargs": kwargs, "error_msg": "Error in obtaining history orders"}
|
||||||
"func": self._history_orders_get,
|
|
||||||
"args": args,
|
|
||||||
"kwargs": kwargs,
|
|
||||||
"error_msg": "Error in obtaining history orders",
|
|
||||||
}
|
|
||||||
res = await self._handler(api)
|
res = await self._handler(api)
|
||||||
return res
|
return res
|
||||||
|
|
||||||
async def history_deals_total(
|
async def history_deals_total(self, date_from: datetime | float, date_to: datetime | float) -> int:
|
||||||
self, date_from: datetime | float, date_to: datetime | float
|
api = {"func": self._history_deals_total, "args": (date_from, date_to), "error_msg": "Error in obtaining total history deals"}
|
||||||
) -> int:
|
|
||||||
api = {
|
|
||||||
"func": self._history_deals_total,
|
|
||||||
"args": (date_from, date_to),
|
|
||||||
"error_msg": "Error in obtaining total history deals",
|
|
||||||
}
|
|
||||||
res = await self._handler(api)
|
res = await self._handler(api)
|
||||||
return res
|
return res
|
||||||
|
|
||||||
async def history_deals_get(
|
async def history_deals_get(
|
||||||
self,
|
self, date_from: datetime | float = None, date_to: datetime | float = None, group: str = "", ticket: int = None, position: int = None
|
||||||
date_from: datetime | float = None,
|
|
||||||
date_to: datetime | float = None,
|
|
||||||
group: str = "",
|
|
||||||
ticket: int = None,
|
|
||||||
position: int = None,
|
|
||||||
) -> tuple[TradeDeal] | None:
|
) -> tuple[TradeDeal] | None:
|
||||||
kwargs = {
|
kwargs = {key: value for key, value in (("group", group), ("ticket", ticket), ("position", position)) if value}
|
||||||
key: value
|
|
||||||
for key, value in (
|
|
||||||
("group", group),
|
|
||||||
("ticket", ticket),
|
|
||||||
("position", position),
|
|
||||||
)
|
|
||||||
if value
|
|
||||||
}
|
|
||||||
args = tuple(arg for arg in (date_from, date_to) if arg)
|
args = tuple(arg for arg in (date_from, date_to) if arg)
|
||||||
api = {
|
api = {"func": self._history_deals_get, "args": args, "kwargs": kwargs, "error_msg": "Error in obtaining history deals"}
|
||||||
"func": self._history_deals_get,
|
|
||||||
"args": args,
|
|
||||||
"kwargs": kwargs,
|
|
||||||
"error_msg": "Error in obtaining history deals",
|
|
||||||
}
|
|
||||||
res = await self._handler(api)
|
res = await self._handler(api)
|
||||||
return res
|
return res
|
||||||
|
|||||||
@@ -354,10 +354,7 @@ class SymbolInfo(Base):
|
|||||||
name: str = ""
|
name: str = ""
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "%(class)s(name=%(name)s)" % {
|
return "%(class)s(name=%(name)s)" % {"class": self.__class__.__name__, "name": self.name}
|
||||||
"class": self.__class__.__name__,
|
|
||||||
"name": self.name,
|
|
||||||
}
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|||||||
@@ -31,9 +31,7 @@ class QueueItem:
|
|||||||
self.task_item(*self.args, **self.kwargs)
|
self.task_item(*self.args, **self.kwargs)
|
||||||
|
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
logger.error(
|
logger.error(f"Error {err} occurred in {self.task_item.__name__} with args {self.args} and kwargs {self.kwargs}")
|
||||||
f"Error {err} occurred in {self.task_item.__name__} with args {self.args} and kwargs {self.kwargs}"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class TaskQueue:
|
class TaskQueue:
|
||||||
@@ -84,9 +82,7 @@ class TaskQueue:
|
|||||||
self.queue.task_done()
|
self.queue.task_done()
|
||||||
self.priority_tasks.discard(item)
|
self.priority_tasks.discard(item)
|
||||||
|
|
||||||
if self.stop and (
|
if self.stop and (self.on_exit == "cancel" or len(self.priority_tasks) == 0):
|
||||||
self.on_exit == "cancel" or len(self.priority_tasks) == 0
|
|
||||||
):
|
|
||||||
self.cancel()
|
self.cancel()
|
||||||
break
|
break
|
||||||
|
|
||||||
@@ -107,36 +103,26 @@ class TaskQueue:
|
|||||||
async def run(self, timeout: int = 0):
|
async def run(self, timeout: int = 0):
|
||||||
start = time.perf_counter()
|
start = time.perf_counter()
|
||||||
try:
|
try:
|
||||||
self.tasks.extend(
|
self.tasks.extend(asyncio.create_task(self.worker()) for _ in range(self.workers))
|
||||||
asyncio.create_task(self.worker()) for _ in range(self.workers)
|
|
||||||
)
|
|
||||||
timeout = timeout or self.timeout
|
timeout = timeout or self.timeout
|
||||||
queue_task = asyncio.create_task(self.queue.join())
|
queue_task = asyncio.create_task(self.queue.join())
|
||||||
|
|
||||||
if timeout:
|
if timeout:
|
||||||
main_task = asyncio.create_task(
|
main_task = asyncio.create_task(asyncio.wait_for(queue_task, timeout=timeout))
|
||||||
asyncio.wait_for(queue_task, timeout=timeout)
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
main_task = queue_task
|
main_task = queue_task
|
||||||
self.tasks.append(main_task)
|
self.tasks.append(main_task)
|
||||||
await main_task
|
await main_task
|
||||||
|
|
||||||
except TimeoutError:
|
except TimeoutError:
|
||||||
logger.warning(
|
logger.warning("Timed out after %d seconds, %d tasks remaining", time.perf_counter() - start, self.queue.qsize())
|
||||||
"Timed out after %d seconds, %d tasks remaining",
|
|
||||||
time.perf_counter() - start,
|
|
||||||
self.queue.qsize(),
|
|
||||||
)
|
|
||||||
self.stop = True
|
self.stop = True
|
||||||
|
|
||||||
except asyncio.CancelledError as _:
|
except asyncio.CancelledError as _:
|
||||||
logger.warning("Main task cancelled")
|
logger.warning("Main task cancelled")
|
||||||
|
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
logger.warning(
|
logger.warning("%s: An error occurred in %s.run", err, self.__class__.__name__)
|
||||||
"%s: An error occurred in %s.run", err, self.__class__.__name__
|
|
||||||
)
|
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
await self.clean_up()
|
await self.clean_up()
|
||||||
@@ -149,9 +135,7 @@ class TaskQueue:
|
|||||||
async def clean_up(self):
|
async def clean_up(self):
|
||||||
try:
|
try:
|
||||||
if self.on_exit == "complete_priority" and len(self.priority_tasks) > 0:
|
if self.on_exit == "complete_priority" and len(self.priority_tasks) > 0:
|
||||||
logger.warning(
|
logger.warning(f"Completing {len(self.priority_tasks)} priority tasks...")
|
||||||
f"Completing {len(self.priority_tasks)} priority tasks..."
|
|
||||||
)
|
|
||||||
queue_task = asyncio.create_task(self.queue.join())
|
queue_task = asyncio.create_task(self.queue.join())
|
||||||
self.tasks.append(queue_task)
|
self.tasks.append(queue_task)
|
||||||
await queue_task
|
await queue_task
|
||||||
@@ -161,9 +145,7 @@ class TaskQueue:
|
|||||||
...
|
...
|
||||||
|
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
logger.error(
|
logger.error(f"%s: Error occurred in %s.clean_up", err, self.__class__.__name__)
|
||||||
f"%s: Error occurred in %s.clean_up", err, self.__class__.__name__
|
|
||||||
)
|
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
self.cancel()
|
self.cancel()
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ class Account(_Base, AccountInfo):
|
|||||||
Attributes:
|
Attributes:
|
||||||
connected (bool): Status of connection to MetaTrader 5 Terminal
|
connected (bool): Status of connection to MetaTrader 5 Terminal
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_instance: Self
|
_instance: Self
|
||||||
connected: bool
|
connected: bool
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ class BackTester:
|
|||||||
config (Config): Config instance
|
config (Config): Config instance
|
||||||
mt (MetaBackTester): MetaTrader instance
|
mt (MetaBackTester): MetaTrader instance
|
||||||
"""
|
"""
|
||||||
|
|
||||||
config: Config
|
config: Config
|
||||||
executor: Executor
|
executor: Executor
|
||||||
mt: MetaBackTester
|
mt: MetaBackTester
|
||||||
@@ -57,18 +58,12 @@ class BackTester:
|
|||||||
self.backtest_engine.setup_account_sync()
|
self.backtest_engine.setup_account_sync()
|
||||||
self.init_strategies_sync()
|
self.init_strategies_sync()
|
||||||
if (strategies := len(self.executor.strategy_runners)) == 0:
|
if (strategies := len(self.executor.strategy_runners)) == 0:
|
||||||
logger.warning(
|
logger.warning("No strategies were added to the backtester. Exiting ...")
|
||||||
"No strategies were added to the backtester. Exiting ..."
|
|
||||||
)
|
|
||||||
raise Exception("No strategies added to the backtester")
|
raise Exception("No strategies added to the backtester")
|
||||||
self.config.task_queue.worker_timeout = 5
|
self.config.task_queue.worker_timeout = 5
|
||||||
self.add_coroutine(
|
self.add_coroutine(coroutine=self.config.task_queue.run, on_separate_thread=True)
|
||||||
coroutine=self.config.task_queue.run, on_separate_thread=True
|
|
||||||
)
|
|
||||||
self.add_coroutine(coroutine=self.executor.exit)
|
self.add_coroutine(coroutine=self.executor.exit)
|
||||||
self.add_coroutine(
|
self.add_coroutine(coroutine=self.backtest_controller.control, on_separate_thread=True)
|
||||||
coroutine=self.backtest_controller.control, on_separate_thread=True
|
|
||||||
)
|
|
||||||
parties = strategies + 1
|
parties = strategies + 1
|
||||||
self.backtest_controller.set_parties(parties=parties)
|
self.backtest_controller.set_parties(parties=parties)
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
@@ -92,31 +87,19 @@ class BackTester:
|
|||||||
await self.backtest_engine.setup_account()
|
await self.backtest_engine.setup_account()
|
||||||
await self.init_strategies()
|
await self.init_strategies()
|
||||||
if (strategies := len(self.executor.strategy_runners)) == 0:
|
if (strategies := len(self.executor.strategy_runners)) == 0:
|
||||||
logger.warning(
|
logger.warning("No strategies were added to the backtester. Exiting ...")
|
||||||
"No strategies were added to the backtester. Exiting ..."
|
|
||||||
)
|
|
||||||
raise Exception("No strategies added to the backtester")
|
raise Exception("No strategies added to the backtester")
|
||||||
self.config.task_queue.worker_timeout = 5
|
self.config.task_queue.worker_timeout = 5
|
||||||
self.add_coroutine(
|
self.add_coroutine(coroutine=self.config.task_queue.run, on_separate_thread=True)
|
||||||
coroutine=self.config.task_queue.run, on_separate_thread=True
|
|
||||||
)
|
|
||||||
self.add_coroutine(coroutine=self.executor.exit)
|
self.add_coroutine(coroutine=self.executor.exit)
|
||||||
self.add_coroutine(
|
self.add_coroutine(coroutine=self.backtest_controller.control, on_separate_thread=True)
|
||||||
coroutine=self.backtest_controller.control, on_separate_thread=True
|
|
||||||
)
|
|
||||||
parties = strategies + 1
|
parties = strategies + 1
|
||||||
self.backtest_controller.set_parties(parties=parties)
|
self.backtest_controller.set_parties(parties=parties)
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
logger.error(f"{err}. Backtester initialization failed")
|
logger.error(f"{err}. Backtester initialization failed")
|
||||||
raise SystemExit
|
raise SystemExit
|
||||||
|
|
||||||
def add_coroutine(
|
def add_coroutine(self, *, coroutine: Callable[..., ...] | Coroutine, on_separate_thread=False, **kwargs):
|
||||||
self,
|
|
||||||
*,
|
|
||||||
coroutine: Callable[..., ...] | Coroutine,
|
|
||||||
on_separate_thread=False,
|
|
||||||
**kwargs,
|
|
||||||
):
|
|
||||||
"""Add a coroutine to the executor.
|
"""Add a coroutine to the executor.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -127,9 +110,7 @@ class BackTester:
|
|||||||
Returns:
|
Returns:
|
||||||
|
|
||||||
"""
|
"""
|
||||||
self.executor.add_coroutine(
|
self.executor.add_coroutine(coroutine=coroutine, kwargs=kwargs, on_separate_thread=on_separate_thread)
|
||||||
coroutine=coroutine, kwargs=kwargs, on_separate_thread=on_separate_thread
|
|
||||||
)
|
|
||||||
|
|
||||||
def execute(self):
|
def execute(self):
|
||||||
"""Execute the bot."""
|
"""Execute the bot."""
|
||||||
@@ -161,14 +142,7 @@ class BackTester:
|
|||||||
"""
|
"""
|
||||||
[self.add_strategy(strategy=strategy) for strategy in strategies]
|
[self.add_strategy(strategy=strategy) for strategy in strategies]
|
||||||
|
|
||||||
def add_strategy_all(
|
def add_strategy_all(self, *, strategy: Type[Strategy], params: dict | None = None, symbols: list[Symbol] = None, **kwargs):
|
||||||
self,
|
|
||||||
*,
|
|
||||||
strategy: Type[Strategy],
|
|
||||||
params: dict | None = None,
|
|
||||||
symbols: list[Symbol] = None,
|
|
||||||
**kwargs,
|
|
||||||
):
|
|
||||||
"""Use this to run a single strategy on multiple symbols with the same parameters and keyword arguments.
|
"""Use this to run a single strategy on multiple symbols with the same parameters and keyword arguments.
|
||||||
|
|
||||||
Keyword Args:
|
Keyword Args:
|
||||||
@@ -177,10 +151,7 @@ class BackTester:
|
|||||||
symbols (list): A list of symbols to run the strategy on
|
symbols (list): A list of symbols to run the strategy on
|
||||||
**kwargs: Additional keyword arguments for the strategy
|
**kwargs: Additional keyword arguments for the strategy
|
||||||
"""
|
"""
|
||||||
[
|
[self.add_strategy(strategy=strategy(symbol=symbol, params=params, **kwargs)) for symbol in symbols]
|
||||||
self.add_strategy(strategy=strategy(symbol=symbol, params=params, **kwargs))
|
|
||||||
for symbol in symbols
|
|
||||||
]
|
|
||||||
|
|
||||||
async def init_strategy(self, *, strategy: Strategy) -> bool:
|
async def init_strategy(self, *, strategy: Strategy) -> bool:
|
||||||
"""Initialize a single strategy. This method is called internally by the bot."""
|
"""Initialize a single strategy. This method is called internally by the bot."""
|
||||||
|
|||||||
+8
-29
@@ -21,6 +21,7 @@ class Bot:
|
|||||||
config (Config): Config instance
|
config (Config): Config instance
|
||||||
mt (MetaTrader): MetaTrader instance
|
mt (MetaTrader): MetaTrader instance
|
||||||
"""
|
"""
|
||||||
|
|
||||||
config: Config
|
config: Config
|
||||||
executor: Executor
|
executor: Executor
|
||||||
mt: MetaTrader
|
mt: MetaTrader
|
||||||
@@ -61,9 +62,7 @@ class Bot:
|
|||||||
raise Exception("Unable to sign in to MetaTrader 5 Terminal")
|
raise Exception("Unable to sign in to MetaTrader 5 Terminal")
|
||||||
logger.info("Login Successful")
|
logger.info("Login Successful")
|
||||||
await self.init_strategies()
|
await self.init_strategies()
|
||||||
self.add_coroutine(
|
self.add_coroutine(coroutine=self.config.task_queue.run, on_separate_thread=True)
|
||||||
coroutine=self.config.task_queue.run, on_separate_thread=True
|
|
||||||
)
|
|
||||||
self.add_coroutine(coroutine=self.executor.exit)
|
self.add_coroutine(coroutine=self.executor.exit)
|
||||||
|
|
||||||
if len(self.executor.strategy_runners) == 0:
|
if len(self.executor.strategy_runners) == 0:
|
||||||
@@ -87,9 +86,7 @@ class Bot:
|
|||||||
raise Exception("Unable to sign in to MetaTrader 5 Terminal")
|
raise Exception("Unable to sign in to MetaTrader 5 Terminal")
|
||||||
logger.info("Login Successful")
|
logger.info("Login Successful")
|
||||||
self.init_strategies_sync()
|
self.init_strategies_sync()
|
||||||
self.add_coroutine(
|
self.add_coroutine(coroutine=self.config.task_queue.run, on_separate_thread=True)
|
||||||
coroutine=self.config.task_queue.run, on_separate_thread=True
|
|
||||||
)
|
|
||||||
self.add_coroutine(coroutine=self.executor.exit)
|
self.add_coroutine(coroutine=self.executor.exit)
|
||||||
|
|
||||||
if len(self.executor.strategy_runners) == 0:
|
if len(self.executor.strategy_runners) == 0:
|
||||||
@@ -107,13 +104,7 @@ class Bot:
|
|||||||
"""
|
"""
|
||||||
self.executor.add_function(function=function, kwargs=kwargs)
|
self.executor.add_function(function=function, kwargs=kwargs)
|
||||||
|
|
||||||
def add_coroutine(
|
def add_coroutine(self, *, coroutine: Callable[..., ...] | Coroutine, on_separate_thread=False, **kwargs):
|
||||||
self,
|
|
||||||
*,
|
|
||||||
coroutine: Callable[..., ...] | Coroutine,
|
|
||||||
on_separate_thread=False,
|
|
||||||
**kwargs,
|
|
||||||
):
|
|
||||||
"""Add a coroutine to the executor.
|
"""Add a coroutine to the executor.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -124,9 +115,7 @@ class Bot:
|
|||||||
Returns:
|
Returns:
|
||||||
|
|
||||||
"""
|
"""
|
||||||
self.executor.add_coroutine(
|
self.executor.add_coroutine(coroutine=coroutine, kwargs=kwargs, on_separate_thread=on_separate_thread)
|
||||||
coroutine=coroutine, kwargs=kwargs, on_separate_thread=on_separate_thread
|
|
||||||
)
|
|
||||||
|
|
||||||
def execute(self):
|
def execute(self):
|
||||||
"""Execute the bot using asyncio.run"""
|
"""Execute the bot using asyncio.run"""
|
||||||
@@ -157,14 +146,7 @@ class Bot:
|
|||||||
"""
|
"""
|
||||||
[self.add_strategy(strategy=strategy) for strategy in strategies]
|
[self.add_strategy(strategy=strategy) for strategy in strategies]
|
||||||
|
|
||||||
def add_strategy_all(
|
def add_strategy_all(self, *, strategy: Type[Strategy], params: dict | None = None, symbols: list[Symbol] = None, **kwargs):
|
||||||
self,
|
|
||||||
*,
|
|
||||||
strategy: Type[Strategy],
|
|
||||||
params: dict | None = None,
|
|
||||||
symbols: list[Symbol] = None,
|
|
||||||
**kwargs,
|
|
||||||
):
|
|
||||||
"""Use this to run a single strategy on multiple symbols with the same parameters and keyword arguments.
|
"""Use this to run a single strategy on multiple symbols with the same parameters and keyword arguments.
|
||||||
|
|
||||||
Keyword Args:
|
Keyword Args:
|
||||||
@@ -173,10 +155,7 @@ class Bot:
|
|||||||
symbols (list): A list of symbols to run the strategy on
|
symbols (list): A list of symbols to run the strategy on
|
||||||
**kwargs: Additional keyword arguments for the strategy
|
**kwargs: Additional keyword arguments for the strategy
|
||||||
"""
|
"""
|
||||||
[
|
[self.add_strategy(strategy=strategy(symbol=symbol, params=params, **kwargs)) for symbol in symbols]
|
||||||
self.add_strategy(strategy=strategy(symbol=symbol, params=params, **kwargs))
|
|
||||||
for symbol in symbols
|
|
||||||
]
|
|
||||||
|
|
||||||
async def init_strategy(self, *, strategy: Strategy) -> bool:
|
async def init_strategy(self, *, strategy: Strategy) -> bool:
|
||||||
"""Initialize a single strategy. This method is called internally by the bot."""
|
"""Initialize a single strategy. This method is called internally by the bot."""
|
||||||
@@ -200,7 +179,7 @@ class Bot:
|
|||||||
if info is not None and tick is not None:
|
if info is not None and tick is not None:
|
||||||
info = info._asdict()
|
info = info._asdict()
|
||||||
info["swap_rollover3days"] = info.get("swap_rollover3days", 0) % 7
|
info["swap_rollover3days"] = info.get("swap_rollover3days", 0) % 7
|
||||||
info['select'] = select
|
info["select"] = select
|
||||||
tick = Tick(**tick._asdict())
|
tick = Tick(**tick._asdict())
|
||||||
strategy.symbol.tick = tick
|
strategy.symbol.tick = tick
|
||||||
strategy.symbol.set_attributes(**info)
|
strategy.symbol.set_attributes(**info)
|
||||||
|
|||||||
+13
-25
@@ -1,4 +1,5 @@
|
|||||||
"""Candle and Candles classes for handling bars from the MetaTrader 5 terminal."""
|
"""Candle and Candles classes for handling bars from the MetaTrader 5 terminal."""
|
||||||
|
|
||||||
import time
|
import time
|
||||||
from typing import Type, Self, Iterable
|
from typing import Type, Self, Iterable
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
@@ -45,9 +46,7 @@ class Candle:
|
|||||||
**kwargs: Candle attributes and values as keyword arguments.
|
**kwargs: Candle attributes and values as keyword arguments.
|
||||||
"""
|
"""
|
||||||
if not all(i in kwargs for i in ["open", "high", "low", "close"]):
|
if not all(i in kwargs for i in ["open", "high", "low", "close"]):
|
||||||
raise ValueError(
|
raise ValueError("Candle must be instantiated with open, high, low and close prices")
|
||||||
"Candle must be instantiated with open, high, low and close prices"
|
|
||||||
)
|
|
||||||
self.time = kwargs.pop("time", time.monotonic_ns())
|
self.time = kwargs.pop("time", time.monotonic_ns())
|
||||||
self.Index = kwargs.pop("Index", 0)
|
self.Index = kwargs.pop("Index", 0)
|
||||||
self.real_volume = kwargs.pop("real_volume", 0)
|
self.real_volume = kwargs.pop("real_volume", 0)
|
||||||
@@ -56,18 +55,15 @@ class Candle:
|
|||||||
self.set_attributes(**kwargs)
|
self.set_attributes(**kwargs)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return (
|
return "%(class)s(Index=%(Index)s, time=%(time)s, open=%(open)s, high=%(high)s, low=%(low)s, close=%(close)s)" % {
|
||||||
"%(class)s(Index=%(Index)s, time=%(time)s, open=%(open)s, high=%(high)s, low=%(low)s, close=%(close)s)"
|
"class": self.__class__.__name__,
|
||||||
% {
|
"open": self.open,
|
||||||
"class": self.__class__.__name__,
|
"high": self.high,
|
||||||
"open": self.open,
|
"low": self.low,
|
||||||
"high": self.high,
|
"close": self.close,
|
||||||
"low": self.low,
|
"time": self.time,
|
||||||
"close": self.close,
|
"Index": self.Index,
|
||||||
"time": self.time,
|
}
|
||||||
"Index": self.Index,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
def __eq__(self, other: Self):
|
def __eq__(self, other: Self):
|
||||||
return self.time == other.time
|
return self.time == other.time
|
||||||
@@ -170,13 +166,7 @@ class Candles:
|
|||||||
timeframe: TimeFrame
|
timeframe: TimeFrame
|
||||||
_data: DataFrame
|
_data: DataFrame
|
||||||
|
|
||||||
def __init__(
|
def __init__(self, *, data: DataFrame | Self | Iterable, flip=False, candle_class: Candle = None):
|
||||||
self,
|
|
||||||
*,
|
|
||||||
data: DataFrame | Self | Iterable,
|
|
||||||
flip=False,
|
|
||||||
candle_class: Candle = None,
|
|
||||||
):
|
|
||||||
"""A container class of Candle objects in chronological order.
|
"""A container class of Candle objects in chronological order.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -236,9 +226,7 @@ class Candles:
|
|||||||
|
|
||||||
if item == "Index":
|
if item == "Index":
|
||||||
return Series(self._data.index)
|
return Series(self._data.index)
|
||||||
raise AttributeError(
|
raise AttributeError(f"Attribute {item} not defined on class {self.__class__.__name__}")
|
||||||
f"Attribute {item} not defined on class {self.__class__.__name__}"
|
|
||||||
)
|
|
||||||
|
|
||||||
def __iter__(self):
|
def __iter__(self):
|
||||||
return (self.Candle(**row._asdict()) for row in self._data.itertuples())
|
return (self.Candle(**row._asdict()) for row in self._data.itertuples())
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ class Executor:
|
|||||||
coroutines (list[Coroutine]): A list of coroutines to run in the executor
|
coroutines (list[Coroutine]): A list of coroutines to run in the executor
|
||||||
functions (dict[Callable, dict]): A dictionary of functions to run in the executor
|
functions (dict[Callable, dict]): A dictionary of functions to run in the executor
|
||||||
"""
|
"""
|
||||||
|
|
||||||
executor: ThreadPoolExecutor
|
executor: ThreadPoolExecutor
|
||||||
tasks: list[asyncio.Task]
|
tasks: list[asyncio.Task]
|
||||||
config: Config
|
config: Config
|
||||||
@@ -36,18 +37,10 @@ class Executor:
|
|||||||
kwargs = kwargs or {}
|
kwargs = kwargs or {}
|
||||||
self.functions[function] = kwargs
|
self.functions[function] = kwargs
|
||||||
|
|
||||||
def add_coroutine(
|
def add_coroutine(self, *, coroutine: Callable | Coroutine, kwargs: dict = None, on_separate_thread=False):
|
||||||
self,
|
|
||||||
*,
|
|
||||||
coroutine: Callable | Coroutine,
|
|
||||||
kwargs: dict = None,
|
|
||||||
on_separate_thread=False,
|
|
||||||
):
|
|
||||||
kwargs = kwargs or {}
|
kwargs = kwargs or {}
|
||||||
coroutine = coroutine(**kwargs)
|
coroutine = coroutine(**kwargs)
|
||||||
self.coroutines.append(
|
self.coroutines.append(coroutine) if on_separate_thread is False else self.coroutine_threads.append(coroutine)
|
||||||
coroutine
|
|
||||||
) if on_separate_thread is False else self.coroutine_threads.append(coroutine)
|
|
||||||
|
|
||||||
def add_strategies(self, *, strategies: tuple[Strategy]):
|
def add_strategies(self, *, strategies: tuple[Strategy]):
|
||||||
"""Add multiple strategies at once
|
"""Add multiple strategies at once
|
||||||
@@ -85,9 +78,7 @@ class Executor:
|
|||||||
|
|
||||||
async def create_coroutines_task(self):
|
async def create_coroutines_task(self):
|
||||||
""""""
|
""""""
|
||||||
task = asyncio.create_task(
|
task = asyncio.create_task(asyncio.gather(*self.coroutines, return_exceptions=True))
|
||||||
asyncio.gather(*self.coroutines, return_exceptions=True)
|
|
||||||
)
|
|
||||||
self.tasks.append(task)
|
self.tasks.append(task)
|
||||||
await task
|
await task
|
||||||
|
|
||||||
@@ -116,9 +107,7 @@ class Executor:
|
|||||||
start = asyncio.get_event_loop().time()
|
start = asyncio.get_event_loop().time()
|
||||||
try:
|
try:
|
||||||
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 < (
|
if self.timeout is not None and self.timeout < (asyncio.get_event_loop().time() - start):
|
||||||
asyncio.get_event_loop().time() - start
|
|
||||||
):
|
|
||||||
self.config.shutdown = True
|
self.config.shutdown = True
|
||||||
|
|
||||||
for strategy in self.strategy_runners:
|
for strategy in self.strategy_runners:
|
||||||
@@ -140,25 +129,11 @@ 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_ = (
|
workers_ = len(self.strategy_runners) + len(self.functions) + len(self.coroutine_threads) + 2
|
||||||
len(self.strategy_runners)
|
|
||||||
+ len(self.functions)
|
|
||||||
+ len(self.coroutine_threads)
|
|
||||||
+ 2
|
|
||||||
)
|
|
||||||
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
|
||||||
[
|
[self.executor.submit(self.run_strategy, strategy) for strategy in self.strategy_runners]
|
||||||
self.executor.submit(self.run_strategy, strategy)
|
[self.executor.submit(function, **kwargs) for function, kwargs in self.functions.items()]
|
||||||
for strategy in self.strategy_runners
|
[self.executor.submit(self.run_coroutine_task, coroutine) for coroutine in self.coroutine_threads]
|
||||||
]
|
|
||||||
[
|
|
||||||
self.executor.submit(function, **kwargs)
|
|
||||||
for function, kwargs in self.functions.items()
|
|
||||||
]
|
|
||||||
[
|
|
||||||
self.executor.submit(self.run_coroutine_task, coroutine)
|
|
||||||
for coroutine in self.coroutine_threads
|
|
||||||
]
|
|
||||||
self.executor.submit(self.run_coroutine_tasks)
|
self.executor.submit(self.run_coroutine_tasks)
|
||||||
|
|||||||
+10
-51
@@ -34,14 +34,7 @@ class History:
|
|||||||
total_orders: int
|
total_orders: int
|
||||||
group: str
|
group: str
|
||||||
|
|
||||||
def __init__(
|
def __init__(self, *, date_from: datetime | float, date_to: datetime | float, group: str = "", use_utc: bool = True):
|
||||||
self,
|
|
||||||
*,
|
|
||||||
date_from: datetime | float,
|
|
||||||
date_to: datetime | float,
|
|
||||||
group: str = "",
|
|
||||||
use_utc: bool = True,
|
|
||||||
):
|
|
||||||
"""
|
"""
|
||||||
Args:
|
Args:
|
||||||
date_from (datetime, float): Date the orders are requested from. Set by the 'datetime' object or as a
|
date_from (datetime, float): Date the orders are requested from. Set by the 'datetime' object or as a
|
||||||
@@ -54,16 +47,8 @@ class History:
|
|||||||
"""
|
"""
|
||||||
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()
|
||||||
date_from = (
|
date_from = date_from if isinstance(date_from, datetime) else datetime.fromtimestamp(date_from)
|
||||||
date_from
|
date_to = date_to if isinstance(date_to, datetime) else datetime.fromtimestamp(date_to)
|
||||||
if isinstance(date_from, datetime)
|
|
||||||
else datetime.fromtimestamp(date_from)
|
|
||||||
)
|
|
||||||
date_to = (
|
|
||||||
date_to
|
|
||||||
if isinstance(date_to, datetime)
|
|
||||||
else datetime.fromtimestamp(date_to)
|
|
||||||
)
|
|
||||||
self.date_from = date_from.astimezone(pytz.UTC) if use_utc else date_from
|
self.date_from = date_from.astimezone(pytz.UTC) if use_utc else date_from
|
||||||
self.date_to = date_to.astimezone(pytz.UTC) if use_utc else date_to
|
self.date_to = date_to.astimezone(pytz.UTC) if use_utc else date_to
|
||||||
self.group = group
|
self.group = group
|
||||||
@@ -74,9 +59,7 @@ class History:
|
|||||||
|
|
||||||
async def initialize(self):
|
async def initialize(self):
|
||||||
"""Get history deals and orders"""
|
"""Get history deals and orders"""
|
||||||
deals, orders = await asyncio.gather(
|
deals, orders = await asyncio.gather(self.get_deals(), self.get_orders(), return_exceptions=True)
|
||||||
self.get_deals(), self.get_orders(), return_exceptions=True
|
|
||||||
)
|
|
||||||
self.deals = deals if isinstance(deals, tuple) else ()
|
self.deals = deals if isinstance(deals, tuple) else ()
|
||||||
self.orders = orders if isinstance(orders, tuple) else ()
|
self.orders = orders if isinstance(orders, tuple) else ()
|
||||||
self.total_deals = len(self.deals)
|
self.total_deals = len(self.deals)
|
||||||
@@ -89,9 +72,7 @@ class History:
|
|||||||
Returns:
|
Returns:
|
||||||
tuple[TradeDeal, ...]: A list of trade deals
|
tuple[TradeDeal, ...]: A list of trade deals
|
||||||
"""
|
"""
|
||||||
deals = await self.mt5.history_deals_get(
|
deals = await self.mt5.history_deals_get(date_from=self.date_from, date_to=self.date_to, group=self.group)
|
||||||
date_from=self.date_from, date_to=self.date_to, group=self.group
|
|
||||||
)
|
|
||||||
if deals is not None:
|
if deals is not None:
|
||||||
return tuple(TradeDeal(**deal._asdict()) for deal in deals)
|
return tuple(TradeDeal(**deal._asdict()) for deal in deals)
|
||||||
logger.warning(f"Failed to get deals")
|
logger.warning(f"Failed to get deals")
|
||||||
@@ -107,12 +88,7 @@ class History:
|
|||||||
Returns:
|
Returns:
|
||||||
tuple[TradeDeal]: A tuple of all deals with the order ticket
|
tuple[TradeDeal]: A tuple of all deals with the order ticket
|
||||||
"""
|
"""
|
||||||
return tuple(
|
return tuple(sorted((deal for deal in self.deals if deal.order == ticket), key=lambda x: x.time_msc))
|
||||||
sorted(
|
|
||||||
(deal for deal in self.deals if deal.order == ticket),
|
|
||||||
key=lambda x: x.time_msc,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_deals_by_position(self, *, position: int = None) -> tuple[TradeDeal, ...]:
|
def get_deals_by_position(self, *, position: int = None) -> tuple[TradeDeal, ...]:
|
||||||
"""
|
"""
|
||||||
@@ -123,12 +99,7 @@ class History:
|
|||||||
Returns:
|
Returns:
|
||||||
tuple[TradeDeal]: A tuple of all deals with the position ticket
|
tuple[TradeDeal]: A tuple of all deals with the position ticket
|
||||||
"""
|
"""
|
||||||
return tuple(
|
return tuple(sorted((deal for deal in self.deals if deal.position_id == position), key=lambda x: x.time_msc))
|
||||||
sorted(
|
|
||||||
(deal for deal in self.deals if deal.position_id == position),
|
|
||||||
key=lambda x: x.time_msc,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
@backoff_decorator
|
@backoff_decorator
|
||||||
async def get_orders(self) -> tuple[TradeOrder, ...]:
|
async def get_orders(self) -> tuple[TradeOrder, ...]:
|
||||||
@@ -137,9 +108,7 @@ class History:
|
|||||||
Returns:
|
Returns:
|
||||||
list[TradeOrder]: A list of trade orders
|
list[TradeOrder]: A list of trade orders
|
||||||
"""
|
"""
|
||||||
orders = await self.mt5.history_orders_get(
|
orders = await self.mt5.history_orders_get(date_from=self.date_from, date_to=self.date_to, group=self.group)
|
||||||
date_from=self.date_from, date_to=self.date_to, group=self.group
|
|
||||||
)
|
|
||||||
|
|
||||||
if orders is not None:
|
if orders is not None:
|
||||||
return tuple(TradeOrder(**order._asdict()) for order in orders)
|
return tuple(TradeOrder(**order._asdict()) for order in orders)
|
||||||
@@ -149,18 +118,8 @@ class History:
|
|||||||
|
|
||||||
def get_orders_by_ticket(self, *, ticket: int) -> tuple[TradeOrder, ...]:
|
def get_orders_by_ticket(self, *, ticket: int) -> tuple[TradeOrder, ...]:
|
||||||
"""filter orders by ticket"""
|
"""filter orders by ticket"""
|
||||||
return tuple(
|
return tuple(sorted((order for order in self.orders if order.ticket == ticket), key=lambda x: x.time_done_msc))
|
||||||
sorted(
|
|
||||||
(order for order in self.orders if order.ticket == ticket),
|
|
||||||
key=lambda x: x.time_done_msc,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_orders_by_position(self, *, position: int) -> tuple[TradeOrder, ...]:
|
def get_orders_by_position(self, *, position: int) -> tuple[TradeOrder, ...]:
|
||||||
"""filter orders by position"""
|
"""filter orders by position"""
|
||||||
return tuple(
|
return tuple(sorted((order for order in self.orders if order.position_id == position), key=lambda x: x.time_done_msc))
|
||||||
sorted(
|
|
||||||
(order for order in self.orders if order.position_id == position),
|
|
||||||
key=lambda x: x.time_done_msc,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|||||||
+7
-34
@@ -25,12 +25,7 @@ class Order(_Base, TradeRequest):
|
|||||||
type_time (OrderTime.DAY): Order time
|
type_time (OrderTime.DAY): Order time
|
||||||
type_filling (OrderFilling.FOK): Order filling
|
type_filling (OrderFilling.FOK): Order filling
|
||||||
"""
|
"""
|
||||||
kwargs = {
|
kwargs = {"action": TradeAction.DEAL, "type_time": OrderTime.DAY, "type_filling": OrderFilling.FOK, **kwargs}
|
||||||
"action": TradeAction.DEAL,
|
|
||||||
"type_time": OrderTime.DAY,
|
|
||||||
"type_filling": OrderFilling.FOK,
|
|
||||||
**kwargs,
|
|
||||||
}
|
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
async def orders_total(self):
|
async def orders_total(self):
|
||||||
@@ -111,9 +106,7 @@ class Order(_Base, TradeRequest):
|
|||||||
Returns:
|
Returns:
|
||||||
float: Returns float value if successful
|
float: Returns float value if successful
|
||||||
"""
|
"""
|
||||||
res = await self.mt5.order_calc_margin(
|
res = await self.mt5.order_calc_margin(self.type, self.symbol, self.volume, self.price)
|
||||||
self.type, self.symbol, self.volume, self.price
|
|
||||||
)
|
|
||||||
return res
|
return res
|
||||||
|
|
||||||
@error_handler(response=0, log_error_msg=False)
|
@error_handler(response=0, log_error_msg=False)
|
||||||
@@ -124,16 +117,8 @@ class Order(_Base, TradeRequest):
|
|||||||
float: Returns float value if successful
|
float: Returns float value if successful
|
||||||
None: If not successful
|
None: If not successful
|
||||||
"""
|
"""
|
||||||
action, symbol, volume, price_open, price_close = (
|
action, symbol, volume, price_open, price_close = (self.type, self.symbol, self.volume, self.price, self.tp)
|
||||||
self.type,
|
res = await self.mt5.order_calc_profit(action, symbol, volume, price_open, price_close)
|
||||||
self.symbol,
|
|
||||||
self.volume,
|
|
||||||
self.price,
|
|
||||||
self.tp,
|
|
||||||
)
|
|
||||||
res = await self.mt5.order_calc_profit(
|
|
||||||
action, symbol, volume, price_open, price_close
|
|
||||||
)
|
|
||||||
return res
|
return res
|
||||||
|
|
||||||
@error_handler(response=0, log_error_msg=False)
|
@error_handler(response=0, log_error_msg=False)
|
||||||
@@ -144,23 +129,11 @@ class Order(_Base, TradeRequest):
|
|||||||
float: Returns float value if successful
|
float: Returns float value if successful
|
||||||
None: If not successful
|
None: If not successful
|
||||||
"""
|
"""
|
||||||
action, symbol, volume, price_open, price_close = (
|
action, symbol, volume, price_open, price_close = (self.type, self.symbol, self.volume, self.price, self.sl)
|
||||||
self.type,
|
res = await self.mt5.order_calc_profit(action, symbol, volume, price_open, price_close)
|
||||||
self.symbol,
|
|
||||||
self.volume,
|
|
||||||
self.price,
|
|
||||||
self.sl,
|
|
||||||
)
|
|
||||||
res = await self.mt5.order_calc_profit(
|
|
||||||
action, symbol, volume, price_open, price_close
|
|
||||||
)
|
|
||||||
return res
|
return res
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def request(self) -> dict:
|
def request(self) -> dict:
|
||||||
"""Return the order request as a dictionary."""
|
"""Return the order request as a dictionary."""
|
||||||
return {
|
return {key: value for key, value in self.dict.items() if key in self.mt5.TradeRequest.__match_args__}
|
||||||
key: value
|
|
||||||
for key, value in self.dict.items()
|
|
||||||
if key in self.mt5.TradeRequest.__match_args__
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
"""Handle Open positions."""
|
"""Handle Open positions."""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
|
|
||||||
@@ -70,9 +71,7 @@ class Positions:
|
|||||||
return tuple(TradePosition(**pos._asdict()) for pos in (positions or ()))
|
return tuple(TradePosition(**pos._asdict()) for pos in (positions or ()))
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def close(
|
async def close(*, ticket: int, symbol: str, price: float, volume: float, order_type: OrderType) -> OrderSendResult:
|
||||||
*, ticket: int, symbol: str, price: float, volume: float, order_type: OrderType
|
|
||||||
) -> OrderSendResult:
|
|
||||||
"""Close an open position for the trading account using the ticket and other parameters.
|
"""Close an open position for the trading account using the ticket and other parameters.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -82,14 +81,7 @@ class Positions:
|
|||||||
volume (float): Volume to close.
|
volume (float): Volume to close.
|
||||||
order_type (OrderType): Order type.
|
order_type (OrderType): Order type.
|
||||||
"""
|
"""
|
||||||
order = Order(
|
order = Order(action=TradeAction.DEAL, price=price, position=ticket, symbol=symbol, volume=volume, type=order_type.opposite)
|
||||||
action=TradeAction.DEAL,
|
|
||||||
price=price,
|
|
||||||
position=ticket,
|
|
||||||
symbol=symbol,
|
|
||||||
volume=volume,
|
|
||||||
type=order_type.opposite,
|
|
||||||
)
|
|
||||||
return await order.send()
|
return await order.send()
|
||||||
|
|
||||||
async def close_position_by_ticket(self, *, ticket: int) -> OrderSendResult | None:
|
async def close_position_by_ticket(self, *, ticket: int) -> OrderSendResult | None:
|
||||||
@@ -127,14 +119,5 @@ class Positions:
|
|||||||
int: Return number of positions closed.
|
int: Return number of positions closed.
|
||||||
"""
|
"""
|
||||||
positions = self.positions or await self.get_positions()
|
positions = self.positions or await self.get_positions()
|
||||||
results = await asyncio.gather(
|
results = await asyncio.gather(*(self.close_position(position=position) for position in positions), return_exceptions=True)
|
||||||
*(self.close_position(position=position) for position in positions),
|
return len([res for res in results if (isinstance(res, OrderSendResult) and res.retcode == 10009)])
|
||||||
return_exceptions=True,
|
|
||||||
)
|
|
||||||
return len(
|
|
||||||
[
|
|
||||||
res
|
|
||||||
for res in results
|
|
||||||
if (isinstance(res, OrderSendResult) and res.retcode == 10009)
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
"""Risk Assessment and Management"""
|
"""Risk Assessment and Management"""
|
||||||
|
|
||||||
from .account import Account
|
from .account import Account
|
||||||
from .positions import Positions
|
from .positions import Positions
|
||||||
|
|
||||||
|
|||||||
@@ -35,12 +35,8 @@ class Result:
|
|||||||
self.name = name or self.parameters.get("name", "Trades")
|
self.name = name or self.parameters.get("name", "Trades")
|
||||||
|
|
||||||
def get_data(self) -> dict:
|
def get_data(self) -> dict:
|
||||||
res = self.result.get_dict(
|
res = self.result.get_dict(exclude={"retcode", "comment", "retcode_external", "request_id", "request"})
|
||||||
exclude={"retcode", "comment", "retcode_external", "request_id", "request"}
|
return self.parameters | res | {"actual_profit": 0, "closed": False, "win": False}
|
||||||
)
|
|
||||||
return (
|
|
||||||
self.parameters | res | {"actual_profit": 0, "closed": False, "win": False}
|
|
||||||
)
|
|
||||||
|
|
||||||
async def save(self, *, trade_record_mode: Literal["csv", "json"] = None):
|
async def save(self, *, trade_record_mode: Literal["csv", "json"] = None):
|
||||||
"""Record trade results as a csv or json file
|
"""Record trade results as a csv or json file
|
||||||
@@ -71,9 +67,7 @@ class Result:
|
|||||||
headers.update(data.keys())
|
headers.update(data.keys())
|
||||||
read_file.close()
|
read_file.close()
|
||||||
with file.open("w", newline="") as write_file:
|
with file.open("w", newline="") as write_file:
|
||||||
writer = csv.DictWriter(
|
writer = csv.DictWriter(write_file, fieldnames=headers, restval=None, extrasaction="ignore")
|
||||||
write_file, fieldnames=headers, restval=None, extrasaction="ignore"
|
|
||||||
)
|
|
||||||
writer.writeheader()
|
writer.writeheader()
|
||||||
writer.writerows(rows)
|
writer.writerows(rows)
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
|
|||||||
+11
-46
@@ -23,12 +23,7 @@ def delta(obj: time) -> timedelta:
|
|||||||
Args:
|
Args:
|
||||||
obj (datetime.time): A datetime.time object.
|
obj (datetime.time): A datetime.time object.
|
||||||
"""
|
"""
|
||||||
return timedelta(
|
return timedelta(hours=obj.hour, minutes=obj.minute, seconds=obj.second, microseconds=obj.microsecond)
|
||||||
hours=obj.hour,
|
|
||||||
minutes=obj.minute,
|
|
||||||
seconds=obj.second,
|
|
||||||
microseconds=obj.microsecond,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def backtest_sleep(secs):
|
async def backtest_sleep(secs):
|
||||||
@@ -58,9 +53,7 @@ class Session:
|
|||||||
*,
|
*,
|
||||||
start: int | time,
|
start: int | time,
|
||||||
end: int | time,
|
end: int | time,
|
||||||
on_start: Literal[
|
on_start: Literal["close_all", "close_win", "close_loss", "custom_start"] = None,
|
||||||
"close_all", "close_win", "close_loss", "custom_start"
|
|
||||||
] = None,
|
|
||||||
on_end: Literal["close_all", "close_win", "close_loss", "custom_end"] = None,
|
on_end: Literal["close_all", "close_win", "close_loss", "custom_end"] = None,
|
||||||
custom_start: Callable = None,
|
custom_start: Callable = None,
|
||||||
custom_end: Callable = None,
|
custom_end: Callable = None,
|
||||||
@@ -79,11 +72,7 @@ class Session:
|
|||||||
custom_end (Callable): A custom function to call when the session ends. Default is None.
|
custom_end (Callable): A custom function to call when the session ends. Default is None.
|
||||||
name (str): A name for the session. Default is a combination of start and end.
|
name (str): A name for the session. Default is a combination of start and end.
|
||||||
"""
|
"""
|
||||||
self.start = (
|
self.start = start.replace(tzinfo=UTC) if isinstance(start, time) else time(hour=start, tzinfo=UTC)
|
||||||
start.replace(tzinfo=UTC)
|
|
||||||
if isinstance(start, time)
|
|
||||||
else time(hour=start, tzinfo=UTC)
|
|
||||||
)
|
|
||||||
self.end = end if isinstance(end, time) else time(hour=end, tzinfo=UTC)
|
self.end = end if isinstance(end, time) else time(hour=end, tzinfo=UTC)
|
||||||
self.on_start = on_start
|
self.on_start = on_start
|
||||||
self.on_end = on_end
|
self.on_end = on_end
|
||||||
@@ -112,9 +101,7 @@ class Session:
|
|||||||
now = (
|
now = (
|
||||||
datetime.now(tz=UTC).time()
|
datetime.now(tz=UTC).time()
|
||||||
if self.config.mode == "live"
|
if self.config.mode == "live"
|
||||||
else datetime.fromtimestamp(
|
else datetime.fromtimestamp(self.config.backtest_engine.cursor.time, tz=UTC).time()
|
||||||
self.config.backtest_engine.cursor.time, tz=UTC
|
|
||||||
).time()
|
|
||||||
)
|
)
|
||||||
return now in self
|
return now in self
|
||||||
|
|
||||||
@@ -133,13 +120,7 @@ class Session:
|
|||||||
return Duration(hours=hours, minutes=minutes, seconds=seconds)
|
return Duration(hours=hours, minutes=minutes, seconds=seconds)
|
||||||
|
|
||||||
async def close_positions(self, *, positions: tuple[TradePosition, ...]):
|
async def close_positions(self, *, positions: tuple[TradePosition, ...]):
|
||||||
results = asyncio.gather(
|
results = asyncio.gather(*(self.positions_manager.close_position(position=position) for position in positions), return_exceptions=True)
|
||||||
*(
|
|
||||||
self.positions_manager.close_position(position=position)
|
|
||||||
for position in positions
|
|
||||||
),
|
|
||||||
return_exceptions=True,
|
|
||||||
)
|
|
||||||
closed = pending = 0
|
closed = pending = 0
|
||||||
for result in results:
|
for result in results:
|
||||||
if isinstance(result, OrderSendResult) and result.retcode == 10009:
|
if isinstance(result, OrderSendResult) and result.retcode == 10009:
|
||||||
@@ -155,16 +136,12 @@ class Session:
|
|||||||
|
|
||||||
async def close_win(self):
|
async def close_win(self):
|
||||||
open_positions = await self.positions_manager.get_positions()
|
open_positions = await self.positions_manager.get_positions()
|
||||||
positions = tuple(
|
positions = tuple(position for position in open_positions if position.profit >= 0)
|
||||||
position for position in open_positions if position.profit >= 0
|
|
||||||
)
|
|
||||||
await self.close_positions(positions=positions)
|
await self.close_positions(positions=positions)
|
||||||
|
|
||||||
async def close_loss(self):
|
async def close_loss(self):
|
||||||
open_positions = await self.positions_manager.get_positions()
|
open_positions = await self.positions_manager.get_positions()
|
||||||
positions = tuple(
|
positions = tuple(position for position in open_positions if position.profit < 0)
|
||||||
position for position in open_positions if position.profit < 0
|
|
||||||
)
|
|
||||||
await self.close_positions(positions=positions)
|
await self.close_positions(positions=positions)
|
||||||
|
|
||||||
async def action(self, *, action):
|
async def action(self, *, action):
|
||||||
@@ -198,9 +175,7 @@ class Session:
|
|||||||
def until(self):
|
def until(self):
|
||||||
"""Get the seconds until the session starts from the current time in seconds."""
|
"""Get the seconds until the session starts from the current time in seconds."""
|
||||||
if self.config.mode == "backtest":
|
if self.config.mode == "backtest":
|
||||||
now = datetime.fromtimestamp(
|
now = datetime.fromtimestamp(self.config.backtest_engine.cursor.time, tz=UTC).time()
|
||||||
self.config.backtest_engine.cursor.time, tz=UTC
|
|
||||||
).time()
|
|
||||||
secs = (delta(self.start) - delta(now)).seconds
|
secs = (delta(self.start) - delta(now)).seconds
|
||||||
else:
|
else:
|
||||||
secs = (delta(self.start) - delta(datetime.now(tz=UTC).time())).seconds
|
secs = (delta(self.start) - delta(datetime.now(tz=UTC).time())).seconds
|
||||||
@@ -237,11 +212,7 @@ class Sessions:
|
|||||||
moment = (
|
moment = (
|
||||||
moment or datetime.now(tz=UTC).time()
|
moment or datetime.now(tz=UTC).time()
|
||||||
if self.config.mode == "live"
|
if self.config.mode == "live"
|
||||||
else (
|
else (datetime.fromtimestamp(self.config.backtest_engine.cursor.time, tz=UTC).time())
|
||||||
datetime.fromtimestamp(
|
|
||||||
self.config.backtest_engine.cursor.time, tz=UTC
|
|
||||||
).time()
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
for session in self.sessions:
|
for session in self.sessions:
|
||||||
if moment in session:
|
if moment in session:
|
||||||
@@ -260,11 +231,7 @@ class Sessions:
|
|||||||
moment = (
|
moment = (
|
||||||
moment or datetime.now(tz=UTC).time()
|
moment or datetime.now(tz=UTC).time()
|
||||||
if self.config.mode == "live"
|
if self.config.mode == "live"
|
||||||
else (
|
else (datetime.fromtimestamp(self.config.backtest_engine.cursor.time, tz=UTC).time())
|
||||||
datetime.fromtimestamp(
|
|
||||||
self.config.backtest_engine.cursor.time, tz=UTC
|
|
||||||
).time()
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
for session in self.sessions:
|
for session in self.sessions:
|
||||||
if delta(moment) < delta(session.start):
|
if delta(moment) < delta(session.start):
|
||||||
@@ -287,9 +254,7 @@ class Sessions:
|
|||||||
return
|
return
|
||||||
|
|
||||||
if self.config.mode == "backtest":
|
if self.config.mode == "backtest":
|
||||||
now = datetime.fromtimestamp(
|
now = datetime.fromtimestamp(self.config.backtest_engine.cursor.time, tz=UTC).time()
|
||||||
self.config.backtest_engine.cursor.time, tz=UTC
|
|
||||||
).time()
|
|
||||||
else:
|
else:
|
||||||
now = datetime.now(tz=UTC).time()
|
now = datetime.now(tz=UTC).time()
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
"""The base class for creating strategies."""
|
"""The base class for creating strategies."""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from time import time
|
from time import time
|
||||||
from typing import TypeVar
|
from typing import TypeVar
|
||||||
@@ -46,9 +47,7 @@ class Strategy(ABC):
|
|||||||
backtest_controller = BackTestController
|
backtest_controller = BackTestController
|
||||||
current_session = Session
|
current_session = Session
|
||||||
|
|
||||||
def __init__(
|
def __init__(self, *, symbol: Symbol, params: dict = None, sessions: Sessions = None, name=""):
|
||||||
self, *, symbol: Symbol, params: dict = None, sessions: Sessions = None, name=""
|
|
||||||
):
|
|
||||||
"""Initiate the parameters dict and add name and symbol fields.
|
"""Initiate the parameters dict and add name and symbol fields.
|
||||||
Use class name as strategy name if name is not provided
|
Use class name as strategy name if name is not provided
|
||||||
|
|
||||||
@@ -62,9 +61,7 @@ class Strategy(ABC):
|
|||||||
self.parameters["symbol"] = symbol.name
|
self.parameters["symbol"] = symbol.name
|
||||||
self.parameters["name"] = self.name
|
self.parameters["name"] = self.name
|
||||||
self.running = True
|
self.running = True
|
||||||
self.sessions = sessions or Sessions(
|
self.sessions = sessions or Sessions(sessions=[Session(start=0, end=dtime(hour=23, minute=59, second=59))])
|
||||||
sessions=[Session(start=0, end=dtime(hour=23, minute=59, second=59))]
|
|
||||||
)
|
|
||||||
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.backtest_controller = BackTestController()
|
self.backtest_controller = BackTestController()
|
||||||
@@ -165,9 +162,7 @@ class Strategy(ABC):
|
|||||||
async def backtest_strategy(self):
|
async def backtest_strategy(self):
|
||||||
"""Backtest the strategy."""
|
"""Backtest the strategy."""
|
||||||
async with self as _:
|
async with self as _:
|
||||||
logger.info(
|
logger.info("Testing %s strategy on %s with Backtester", self.name, self.symbol.name)
|
||||||
"Testing %s strategy on %s with Backtester", self.name, self.symbol.name
|
|
||||||
)
|
|
||||||
while self.running:
|
while self.running:
|
||||||
try:
|
try:
|
||||||
await self.sessions.check()
|
await self.sessions.check()
|
||||||
|
|||||||
+12
-48
@@ -1,4 +1,5 @@
|
|||||||
"""Symbol class for handling a financial instrument."""
|
"""Symbol class for handling a financial instrument."""
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
|
|
||||||
@@ -157,10 +158,7 @@ class Symbol(_Base, SymbolInfo):
|
|||||||
if check := self.volume_min <= volume <= self.volume_max:
|
if check := self.volume_min <= volume <= self.volume_max:
|
||||||
return check, volume
|
return check, volume
|
||||||
else:
|
else:
|
||||||
return (
|
return (check, self.volume_min if volume <= self.volume_min else self.volume_max)
|
||||||
check,
|
|
||||||
self.volume_min if volume <= self.volume_min else self.volume_max,
|
|
||||||
)
|
|
||||||
|
|
||||||
def round_off_volume(self, *, volume: float, round_down: bool = False) -> float:
|
def round_off_volume(self, *, volume: float, round_down: bool = False) -> float:
|
||||||
"""Round off the volume to the nearest volume step.
|
"""Round off the volume to the nearest volume step.
|
||||||
@@ -177,11 +175,7 @@ class Symbol(_Base, SymbolInfo):
|
|||||||
async def amount_in_quote_currency(self, *, amount: float) -> float:
|
async def amount_in_quote_currency(self, *, amount: float) -> float:
|
||||||
"""Convert the amount to the quote currency of the symbol."""
|
"""Convert the amount to the quote currency of the symbol."""
|
||||||
if self.currency_profit != self.account.currency:
|
if self.currency_profit != self.account.currency:
|
||||||
amount = await self.convert_currency(
|
amount = await self.convert_currency(amount=amount, from_currency=self.account.currency, to_currency=self.currency_profit)
|
||||||
amount=amount,
|
|
||||||
from_currency=self.account.currency,
|
|
||||||
to_currency=self.currency_profit,
|
|
||||||
)
|
|
||||||
return amount
|
return amount
|
||||||
|
|
||||||
async def compute_volume(self) -> float:
|
async def compute_volume(self) -> float:
|
||||||
@@ -194,9 +188,7 @@ class Symbol(_Base, SymbolInfo):
|
|||||||
"""
|
"""
|
||||||
return self.volume_min
|
return self.volume_min
|
||||||
|
|
||||||
async def convert_currency(
|
async def convert_currency(self, *, amount: float, from_currency: str, to_currency: str) -> float:
|
||||||
self, *, amount: float, from_currency: str, to_currency: str
|
|
||||||
) -> float:
|
|
||||||
"""Convert a given amount from one currency to the other.
|
"""Convert a given amount from one currency to the other.
|
||||||
Args:
|
Args:
|
||||||
amount: Amount to convert
|
amount: Amount to convert
|
||||||
@@ -215,14 +207,10 @@ class Symbol(_Base, SymbolInfo):
|
|||||||
if tick is not None:
|
if tick is not None:
|
||||||
return round(amount / tick.ask, 2)
|
return round(amount / tick.ask, 2)
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
logger.warning(
|
logger.warning(f"{err}: Currency conversion failed: Unable to convert {amount} in {quote} to {base}")
|
||||||
f"{err}: Currency conversion failed: Unable to convert {amount} in {quote} to {base}"
|
|
||||||
)
|
|
||||||
|
|
||||||
@backoff_decorator
|
@backoff_decorator
|
||||||
async def copy_rates_from(
|
async def copy_rates_from(self, *, timeframe: TimeFrame, date_from: datetime | int, count: int = 500) -> Candles:
|
||||||
self, *, timeframe: TimeFrame, date_from: datetime | int, count: int = 500
|
|
||||||
) -> Candles:
|
|
||||||
"""
|
"""
|
||||||
Get bars from the MetaTrader 5 terminal starting from the specified date.
|
Get bars from the MetaTrader 5 terminal starting from the specified date.
|
||||||
|
|
||||||
@@ -246,9 +234,7 @@ class Symbol(_Base, SymbolInfo):
|
|||||||
raise ValueError(f"Could not get rates for {self.name}.")
|
raise ValueError(f"Could not get rates for {self.name}.")
|
||||||
|
|
||||||
@backoff_decorator
|
@backoff_decorator
|
||||||
async def copy_rates_from_pos(
|
async def copy_rates_from_pos(self, *, timeframe: TimeFrame, count: int = 500, start_position: int = 0) -> Candles:
|
||||||
self, *, timeframe: TimeFrame, count: int = 500, start_position: int = 0
|
|
||||||
) -> Candles:
|
|
||||||
"""Get bars from the MetaTrader 5 terminal starting from the specified index.
|
"""Get bars from the MetaTrader 5 terminal starting from the specified index.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -265,21 +251,13 @@ class Symbol(_Base, SymbolInfo):
|
|||||||
Raises:
|
Raises:
|
||||||
ValueError: If request was unsuccessful and None was returned
|
ValueError: If request was unsuccessful and None was returned
|
||||||
"""
|
"""
|
||||||
rates = await self.mt5.copy_rates_from_pos(
|
rates = await self.mt5.copy_rates_from_pos(self.name, timeframe, start_position, count)
|
||||||
self.name, timeframe, start_position, count
|
|
||||||
)
|
|
||||||
if rates is not None:
|
if rates is not None:
|
||||||
return Candles(data=rates)
|
return Candles(data=rates)
|
||||||
raise ValueError(f"Could not get rates for {self.name}.")
|
raise ValueError(f"Could not get rates for {self.name}.")
|
||||||
|
|
||||||
@backoff_decorator
|
@backoff_decorator
|
||||||
async def copy_rates_range(
|
async def copy_rates_range(self, *, timeframe: TimeFrame, date_from: datetime | int, date_to: datetime | int) -> Candles:
|
||||||
self,
|
|
||||||
*,
|
|
||||||
timeframe: TimeFrame,
|
|
||||||
date_from: datetime | int,
|
|
||||||
date_to: datetime | int,
|
|
||||||
) -> Candles:
|
|
||||||
"""Get bars in the specified date range from the MetaTrader 5 terminal.
|
"""Get bars in the specified date range from the MetaTrader 5 terminal.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -299,21 +277,13 @@ class Symbol(_Base, SymbolInfo):
|
|||||||
Raises:
|
Raises:
|
||||||
ValueError: If request was unsuccessful and None was returned
|
ValueError: If request was unsuccessful and None was returned
|
||||||
"""
|
"""
|
||||||
rates = await self.mt5.copy_rates_range(
|
rates = await self.mt5.copy_rates_range(symbol=self.name, timeframe=timeframe, date_from=date_from, date_to=date_to)
|
||||||
symbol=self.name, timeframe=timeframe, date_from=date_from, date_to=date_to
|
|
||||||
)
|
|
||||||
if rates is not None:
|
if rates is not None:
|
||||||
return Candles(data=rates)
|
return Candles(data=rates)
|
||||||
raise ValueError(f"Could not get rates for {self.name}.")
|
raise ValueError(f"Could not get rates for {self.name}.")
|
||||||
|
|
||||||
@backoff_decorator
|
@backoff_decorator
|
||||||
async def copy_ticks_from(
|
async def copy_ticks_from(self, *, date_from: datetime | int, count: int = 100, flags: CopyTicks = CopyTicks.ALL) -> Ticks:
|
||||||
self,
|
|
||||||
*,
|
|
||||||
date_from: datetime | int,
|
|
||||||
count: int = 100,
|
|
||||||
flags: CopyTicks = CopyTicks.ALL,
|
|
||||||
) -> Ticks:
|
|
||||||
"""
|
"""
|
||||||
Get ticks from the MetaTrader 5 terminal starting from the specified date.
|
Get ticks from the MetaTrader 5 terminal starting from the specified date.
|
||||||
|
|
||||||
@@ -336,13 +306,7 @@ class Symbol(_Base, SymbolInfo):
|
|||||||
raise ValueError(f"Could not get ticks for {self.name}.")
|
raise ValueError(f"Could not get ticks for {self.name}.")
|
||||||
|
|
||||||
@backoff_decorator
|
@backoff_decorator
|
||||||
async def copy_ticks_range(
|
async def copy_ticks_range(self, *, date_from: datetime | int, date_to: datetime | int, flags: CopyTicks = CopyTicks.ALL) -> Ticks:
|
||||||
self,
|
|
||||||
*,
|
|
||||||
date_from: datetime | int,
|
|
||||||
date_to: datetime | int,
|
|
||||||
flags: CopyTicks = CopyTicks.ALL,
|
|
||||||
) -> Ticks:
|
|
||||||
"""Get ticks for the specified date range from the MetaTrader 5 terminal.
|
"""Get ticks for the specified date range from the MetaTrader 5 terminal.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
"""Terminal related functions and properties"""
|
"""Terminal related functions and properties"""
|
||||||
|
|
||||||
from typing import NamedTuple
|
from typing import NamedTuple
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
|
|
||||||
@@ -7,9 +8,7 @@ from ..core.base import _Base
|
|||||||
|
|
||||||
logger = getLogger(__name__)
|
logger = getLogger(__name__)
|
||||||
|
|
||||||
Version = NamedTuple(
|
Version = NamedTuple("Version", (("version", str), ("build", int), ("release_date", str)))
|
||||||
"Version", (("version", str), ("build", int), ("release_date", str))
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class Terminal(_Base, TerminalInfo):
|
class Terminal(_Base, TerminalInfo):
|
||||||
|
|||||||
+11
-18
@@ -38,27 +38,22 @@ class Tick:
|
|||||||
"""Initialize the Tick class. Set attributes from keyword arguments. bid, ask, last and volume must be
|
"""Initialize the Tick class. Set attributes from keyword arguments. bid, ask, last and volume must be
|
||||||
present"""
|
present"""
|
||||||
if not all(key in kwargs for key in ["bid", "ask", "last", "volume"]):
|
if not all(key in kwargs for key in ["bid", "ask", "last", "volume"]):
|
||||||
raise ValueError(
|
raise ValueError("bid, ask, last and volume, time must be present in the keyword arguments")
|
||||||
"bid, ask, last and volume, time must be present in the keyword arguments"
|
|
||||||
)
|
|
||||||
self.Index = kwargs.pop("Index", 0)
|
self.Index = kwargs.pop("Index", 0)
|
||||||
self.time = kwargs.pop("time", time.monotonic())
|
self.time = kwargs.pop("time", time.monotonic())
|
||||||
self.time_msc = int(self.time * 1000)
|
self.time_msc = int(self.time * 1000)
|
||||||
self.set_attributes(**kwargs)
|
self.set_attributes(**kwargs)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return (
|
return "%(class)s(Index=%(Index)s, time=%(time)s, bid=%(bid)s, ask=%(ask)s, last=%(last)s, volume=%(volume)s)" % {
|
||||||
"%(class)s(Index=%(Index)s, time=%(time)s, bid=%(bid)s, ask=%(ask)s, last=%(last)s, volume=%(volume)s)"
|
"class": self.__class__.__name__,
|
||||||
% {
|
"time": self.time,
|
||||||
"class": self.__class__.__name__,
|
"bid": self.bid,
|
||||||
"time": self.time,
|
"ask": self.ask,
|
||||||
"bid": self.bid,
|
"last": self.last,
|
||||||
"ask": self.ask,
|
"volume": self.volume,
|
||||||
"last": self.last,
|
"Index": self.Index,
|
||||||
"volume": self.volume,
|
}
|
||||||
"Index": self.Index,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
def __eq__(self, other: Self):
|
def __eq__(self, other: Self):
|
||||||
return self.time == other.time
|
return self.time == other.time
|
||||||
@@ -148,9 +143,7 @@ class Ticks:
|
|||||||
def __getattr__(self, item):
|
def __getattr__(self, item):
|
||||||
if item in list(self._data.columns.values):
|
if item in list(self._data.columns.values):
|
||||||
return self._data[item]
|
return self._data[item]
|
||||||
raise AttributeError(
|
raise AttributeError(f"Attribute {item} not defined on class {self.__class__.__name__}")
|
||||||
f"Attribute {item} not defined on class {self.__class__.__name__}"
|
|
||||||
)
|
|
||||||
|
|
||||||
def __getitem__(self, index) -> Tick | Self:
|
def __getitem__(self, index) -> Tick | Self:
|
||||||
if isinstance(index, slice):
|
if isinstance(index, slice):
|
||||||
|
|||||||
@@ -73,12 +73,7 @@ class TradeRecords:
|
|||||||
rows = await self.update_rows(rows=rows)
|
rows = await self.update_rows(rows=rows)
|
||||||
|
|
||||||
with open(file, mode="w", newline="") as fw:
|
with open(file, mode="w", newline="") as fw:
|
||||||
writer = csv.DictWriter(
|
writer = csv.DictWriter(fw, fieldnames=reader.fieldnames, extrasaction="ignore", restval=None)
|
||||||
fw,
|
|
||||||
fieldnames=reader.fieldnames,
|
|
||||||
extrasaction="ignore",
|
|
||||||
restval=None,
|
|
||||||
)
|
|
||||||
writer.writeheader()
|
writer.writeheader()
|
||||||
writer.writerows(rows)
|
writer.writerows(rows)
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
@@ -119,12 +114,7 @@ class TradeRecords:
|
|||||||
deals = [
|
deals = [
|
||||||
deal
|
deal
|
||||||
for deal in deals
|
for deal in deals
|
||||||
if (
|
if (deal.order != deal.position_id and deal.position_id == order and deal.entry == 1 and deal.position_id not in position_ids)
|
||||||
deal.order != deal.position_id
|
|
||||||
and deal.position_id == order
|
|
||||||
and deal.entry == 1
|
|
||||||
and deal.position_id not in position_ids
|
|
||||||
)
|
|
||||||
]
|
]
|
||||||
deals.sort(key=lambda deal: deal.time_msc)
|
deals.sort(key=lambda deal: deal.time_msc)
|
||||||
deal = deals[-1]
|
deal = deals[-1]
|
||||||
@@ -157,16 +147,12 @@ class TradeRecords:
|
|||||||
|
|
||||||
async def update_csv_records(self):
|
async def update_csv_records(self):
|
||||||
"""Update csv trade records in the records_dir folder."""
|
"""Update csv trade records in the records_dir folder."""
|
||||||
records = [
|
records = [self.read_update_csv(file=record) for record in self.get_csv_records()]
|
||||||
self.read_update_csv(file=record) for record in self.get_csv_records()
|
|
||||||
]
|
|
||||||
await asyncio.gather(*records)
|
await asyncio.gather(*records)
|
||||||
|
|
||||||
async def update_json_records(self):
|
async def update_json_records(self):
|
||||||
"""Update json trade records in the records_dir folder."""
|
"""Update json trade records in the records_dir folder."""
|
||||||
records = [
|
records = [self.read_update_json(file=record) for record in self.get_json_records()]
|
||||||
self.read_update_json(file=record) for record in self.get_json_records()
|
|
||||||
]
|
|
||||||
await asyncio.gather(*records)
|
await asyncio.gather(*records)
|
||||||
|
|
||||||
async def update_csv_record(self, *, file: Path | str):
|
async def update_csv_record(self, *, file: Path | str):
|
||||||
|
|||||||
+12
-53
@@ -60,17 +60,11 @@ class Trader(ABC):
|
|||||||
sl, tp = pips, pips * (risk_to_reward or self.ram.risk_to_reward)
|
sl, tp = pips, pips * (risk_to_reward or self.ram.risk_to_reward)
|
||||||
price = self.order.price
|
price = self.order.price
|
||||||
if self.order.type == OrderType.BUY:
|
if self.order.type == OrderType.BUY:
|
||||||
self.order.sl, self.order.tp = round(price - sl, self.symbol.digits), round(
|
self.order.sl, self.order.tp = round(price - sl, self.symbol.digits), round(price + tp, self.symbol.digits)
|
||||||
price + tp, self.symbol.digits
|
|
||||||
)
|
|
||||||
elif self.order.type == OrderType.SELL:
|
elif self.order.type == OrderType.SELL:
|
||||||
self.order.sl, self.order.tp = round(price + sl, self.symbol.digits), round(
|
self.order.sl, self.order.tp = round(price + sl, self.symbol.digits), round(price - tp, self.symbol.digits)
|
||||||
price - tp, self.symbol.digits
|
|
||||||
)
|
|
||||||
|
|
||||||
def set_trade_stop_levels_points(
|
def set_trade_stop_levels_points(self, *, points: float, risk_to_reward: float = None):
|
||||||
self, *, points: float, risk_to_reward: float = None
|
|
||||||
):
|
|
||||||
"""Set the stop loss and take profit levels of the order based on the points and the risk to reward ratio.
|
"""Set the stop loss and take profit levels of the order based on the points and the risk to reward ratio.
|
||||||
It is assumed that order_type and price are already set before calling this method.
|
It is assumed that order_type and price are already set before calling this method.
|
||||||
|
|
||||||
@@ -83,23 +77,12 @@ class Trader(ABC):
|
|||||||
price, digits = self.order.price, self.symbol.digits
|
price, digits = self.order.price, self.symbol.digits
|
||||||
|
|
||||||
if self.order.type == OrderType.BUY:
|
if self.order.type == OrderType.BUY:
|
||||||
self.order.sl, self.order.tp = round(price - sl, self.symbol.digits), round(
|
self.order.sl, self.order.tp = round(price - sl, self.symbol.digits), round(price + tp, digits)
|
||||||
price + tp, digits
|
|
||||||
)
|
|
||||||
|
|
||||||
elif self.order.type == OrderType.SELL:
|
elif self.order.type == OrderType.SELL:
|
||||||
self.order.sl, self.order.tp = round(price + sl, self.symbol.digits), round(
|
self.order.sl, self.order.tp = round(price + sl, self.symbol.digits), round(price - tp, digits)
|
||||||
price - tp, digits
|
|
||||||
)
|
|
||||||
|
|
||||||
async def create_order_with_stops(
|
async def create_order_with_stops(self, *, order_type: OrderType, sl: float, tp: float, amount_to_risk: float = None):
|
||||||
self,
|
|
||||||
*,
|
|
||||||
order_type: OrderType,
|
|
||||||
sl: float,
|
|
||||||
tp: float,
|
|
||||||
amount_to_risk: float = None,
|
|
||||||
):
|
|
||||||
"""Create an order with stop loss and take profit levels. Use the amount to risk per trade to
|
"""Create an order with stop loss and take profit levels. Use the amount to risk per trade to
|
||||||
calculate the volume.
|
calculate the volume.
|
||||||
|
|
||||||
@@ -115,18 +98,9 @@ class Trader(ABC):
|
|||||||
tick = await self.symbol.info_tick()
|
tick = await self.symbol.info_tick()
|
||||||
price = tick.ask if order_type == OrderType.BUY else tick.bid
|
price = tick.ask if order_type == OrderType.BUY else tick.bid
|
||||||
volume = await self.symbol.compute_volume_sl(amount=amount, price=price, sl=sl)
|
volume = await self.symbol.compute_volume_sl(amount=amount, price=price, sl=sl)
|
||||||
self.order.set_attributes(
|
self.order.set_attributes(sl=sl, tp=tp, volume=volume, price=price, type=order_type)
|
||||||
sl=sl, tp=tp, volume=volume, price=price, type=order_type
|
|
||||||
)
|
|
||||||
|
|
||||||
async def create_order_with_sl(
|
async def create_order_with_sl(self, *, order_type: OrderType, sl: float, amount_to_risk: float = None, risk_to_reward: float = None):
|
||||||
self,
|
|
||||||
*,
|
|
||||||
order_type: OrderType,
|
|
||||||
sl: float,
|
|
||||||
amount_to_risk: float = None,
|
|
||||||
risk_to_reward: float = None,
|
|
||||||
):
|
|
||||||
"""
|
"""
|
||||||
Create an order with a given stop_loss level. Use the amount to risk per trade to calculate the volume.
|
Create an order with a given stop_loss level. Use the amount to risk per trade to calculate the volume.
|
||||||
|
|
||||||
@@ -146,18 +120,9 @@ class Trader(ABC):
|
|||||||
dtp = dsl * (risk_to_reward or self.ram.risk_to_reward)
|
dtp = dsl * (risk_to_reward or self.ram.risk_to_reward)
|
||||||
tp = price + dtp if order_type == OrderType.BUY else price - dtp
|
tp = price + dtp if order_type == OrderType.BUY else price - dtp
|
||||||
volume = await self.symbol.compute_volume_sl(amount=amount, price=price, sl=sl)
|
volume = await self.symbol.compute_volume_sl(amount=amount, price=price, sl=sl)
|
||||||
self.order.set_attributes(
|
self.order.set_attributes(sl=sl, tp=tp, volume=volume, price=price, type=order_type)
|
||||||
sl=sl, tp=tp, volume=volume, price=price, type=order_type
|
|
||||||
)
|
|
||||||
|
|
||||||
async def create_order_with_points(
|
async def create_order_with_points(self, *, order_type: OrderType, points: float, amount_to_risk: float = None, risk_to_reward: float = None):
|
||||||
self,
|
|
||||||
*,
|
|
||||||
order_type: OrderType,
|
|
||||||
points: float,
|
|
||||||
amount_to_risk: float = None,
|
|
||||||
risk_to_reward: float = None,
|
|
||||||
):
|
|
||||||
"""Create an order with specific points to risk. Use the amount to risk per trade to calculate the volume.
|
"""Create an order with specific points to risk. Use the amount to risk per trade to calculate the volume.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -218,9 +183,7 @@ class Trader(ABC):
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
@error_handler
|
@error_handler
|
||||||
async def record_trade(
|
async def record_trade(self, *, result: OrderSendResult, parameters: dict = None, name: str = ""):
|
||||||
self, *, result: OrderSendResult, parameters: dict = None, name: str = ""
|
|
||||||
):
|
|
||||||
"""Record the trade in csv or json.
|
"""Record the trade in csv or json.
|
||||||
Args:
|
Args:
|
||||||
result (OrderSendResult): Result of the order send
|
result (OrderSendResult): Result of the order send
|
||||||
@@ -232,11 +195,7 @@ class Trader(ABC):
|
|||||||
params = {**parameters} or {}
|
params = {**parameters} or {}
|
||||||
profit = await self.order.calc_profit()
|
profit = await self.order.calc_profit()
|
||||||
params["expected_profit"] = profit
|
params["expected_profit"] = profit
|
||||||
date = (
|
date = datetime.now(tz=UTC) if self.config.mode == "live" else datetime.fromtimestamp(self.config.backtest_engine.cursor.time, tz=UTC)
|
||||||
datetime.now(tz=UTC)
|
|
||||||
if self.config.mode == "live"
|
|
||||||
else datetime.fromtimestamp(self.config.backtest_engine.cursor.time, tz=UTC)
|
|
||||||
)
|
|
||||||
params["date"] = date.strftime("%Y-%m-%d %H:%M:%S.%f")
|
params["date"] = date.strftime("%Y-%m-%d %H:%M:%S.%f")
|
||||||
res = Result(result=result, parameters=params, name=name)
|
res = Result(result=result, parameters=params, name=name)
|
||||||
self.config.task_queue.add(item=QueueItem(res.save), must_complete=True)
|
self.config.task_queue.add(item=QueueItem(res.save), must_complete=True)
|
||||||
|
|||||||
@@ -32,11 +32,7 @@ async def close_all_positions():
|
|||||||
positions = await mt.positions_get()
|
positions = await mt.positions_get()
|
||||||
tasks = []
|
tasks = []
|
||||||
for position in positions:
|
for position in positions:
|
||||||
order_type = (
|
order_type = mt.ORDER_TYPE_BUY if position.type == mt.ORDER_TYPE_SELL else mt.ORDER_TYPE_SELL
|
||||||
mt.ORDER_TYPE_BUY
|
|
||||||
if position.type == mt.ORDER_TYPE_SELL
|
|
||||||
else mt.ORDER_TYPE_SELL
|
|
||||||
)
|
|
||||||
req = {
|
req = {
|
||||||
"action": mt.TRADE_ACTION_DEAL,
|
"action": mt.TRADE_ACTION_DEAL,
|
||||||
"symbol": position.symbol,
|
"symbol": position.symbol,
|
||||||
@@ -54,9 +50,7 @@ async def close_all_positions():
|
|||||||
@pytest.fixture(scope="package", autouse=True)
|
@pytest.fixture(scope="package", autouse=True)
|
||||||
async def config(request):
|
async def config(request):
|
||||||
Path("tests/backtest/configs").mkdir(exist_ok=True)
|
Path("tests/backtest/configs").mkdir(exist_ok=True)
|
||||||
with open("aiomql.json", "r") as fh, open(
|
with open("aiomql.json", "r") as fh, open("tests/backtest/configs/test2.json", "w") as fh1, open("tests/backtest/test.json", "w") as fh2:
|
||||||
"tests/backtest/configs/test2.json", "w"
|
|
||||||
) as fh1, open("tests/backtest/test.json", "w") as fh2:
|
|
||||||
data = json.load(fh)
|
data = json.load(fh)
|
||||||
data["mode"] = "backtest"
|
data["mode"] = "backtest"
|
||||||
json.dump(data, fh1, indent=2)
|
json.dump(data, fh1, indent=2)
|
||||||
@@ -77,19 +71,14 @@ async def mt():
|
|||||||
|
|
||||||
@pytest.fixture(scope="package")
|
@pytest.fixture(scope="package")
|
||||||
async def period():
|
async def period():
|
||||||
return {
|
return {"start": datetime(2024, 2, 1, hour=8, tzinfo=UTC), "end": datetime(2024, 2, 7, hour=16, tzinfo=UTC)}
|
||||||
"start": datetime(2024, 2, 1, hour=8, tzinfo=UTC),
|
|
||||||
"end": datetime(2024, 2, 7, hour=16, tzinfo=UTC),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="package")
|
@pytest.fixture(scope="package")
|
||||||
async def backtest_engine(period):
|
async def backtest_engine(period):
|
||||||
start = period["start"]
|
start = period["start"]
|
||||||
end = period["end"]
|
end = period["end"]
|
||||||
return BackTestEngine(
|
return BackTestEngine(start=start, end=end, name="backtest_data", assign_to_config=True, preload=False)
|
||||||
start=start, end=end, name="backtest_data", assign_to_config=True, preload=False
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="function")
|
@pytest.fixture(scope="function")
|
||||||
|
|||||||
@@ -27,9 +27,7 @@ async def make_buy_sell_orders():
|
|||||||
return {"buy": Order(**buy_req), "sell": Order(**sell_req)}
|
return {"buy": Order(**buy_req), "sell": Order(**sell_req)}
|
||||||
|
|
||||||
|
|
||||||
def test_trade_mode(
|
def test_trade_mode(config, backtest_engine, history, positions, order_sell, order_buy, btc_usd):
|
||||||
config, backtest_engine, history, positions, order_sell, order_buy, btc_usd
|
|
||||||
):
|
|
||||||
assert config.mode == "backtest"
|
assert config.mode == "backtest"
|
||||||
assert isinstance(backtest_engine, BackTestEngine)
|
assert isinstance(backtest_engine, BackTestEngine)
|
||||||
assert isinstance(history.mt5, MetaBackTester)
|
assert isinstance(history.mt5, MetaBackTester)
|
||||||
@@ -79,26 +77,16 @@ async def test_history(backtest_engine, history, order_sell, order_buy, position
|
|||||||
async def test_margin(backtest_engine, order_sell, order_buy):
|
async def test_margin(backtest_engine, order_sell, order_buy):
|
||||||
await backtest_engine.setup_account(balance=100)
|
await backtest_engine.setup_account(balance=100)
|
||||||
so_margin = await backtest_engine.order_calc_margin(
|
so_margin = await backtest_engine.order_calc_margin(
|
||||||
action=order_sell.action,
|
action=order_sell.action, volume=order_sell.volume, symbol=order_sell.symbol, price=order_sell.price
|
||||||
volume=order_sell.volume,
|
|
||||||
symbol=order_sell.symbol,
|
|
||||||
price=order_sell.price,
|
|
||||||
)
|
)
|
||||||
bo_margin = await backtest_engine.order_calc_margin(
|
bo_margin = await backtest_engine.order_calc_margin(
|
||||||
action=order_buy.action,
|
action=order_buy.action, volume=order_buy.volume, symbol=order_buy.symbol, price=order_buy.price
|
||||||
volume=order_buy.volume,
|
|
||||||
symbol=order_buy.symbol,
|
|
||||||
price=order_buy.price,
|
|
||||||
)
|
)
|
||||||
total_margin = so_margin + bo_margin
|
total_margin = so_margin + bo_margin
|
||||||
await backtest_engine.order_send(request=order_sell.request)
|
await backtest_engine.order_send(request=order_sell.request)
|
||||||
await backtest_engine.order_send(request=order_buy.request)
|
await backtest_engine.order_send(request=order_buy.request)
|
||||||
# noinspection PyTestUnpassedFixture
|
# noinspection PyTestUnpassedFixture
|
||||||
assert (
|
assert backtest_engine.positions.margin == total_margin == backtest_engine._account.margin
|
||||||
backtest_engine.positions.margin
|
|
||||||
== total_margin
|
|
||||||
== backtest_engine._account.margin
|
|
||||||
)
|
|
||||||
backtest_engine.reset(clear_data=True)
|
backtest_engine.reset(clear_data=True)
|
||||||
|
|
||||||
|
|
||||||
@@ -124,11 +112,7 @@ async def test_account(backtest_engine, positions):
|
|||||||
deal = backtest_engine.deals.history_deals_get(position=bo.order)
|
deal = backtest_engine.deals.history_deals_get(position=bo.order)
|
||||||
bo_profit = deal[-1].profit
|
bo_profit = deal[-1].profit
|
||||||
assert len(all_pos) == 1
|
assert len(all_pos) == 1
|
||||||
assert (
|
assert backtest_engine.positions.margin == backtest_engine._account.margin == backtest_engine.positions.margins[so.order]
|
||||||
backtest_engine.positions.margin
|
|
||||||
== backtest_engine._account.margin
|
|
||||||
== backtest_engine.positions.margins[so.order]
|
|
||||||
)
|
|
||||||
profit = sum([pos.profit for pos in all_pos])
|
profit = sum([pos.profit for pos in all_pos])
|
||||||
n_balance = backtest_engine._account.balance
|
n_balance = backtest_engine._account.balance
|
||||||
n_equity = backtest_engine._account.equity
|
n_equity = backtest_engine._account.equity
|
||||||
@@ -155,9 +139,7 @@ async def test_wrapup(positions, buy_order, sell_order, backtest_engine, config)
|
|||||||
last_equity = backtest_engine._account.equity
|
last_equity = backtest_engine._account.equity
|
||||||
last_profit = backtest_engine._account.profit
|
last_profit = backtest_engine._account.profit
|
||||||
tdata = GetData.load_data(name=config.backtest_dir / f"{backtest_engine.name}.pkl")
|
tdata = GetData.load_data(name=config.backtest_dir / f"{backtest_engine.name}.pkl")
|
||||||
new_bte = BackTestEngine(
|
new_bte = BackTestEngine(data=tdata, restart=False, assign_to_config=False, preload=False)
|
||||||
data=tdata, restart=False, assign_to_config=False, preload=False
|
|
||||||
)
|
|
||||||
assert new_bte._account.balance == last_balance
|
assert new_bte._account.balance == last_balance
|
||||||
assert new_bte._account.equity == last_equity
|
assert new_bte._account.equity == last_equity
|
||||||
assert new_bte._account.profit == last_profit
|
assert new_bte._account.profit == last_profit
|
||||||
|
|||||||
@@ -21,8 +21,4 @@ async def test_deals_manager(backtest_engine, sell_order, buy_order, period, pos
|
|||||||
deals = backtest_engine.deals.history_deals_get(position=bo.order)
|
deals = backtest_engine.deals.history_deals_get(position=bo.order)
|
||||||
assert len(deals) <= 2
|
assert len(deals) <= 2
|
||||||
orders = backtest_engine.deals.get_deals_range(date_from=start, date_to=end)
|
orders = backtest_engine.deals.get_deals_range(date_from=start, date_to=end)
|
||||||
assert (
|
assert len(orders) == backtest_engine.deals.history_deals_total(date_from=start, date_to=end) == len(backtest_engine.deals._data.keys())
|
||||||
len(orders)
|
|
||||||
== backtest_engine.deals.history_deals_total(date_from=start, date_to=end)
|
|
||||||
== len(backtest_engine.deals._data.keys())
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
# noinspection PyTestUnpassedFixture
|
# noinspection PyTestUnpassedFixture
|
||||||
async def test_orders_manager(
|
async def test_orders_manager(backtest_engine, sell_order, buy_order, period, positions):
|
||||||
backtest_engine, sell_order, buy_order, period, positions
|
|
||||||
):
|
|
||||||
backtest_engine.reset(clear_data=True)
|
backtest_engine.reset(clear_data=True)
|
||||||
await backtest_engine.setup_account(balance=100)
|
await backtest_engine.setup_account(balance=100)
|
||||||
backtest_engine.fast_forward(steps=100)
|
backtest_engine.fast_forward(steps=100)
|
||||||
@@ -23,8 +21,4 @@ async def test_orders_manager(
|
|||||||
orders = backtest_engine.orders.history_orders_get(position=bo.order)
|
orders = backtest_engine.orders.history_orders_get(position=bo.order)
|
||||||
assert len(orders) <= 2
|
assert len(orders) <= 2
|
||||||
orders = backtest_engine.orders.get_orders_range(date_from=start, date_to=end)
|
orders = backtest_engine.orders.get_orders_range(date_from=start, date_to=end)
|
||||||
assert (
|
assert len(orders) == backtest_engine.orders.history_orders_total(date_from=start, date_to=end) == len(backtest_engine.orders._data.keys())
|
||||||
len(orders)
|
|
||||||
== backtest_engine.orders.history_orders_total(date_from=start, date_to=end)
|
|
||||||
== len(backtest_engine.orders._data.keys())
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -29,11 +29,7 @@ async def close_all_positions():
|
|||||||
positions = await mt.positions_get()
|
positions = await mt.positions_get()
|
||||||
tasks = []
|
tasks = []
|
||||||
for position in positions:
|
for position in positions:
|
||||||
order_type = (
|
order_type = mt.ORDER_TYPE_BUY if position.type == mt.ORDER_TYPE_SELL else mt.ORDER_TYPE_SELL
|
||||||
mt.ORDER_TYPE_BUY
|
|
||||||
if position.type == mt.ORDER_TYPE_SELL
|
|
||||||
else mt.ORDER_TYPE_SELL
|
|
||||||
)
|
|
||||||
req = {
|
req = {
|
||||||
"action": mt.TRADE_ACTION_DEAL,
|
"action": mt.TRADE_ACTION_DEAL,
|
||||||
"symbol": position.symbol,
|
"symbol": position.symbol,
|
||||||
@@ -51,9 +47,7 @@ async def close_all_positions():
|
|||||||
@pytest.fixture(scope="package", autouse=True)
|
@pytest.fixture(scope="package", autouse=True)
|
||||||
async def config(request):
|
async def config(request):
|
||||||
Path("tests/live/configs").mkdir(exist_ok=True)
|
Path("tests/live/configs").mkdir(exist_ok=True)
|
||||||
with open("aiomql.json", "r") as fh, open(
|
with open("aiomql.json", "r") as fh, open("tests/live/configs/test2.json", "w") as fh1, open("tests/live/test.json", "w") as fh2:
|
||||||
"tests/live/configs/test2.json", "w"
|
|
||||||
) as fh1, open("tests/live/test.json", "w") as fh2:
|
|
||||||
data = json.load(fh)
|
data = json.load(fh)
|
||||||
json.dump(data, fh1, indent=2)
|
json.dump(data, fh1, indent=2)
|
||||||
json.dump(data, fh2, indent=2)
|
json.dump(data, fh2, indent=2)
|
||||||
|
|||||||
@@ -6,10 +6,7 @@ from aiomql.contrib.symbols import ForexSymbol
|
|||||||
|
|
||||||
|
|
||||||
async def test_bot():
|
async def test_bot():
|
||||||
logging.basicConfig(
|
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s")
|
||||||
level=logging.INFO,
|
|
||||||
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
|
||||||
)
|
|
||||||
syms = ["BTCUSD", "SOLUSD", "ETHUSD"]
|
syms = ["BTCUSD", "SOLUSD", "ETHUSD"]
|
||||||
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]
|
||||||
|
|||||||
@@ -6,10 +6,7 @@ from aiomql.contrib.symbols import ForexSymbol
|
|||||||
|
|
||||||
|
|
||||||
def test_bot_sync():
|
def test_bot_sync():
|
||||||
logging.basicConfig(
|
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s")
|
||||||
level=logging.INFO,
|
|
||||||
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
|
||||||
)
|
|
||||||
syms = ["BTCUSD", "SOLUSD", "ETHUSD"]
|
syms = ["BTCUSD", "SOLUSD", "ETHUSD"]
|
||||||
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]
|
||||||
|
|||||||
@@ -36,13 +36,7 @@ class TestRecordsAndResults:
|
|||||||
async def sell(self, mt):
|
async def sell(self, mt):
|
||||||
sym = "BTCUSD"
|
sym = "BTCUSD"
|
||||||
sym_info = await mt.symbol_info(sym)
|
sym_info = await mt.symbol_info(sym)
|
||||||
return {
|
return {"action": mt.TRADE_ACTION_DEAL, "symbol": sym, "volume": sym_info.volume_min, "type": mt.ORDER_TYPE_SELL, "price": sym_info.bid}
|
||||||
"action": mt.TRADE_ACTION_DEAL,
|
|
||||||
"symbol": sym,
|
|
||||||
"volume": sym_info.volume_min,
|
|
||||||
"type": mt.ORDER_TYPE_SELL,
|
|
||||||
"price": sym_info.bid,
|
|
||||||
}
|
|
||||||
|
|
||||||
@pytest.fixture(scope="class", autouse=True)
|
@pytest.fixture(scope="class", autouse=True)
|
||||||
async def setup(self, sell, buy, mt):
|
async def setup(self, sell, buy, mt):
|
||||||
@@ -50,24 +44,11 @@ class TestRecordsAndResults:
|
|||||||
buy_res_2 = await mt.order_send(buy)
|
buy_res_2 = await mt.order_send(buy)
|
||||||
sell_res = await mt.order_send(sell)
|
sell_res = await mt.order_send(sell)
|
||||||
sell_res_2 = await mt.order_send(sell)
|
sell_res_2 = await mt.order_send(sell)
|
||||||
buy_res = Result(
|
buy_res = Result(result=OrderSendResult(**buy_res._asdict()), name="test_result")
|
||||||
result=OrderSendResult(**buy_res._asdict()), name="test_result"
|
sell_res = Result(result=OrderSendResult(**sell_res._asdict()), name="test_result")
|
||||||
)
|
sell_res_2 = Result(result=OrderSendResult(**sell_res_2._asdict()), name="test_result")
|
||||||
sell_res = Result(
|
buy_res_2 = Result(result=OrderSendResult(**buy_res_2._asdict()), name="test_result")
|
||||||
result=OrderSendResult(**sell_res._asdict()), name="test_result"
|
await asyncio.gather(buy_res.save(), sell_res.save(), buy_res_2.save(trade_record_mode="json"), sell_res_2.save(trade_record_mode="json"))
|
||||||
)
|
|
||||||
sell_res_2 = Result(
|
|
||||||
result=OrderSendResult(**sell_res_2._asdict()), name="test_result"
|
|
||||||
)
|
|
||||||
buy_res_2 = Result(
|
|
||||||
result=OrderSendResult(**buy_res_2._asdict()), name="test_result"
|
|
||||||
)
|
|
||||||
await asyncio.gather(
|
|
||||||
buy_res.save(),
|
|
||||||
sell_res.save(),
|
|
||||||
buy_res_2.save(trade_record_mode="json"),
|
|
||||||
sell_res_2.save(trade_record_mode="json"),
|
|
||||||
)
|
|
||||||
await Positions().close_all()
|
await Positions().close_all()
|
||||||
|
|
||||||
def test_records_dir(self):
|
def test_records_dir(self):
|
||||||
@@ -90,9 +71,7 @@ class TestRecordsAndResults:
|
|||||||
|
|
||||||
async def test_json_records(self):
|
async def test_json_records(self):
|
||||||
json_records = self.trade_records.get_json_records()
|
json_records = self.trade_records.get_json_records()
|
||||||
matched_recs = [
|
matched_recs = [record for record in json_records if record.match("test_result.json")]
|
||||||
record for record in json_records if record.match("test_result.json")
|
|
||||||
]
|
|
||||||
assert len(matched_recs) == 1
|
assert len(matched_recs) == 1
|
||||||
record = matched_recs[0]
|
record = matched_recs[0]
|
||||||
record_data = json.load(record.open())
|
record_data = json.load(record.open())
|
||||||
@@ -106,9 +85,7 @@ class TestRecordsAndResults:
|
|||||||
|
|
||||||
async def test_csv_records(self):
|
async def test_csv_records(self):
|
||||||
csv_records = self.trade_records.get_csv_records()
|
csv_records = self.trade_records.get_csv_records()
|
||||||
matched_recs = [
|
matched_recs = [record for record in csv_records if record.match("test_result.csv")]
|
||||||
record for record in csv_records if record.match("test_result.csv")
|
|
||||||
]
|
|
||||||
assert len(matched_recs) == 1
|
assert len(matched_recs) == 1
|
||||||
record = matched_recs[0]
|
record = matched_recs[0]
|
||||||
record_data = DictReader(record.open())
|
record_data = DictReader(record.open())
|
||||||
|
|||||||
@@ -14,40 +14,20 @@ class TestBackTestEngine:
|
|||||||
def setup_class(cls):
|
def setup_class(cls):
|
||||||
cls.start = datetime(2024, 2, 1)
|
cls.start = datetime(2024, 2, 1)
|
||||||
cls.end = datetime(2024, 2, 7)
|
cls.end = datetime(2024, 2, 7)
|
||||||
cls.g_data = GetData(
|
cls.g_data = GetData(start=cls.start, end=cls.end, symbols=["BTCUSD", "SOLUSD"], timeframes=[TimeFrame.H1, TimeFrame.H2], name="test_engine")
|
||||||
start=cls.start,
|
cls.bte = BackTestEngine(start=cls.start, end=cls.end, assign_to_config=True, preload=False)
|
||||||
end=cls.end,
|
|
||||||
symbols=["BTCUSD", "SOLUSD"],
|
|
||||||
timeframes=[TimeFrame.H1, TimeFrame.H2],
|
|
||||||
name="test_engine",
|
|
||||||
)
|
|
||||||
cls.bte = BackTestEngine(
|
|
||||||
start=cls.start, end=cls.end, assign_to_config=True, preload=False
|
|
||||||
)
|
|
||||||
|
|
||||||
@pytest.fixture(scope="class")
|
@pytest.fixture(scope="class")
|
||||||
async def bte2(self):
|
async def bte2(self):
|
||||||
await self.g_data.get_data()
|
await self.g_data.get_data()
|
||||||
bte2 = BackTestEngine(
|
bte2 = BackTestEngine(start=self.start, end=self.end, data=self.g_data.data, use_terminal=False, preload=False)
|
||||||
start=self.start,
|
|
||||||
end=self.end,
|
|
||||||
data=self.g_data.data,
|
|
||||||
use_terminal=False,
|
|
||||||
preload=False,
|
|
||||||
)
|
|
||||||
await bte2.setup_account(balance=100)
|
await bte2.setup_account(balance=100)
|
||||||
return bte2
|
return bte2
|
||||||
|
|
||||||
@pytest.fixture(scope="class")
|
@pytest.fixture(scope="class")
|
||||||
async def sell_order(self):
|
async def sell_order(self):
|
||||||
sym = await self.bte.get_symbol_info(symbol="BTCUSD")
|
sym = await self.bte.get_symbol_info(symbol="BTCUSD")
|
||||||
request = {
|
request = {"type": OrderType.SELL, "symbol": "BTCUSD", "volume": sym.volume_min, "price": sym.bid, "action": TradeAction.DEAL}
|
||||||
"type": OrderType.SELL,
|
|
||||||
"symbol": "BTCUSD",
|
|
||||||
"volume": sym.volume_min,
|
|
||||||
"price": sym.bid,
|
|
||||||
"action": TradeAction.DEAL,
|
|
||||||
}
|
|
||||||
return request
|
return request
|
||||||
|
|
||||||
@pytest.fixture(scope="class")
|
@pytest.fixture(scope="class")
|
||||||
@@ -67,16 +47,11 @@ class TestBackTestEngine:
|
|||||||
}
|
}
|
||||||
return request
|
return request
|
||||||
|
|
||||||
def modify_stops(self, order):
|
def modify_stops(self, order): ...
|
||||||
...
|
|
||||||
|
|
||||||
def test_span_and_range(self):
|
def test_span_and_range(self):
|
||||||
assert self.bte.range == range(
|
assert self.bte.range == range(0, int((self.end - self.start).total_seconds()), self.bte.speed)
|
||||||
0, int((self.end - self.start).total_seconds()), self.bte.speed
|
assert self.bte.span == range(int(self.start.timestamp()), int(self.end.timestamp()), self.bte.speed)
|
||||||
)
|
|
||||||
assert self.bte.span == range(
|
|
||||||
int(self.start.timestamp()), int(self.end.timestamp()), self.bte.speed
|
|
||||||
)
|
|
||||||
assert len(self.bte.span) == len(self.bte.range)
|
assert len(self.bte.span) == len(self.bte.range)
|
||||||
|
|
||||||
def test_cursor(self):
|
def test_cursor(self):
|
||||||
@@ -158,16 +133,12 @@ class TestBackTestEngine:
|
|||||||
|
|
||||||
async def test_get_rates_from(self):
|
async def test_get_rates_from(self):
|
||||||
start = datetime(2024, 2, 3, 12, 43, tzinfo=UTC)
|
start = datetime(2024, 2, 3, 12, 43, tzinfo=UTC)
|
||||||
rates = await self.bte.get_rates_from(
|
rates = await self.bte.get_rates_from(symbol="BTCUSD", timeframe=TimeFrame.H1, date_from=start, count=24)
|
||||||
symbol="BTCUSD", timeframe=TimeFrame.H1, date_from=start, count=24
|
|
||||||
)
|
|
||||||
assert len(rates) == 24
|
assert len(rates) == 24
|
||||||
|
|
||||||
async def test_get_rates_from_2(self, bte2):
|
async def test_get_rates_from_2(self, bte2):
|
||||||
start = datetime(2024, 2, 3, 12, 12, tzinfo=UTC)
|
start = datetime(2024, 2, 3, 12, 12, tzinfo=UTC)
|
||||||
rates = await bte2.get_rates_from(
|
rates = await bte2.get_rates_from(symbol="BTCUSD", timeframe=TimeFrame.H1, date_from=start, count=24)
|
||||||
symbol="BTCUSD", timeframe=TimeFrame.H1, date_from=start, count=24
|
|
||||||
)
|
|
||||||
assert len(rates) == 24
|
assert len(rates) == 24
|
||||||
|
|
||||||
async def test_get_rates_from_pos(self):
|
async def test_get_rates_from_pos(self):
|
||||||
@@ -175,51 +146,37 @@ class TestBackTestEngine:
|
|||||||
self.bte.go_to(time=now)
|
self.bte.go_to(time=now)
|
||||||
tf = TimeFrame.H2
|
tf = TimeFrame.H2
|
||||||
start_pos = 2
|
start_pos = 2
|
||||||
rates = await self.bte.get_rates_from_pos(
|
rates = await self.bte.get_rates_from_pos(symbol="BTCUSD", timeframe=tf, start_pos=start_pos, count=24)
|
||||||
symbol="BTCUSD", timeframe=tf, start_pos=start_pos, count=24
|
|
||||||
)
|
|
||||||
assert len(rates) == 24
|
assert len(rates) == 24
|
||||||
assert int(rates[-1][0]) == round_down(
|
assert int(rates[-1][0]) == round_down(int(now.replace(hour=now.hour - start_pos).timestamp()), tf.seconds)
|
||||||
int(now.replace(hour=now.hour - start_pos).timestamp()), tf.seconds
|
|
||||||
)
|
|
||||||
|
|
||||||
async def test_get_rates_from_pos2(self, bte2):
|
async def test_get_rates_from_pos2(self, bte2):
|
||||||
now = datetime(2024, 2, 4, 12, 15, tzinfo=UTC)
|
now = datetime(2024, 2, 4, 12, 15, tzinfo=UTC)
|
||||||
bte2.go_to(time=now)
|
bte2.go_to(time=now)
|
||||||
tf = TimeFrame.H1
|
tf = TimeFrame.H1
|
||||||
start_pos = 2
|
start_pos = 2
|
||||||
rates = await bte2.get_rates_from_pos(
|
rates = await bte2.get_rates_from_pos(symbol="BTCUSD", timeframe=tf, start_pos=start_pos, count=24)
|
||||||
symbol="BTCUSD", timeframe=tf, start_pos=start_pos, count=24
|
assert int(rates[-1][0]) == round_down(int(now.replace(hour=10).timestamp()), tf.seconds)
|
||||||
)
|
|
||||||
assert int(rates[-1][0]) == round_down(
|
|
||||||
int(now.replace(hour=10).timestamp()), tf.seconds
|
|
||||||
)
|
|
||||||
# assert int(rates[-1][0]) == round_up(int(now.timestamp()), tf.seconds) - start_pos * tf.seconds
|
# assert int(rates[-1][0]) == round_up(int(now.timestamp()), tf.seconds) - start_pos * tf.seconds
|
||||||
assert len(rates) == 24
|
assert len(rates) == 24
|
||||||
|
|
||||||
async def test_get_rates_range(self):
|
async def test_get_rates_range(self):
|
||||||
start = datetime(2024, 2, 3, 12, tzinfo=UTC)
|
start = datetime(2024, 2, 3, 12, tzinfo=UTC)
|
||||||
end = datetime(2024, 2, 4, 18, tzinfo=UTC)
|
end = datetime(2024, 2, 4, 18, tzinfo=UTC)
|
||||||
rates = await self.bte.get_rates_range(
|
rates = await self.bte.get_rates_range(symbol="BTCUSD", timeframe=TimeFrame.H1, date_from=start, date_to=end)
|
||||||
symbol="BTCUSD", timeframe=TimeFrame.H1, date_from=start, date_to=end
|
|
||||||
)
|
|
||||||
assert len(rates) == 31
|
assert len(rates) == 31
|
||||||
assert int(rates[-1][0]) == int(end.timestamp())
|
assert int(rates[-1][0]) == int(end.timestamp())
|
||||||
|
|
||||||
async def test_get_rates_range2(self, bte2):
|
async def test_get_rates_range2(self, bte2):
|
||||||
start = datetime(2024, 2, 3, 12, tzinfo=UTC)
|
start = datetime(2024, 2, 3, 12, tzinfo=UTC)
|
||||||
end = datetime(2024, 2, 4, 18, tzinfo=UTC)
|
end = datetime(2024, 2, 4, 18, tzinfo=UTC)
|
||||||
rates = await bte2.get_rates_range(
|
rates = await bte2.get_rates_range(symbol="BTCUSD", timeframe=TimeFrame.H1, date_from=start, date_to=end)
|
||||||
symbol="BTCUSD", timeframe=TimeFrame.H1, date_from=start, date_to=end
|
|
||||||
)
|
|
||||||
assert len(rates) == 31
|
assert len(rates) == 31
|
||||||
assert int(rates[-1][0]) == int(end.timestamp())
|
assert int(rates[-1][0]) == int(end.timestamp())
|
||||||
|
|
||||||
async def test_get_ticks_from(self):
|
async def test_get_ticks_from(self):
|
||||||
start = datetime(2024, 2, 3, 12, tzinfo=UTC)
|
start = datetime(2024, 2, 3, 12, tzinfo=UTC)
|
||||||
ticks = await self.bte.get_ticks_from(
|
ticks = await self.bte.get_ticks_from(symbol="BTCUSD", date_from=start, count=24)
|
||||||
symbol="BTCUSD", date_from=start, count=24
|
|
||||||
)
|
|
||||||
assert len(ticks) == 24
|
assert len(ticks) == 24
|
||||||
|
|
||||||
async def test_get_ticks_from2(self, bte2):
|
async def test_get_ticks_from2(self, bte2):
|
||||||
@@ -230,23 +187,15 @@ class TestBackTestEngine:
|
|||||||
async def test_get_ticks_range(self):
|
async def test_get_ticks_range(self):
|
||||||
start = datetime(2024, 2, 3, 12, tzinfo=UTC)
|
start = datetime(2024, 2, 3, 12, tzinfo=UTC)
|
||||||
end = datetime(2024, 2, 3, 15, tzinfo=UTC)
|
end = datetime(2024, 2, 3, 15, tzinfo=UTC)
|
||||||
ticks = await self.bte.get_ticks_range(
|
ticks = await self.bte.get_ticks_range(symbol="BTCUSD", date_from=start, date_to=end)
|
||||||
symbol="BTCUSD", date_from=start, date_to=end
|
approx_total = (end - start).total_seconds() // 2 # assuming 2 ticks per second at least
|
||||||
)
|
|
||||||
approx_total = (
|
|
||||||
end - start
|
|
||||||
).total_seconds() // 2 # assuming 2 ticks per second at least
|
|
||||||
assert len(ticks) >= approx_total
|
assert len(ticks) >= approx_total
|
||||||
|
|
||||||
async def test_get_ticks_range2(self, bte2):
|
async def test_get_ticks_range2(self, bte2):
|
||||||
start = datetime(2024, 2, 3, 12, tzinfo=UTC)
|
start = datetime(2024, 2, 3, 12, tzinfo=UTC)
|
||||||
end = datetime(2024, 2, 3, 15, tzinfo=UTC)
|
end = datetime(2024, 2, 3, 15, tzinfo=UTC)
|
||||||
ticks = await bte2.get_ticks_range(
|
ticks = await bte2.get_ticks_range(symbol="BTCUSD", date_from=start, date_to=end)
|
||||||
symbol="BTCUSD", date_from=start, date_to=end
|
approx_total = (end - start).total_seconds() // 2 # assuming 2 ticks per second at least
|
||||||
)
|
|
||||||
approx_total = (
|
|
||||||
end - start
|
|
||||||
).total_seconds() // 2 # assuming 2 ticks per second at least
|
|
||||||
assert len(ticks) >= approx_total
|
assert len(ticks) >= approx_total
|
||||||
|
|
||||||
async def test_price_tick(self, bte2):
|
async def test_price_tick(self, bte2):
|
||||||
@@ -287,22 +236,14 @@ class TestBackTestEngine:
|
|||||||
tp = sym_info.ask + dsl
|
tp = sym_info.ask + dsl
|
||||||
|
|
||||||
profit = await self.bte.order_calc_profit(
|
profit = await self.bte.order_calc_profit(
|
||||||
action=OrderType.BUY,
|
action=OrderType.BUY, symbol=sym, volume=sym_info.volume_min, price_open=sym_info.ask, price_close=tp
|
||||||
symbol=sym,
|
|
||||||
volume=sym_info.volume_min,
|
|
||||||
price_open=sym_info.ask,
|
|
||||||
price_close=tp,
|
|
||||||
)
|
)
|
||||||
assert profit > 0
|
assert profit > 0
|
||||||
sym_info2 = await bte2.get_symbol_info(symbol=sym)
|
sym_info2 = await bte2.get_symbol_info(symbol=sym)
|
||||||
dsl2 = (sym_info2.trade_stops_level + sym_info2.spread) * 2 * sym_info2.point
|
dsl2 = (sym_info2.trade_stops_level + sym_info2.spread) * 2 * sym_info2.point
|
||||||
tp2 = sym_info2.ask + dsl2
|
tp2 = sym_info2.ask + dsl2
|
||||||
profit2 = await bte2.order_calc_profit(
|
profit2 = await bte2.order_calc_profit(
|
||||||
action=OrderType.BUY,
|
action=OrderType.BUY, symbol=sym, volume=sym_info2.volume_min, price_open=sym_info2.ask, price_close=tp2
|
||||||
symbol=sym,
|
|
||||||
volume=sym_info2.volume_min,
|
|
||||||
price_open=sym_info2.ask,
|
|
||||||
price_close=tp2,
|
|
||||||
)
|
)
|
||||||
assert ceil(profit) == ceil(profit2)
|
assert ceil(profit) == ceil(profit2)
|
||||||
|
|
||||||
@@ -314,20 +255,10 @@ class TestBackTestEngine:
|
|||||||
bte2.go_to(time=moment)
|
bte2.go_to(time=moment)
|
||||||
sym = "BTCUSD"
|
sym = "BTCUSD"
|
||||||
sym_info = await self.bte.get_symbol_info(symbol=sym)
|
sym_info = await self.bte.get_symbol_info(symbol=sym)
|
||||||
margin = await self.bte.order_calc_margin(
|
margin = await self.bte.order_calc_margin(action=OrderType.SELL, symbol=sym, volume=sym_info.volume_min, price=sym_info.bid)
|
||||||
action=OrderType.SELL,
|
|
||||||
symbol=sym,
|
|
||||||
volume=sym_info.volume_min,
|
|
||||||
price=sym_info.bid,
|
|
||||||
)
|
|
||||||
assert margin > 0
|
assert margin > 0
|
||||||
sym_info2 = await self.bte.get_symbol_info(symbol=sym)
|
sym_info2 = await self.bte.get_symbol_info(symbol=sym)
|
||||||
margin2 = await bte2.order_calc_margin(
|
margin2 = await bte2.order_calc_margin(action=OrderType.SELL, symbol=sym, volume=sym_info2.volume_min, price=sym_info2.bid)
|
||||||
action=OrderType.SELL,
|
|
||||||
symbol=sym,
|
|
||||||
volume=sym_info2.volume_min,
|
|
||||||
price=sym_info2.bid,
|
|
||||||
)
|
|
||||||
assert margin2 > 0
|
assert margin2 > 0
|
||||||
|
|
||||||
async def test_order_check(self, buy_order, sell_order):
|
async def test_order_check(self, buy_order, sell_order):
|
||||||
|
|||||||
@@ -11,12 +11,8 @@ from aiomql.core.constants import TimeFrame
|
|||||||
class TestCandle:
|
class TestCandle:
|
||||||
@classmethod
|
@classmethod
|
||||||
def setup_class(cls):
|
def setup_class(cls):
|
||||||
cls.bullish_candle = Candle(
|
cls.bullish_candle = Candle(open=1.3421, high=1.3462, low=1.3405, close=1.3452, time=0, Index=0)
|
||||||
open=1.3421, high=1.3462, low=1.3405, close=1.3452, time=0, Index=0
|
cls.bearish_candle = Candle(open=1.3452, high=1.3405, low=1.3462, close=1.3421, time=1, Index=1)
|
||||||
)
|
|
||||||
cls.bearish_candle = Candle(
|
|
||||||
open=1.3452, high=1.3405, low=1.3462, close=1.3421, time=1, Index=1
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_repr(self):
|
def test_repr(self):
|
||||||
repr_str = repr(self.bearish_candle)
|
repr_str = repr(self.bearish_candle)
|
||||||
|
|||||||
@@ -14,13 +14,7 @@ class TestGetData:
|
|||||||
cls.end = datetime(2024, 2, 2, tzinfo=UTC)
|
cls.end = datetime(2024, 2, 2, tzinfo=UTC)
|
||||||
cls.symbols = ["BTCUSD", "ETHUSD"]
|
cls.symbols = ["BTCUSD", "ETHUSD"]
|
||||||
cls.timeframes = [TimeFrame.H1, TimeFrame.H2]
|
cls.timeframes = [TimeFrame.H1, TimeFrame.H2]
|
||||||
cls.g_data = GetData(
|
cls.g_data = GetData(start=cls.start, end=cls.end, symbols=cls.symbols, timeframes=cls.timeframes, name="test_data")
|
||||||
start=cls.start,
|
|
||||||
end=cls.end,
|
|
||||||
symbols=cls.symbols,
|
|
||||||
timeframes=cls.timeframes,
|
|
||||||
name="test_data",
|
|
||||||
)
|
|
||||||
|
|
||||||
@pytest.fixture(scope="class", autouse=True)
|
@pytest.fixture(scope="class", autouse=True)
|
||||||
async def get_data(self):
|
async def get_data(self):
|
||||||
@@ -34,9 +28,7 @@ class TestGetData:
|
|||||||
assert self.g_data.timeframes == set(self.timeframes)
|
assert self.g_data.timeframes == set(self.timeframes)
|
||||||
assert self.g_data.name == "test_data"
|
assert self.g_data.name == "test_data"
|
||||||
assert self.g_data.range == range(int((self.end - self.start).total_seconds()))
|
assert self.g_data.range == range(int((self.end - self.start).total_seconds()))
|
||||||
assert self.g_data.span == range(
|
assert self.g_data.span == range(int(self.start.timestamp()), int(self.end.timestamp()))
|
||||||
int(self.start.timestamp()), int(self.end.timestamp())
|
|
||||||
)
|
|
||||||
|
|
||||||
async def test_get_data(self):
|
async def test_get_data(self):
|
||||||
assert self.g_data.data.fully_loaded is True
|
assert self.g_data.data.fully_loaded is True
|
||||||
|
|||||||
@@ -111,19 +111,13 @@ class TestMetaTrader:
|
|||||||
assert res.shape[0] == 10
|
assert res.shape[0] == 10
|
||||||
|
|
||||||
async def test_copy_ticks_from(self):
|
async def test_copy_ticks_from(self):
|
||||||
res = await self.mt.copy_ticks_from(
|
res = await self.mt.copy_ticks_from(self.symbol, self.start, 10, self.mt.COPY_TICKS_ALL)
|
||||||
self.symbol, self.start, 10, self.mt.COPY_TICKS_ALL
|
|
||||||
)
|
|
||||||
assert res is not None
|
assert res is not None
|
||||||
assert res.shape[0] == 10
|
assert res.shape[0] == 10
|
||||||
|
|
||||||
async def test_copy_ticks_range(self):
|
async def test_copy_ticks_range(self):
|
||||||
res = await self.mt.copy_ticks_range(
|
res = await self.mt.copy_ticks_range(self.symbol, self.start, self.end, self.mt.COPY_TICKS_ALL)
|
||||||
self.symbol, self.start, self.end, self.mt.COPY_TICKS_ALL
|
res2 = self.mt5.copy_ticks_range(self.symbol, self.start, self.end, self.mt5.COPY_TICKS_ALL)
|
||||||
)
|
|
||||||
res2 = self.mt5.copy_ticks_range(
|
|
||||||
self.symbol, self.start, self.end, self.mt5.COPY_TICKS_ALL
|
|
||||||
)
|
|
||||||
assert res is not None
|
assert res is not None
|
||||||
assert res.shape[0] == res2.shape[0]
|
assert res.shape[0] == res2.shape[0]
|
||||||
|
|
||||||
@@ -149,9 +143,7 @@ class TestMetaTrader:
|
|||||||
price_open = buy_order["price"]
|
price_open = buy_order["price"]
|
||||||
price_close = buy_order["tp"]
|
price_close = buy_order["tp"]
|
||||||
type_ = buy_order["type"]
|
type_ = buy_order["type"]
|
||||||
res = await self.mt.order_calc_profit(
|
res = await self.mt.order_calc_profit(type_, self.symbol, volume, price_open, price_close)
|
||||||
type_, self.symbol, volume, price_open, price_close
|
|
||||||
)
|
|
||||||
assert isinstance(res, float)
|
assert isinstance(res, float)
|
||||||
|
|
||||||
async def test_order_check(self, buy_order):
|
async def test_order_check(self, buy_order):
|
||||||
|
|||||||
@@ -36,9 +36,7 @@ class TestResult:
|
|||||||
|
|
||||||
async def test_json(self, order_results):
|
async def test_json(self, order_results):
|
||||||
res1, res2 = order_results
|
res1, res2 = order_results
|
||||||
await asyncio.gather(
|
await asyncio.gather(res1.save(trade_record_mode="json"), res2.save(trade_record_mode="json"))
|
||||||
res1.save(trade_record_mode="json"), res2.save(trade_record_mode="json")
|
|
||||||
)
|
|
||||||
assert res1.config.records_dir.exists()
|
assert res1.config.records_dir.exists()
|
||||||
record = res1.config.records_dir / f"{res1.name}.json"
|
record = res1.config.records_dir / f"{res1.name}.json"
|
||||||
assert record.exists()
|
assert record.exists()
|
||||||
|
|||||||
@@ -15,9 +15,7 @@ class TestSessions:
|
|||||||
def make_session(self):
|
def make_session(self):
|
||||||
end = time(hour=16, minute=59, second=59, microsecond=999_999, tzinfo=UTC)
|
end = time(hour=16, minute=59, second=59, microsecond=999_999, tzinfo=UTC)
|
||||||
london = Session(start=8, end=end, name="London", on_end="close_all")
|
london = Session(start=8, end=end, name="London", on_end="close_all")
|
||||||
start, end = time(hour=0, tzinfo=UTC), time(
|
start, end = time(hour=0, tzinfo=UTC), time(hour=23, minute=59, second=59, tzinfo=UTC)
|
||||||
hour=23, minute=59, second=59, tzinfo=UTC
|
|
||||||
)
|
|
||||||
all_day = Session(start=start, end=end, name="AllDay", on_end="close_all")
|
all_day = Session(start=start, end=end, name="AllDay", on_end="close_all")
|
||||||
end = time(hour=6, minute=59, second=59, microsecond=999_999, tzinfo=UTC)
|
end = time(hour=6, minute=59, second=59, microsecond=999_999, tzinfo=UTC)
|
||||||
over_night = Session(start=18, end=end, name="OverNight", on_end="close_all")
|
over_night = Session(start=18, end=end, name="OverNight", on_end="close_all")
|
||||||
|
|||||||
@@ -35,19 +35,13 @@ class TestSymbol:
|
|||||||
async def test_rates(self, btc):
|
async def test_rates(self, btc):
|
||||||
start = datetime(year=2023, month=10, day=5)
|
start = datetime(year=2023, month=10, day=5)
|
||||||
end = start + timedelta(hours=9)
|
end = start + timedelta(hours=9)
|
||||||
rates_from = await btc.copy_rates_from(
|
rates_from = await btc.copy_rates_from(timeframe=btc.mt5.TIMEFRAME_H1, date_from=start, count=10)
|
||||||
timeframe=btc.mt5.TIMEFRAME_H1, date_from=start, count=10
|
|
||||||
)
|
|
||||||
assert isinstance(rates_from, Candles)
|
assert isinstance(rates_from, Candles)
|
||||||
assert len(rates_from) == 10
|
assert len(rates_from) == 10
|
||||||
rates_from_pos = await btc.copy_rates_from_pos(
|
rates_from_pos = await btc.copy_rates_from_pos(timeframe=btc.mt5.TIMEFRAME_H1, count=10, start_position=0)
|
||||||
timeframe=btc.mt5.TIMEFRAME_H1, count=10, start_position=0
|
|
||||||
)
|
|
||||||
assert isinstance(rates_from_pos, Candles)
|
assert isinstance(rates_from_pos, Candles)
|
||||||
assert len(rates_from_pos) == 10
|
assert len(rates_from_pos) == 10
|
||||||
rates_range = await btc.copy_rates_range(
|
rates_range = await btc.copy_rates_range(timeframe=btc.mt5.TIMEFRAME_H1, date_from=start, date_to=end)
|
||||||
timeframe=btc.mt5.TIMEFRAME_H1, date_from=start, date_to=end
|
|
||||||
)
|
|
||||||
assert isinstance(rates_range, Candles)
|
assert isinstance(rates_range, Candles)
|
||||||
assert len(rates_range) == 10
|
assert len(rates_range) == 10
|
||||||
ticks_from = await btc.copy_ticks_from(date_from=start, count=10)
|
ticks_from = await btc.copy_ticks_from(date_from=start, count=10)
|
||||||
|
|||||||
@@ -27,9 +27,7 @@ class TestTrader:
|
|||||||
assert res.retcode == 10009
|
assert res.retcode == 10009
|
||||||
|
|
||||||
async def test_create_order_with_sl(self):
|
async def test_create_order_with_sl(self):
|
||||||
sl = (
|
sl = (self.trader.symbol.trade_stops_level * 2 + self.trader.symbol.spread) * self.trader.symbol.point
|
||||||
self.trader.symbol.trade_stops_level * 2 + self.trader.symbol.spread
|
|
||||||
) * self.trader.symbol.point
|
|
||||||
tick = await self.trader.symbol.info_tick()
|
tick = await self.trader.symbol.info_tick()
|
||||||
sl = tick.bid + sl
|
sl = tick.bid + sl
|
||||||
await self.trader.create_order_with_sl(order_type=OrderType.SELL, sl=sl)
|
await self.trader.create_order_with_sl(order_type=OrderType.SELL, sl=sl)
|
||||||
@@ -44,9 +42,7 @@ class TestTrader:
|
|||||||
|
|
||||||
async def test_create_order_with_points(self):
|
async def test_create_order_with_points(self):
|
||||||
points = self.trader.symbol.trade_stops_level * 2 + self.trader.symbol.spread
|
points = self.trader.symbol.trade_stops_level * 2 + self.trader.symbol.spread
|
||||||
await self.trader.create_order_with_points(
|
await self.trader.create_order_with_points(order_type=OrderType.BUY, points=points)
|
||||||
order_type=OrderType.BUY, points=points
|
|
||||||
)
|
|
||||||
res = await self.trader.order.send()
|
res = await self.trader.order.send()
|
||||||
profit = floor(await self.trader.order.calc_profit())
|
profit = floor(await self.trader.order.calc_profit())
|
||||||
loss = -floor(abs(await self.trader.order.calc_loss()))
|
loss = -floor(abs(await self.trader.order.calc_loss()))
|
||||||
@@ -57,16 +53,12 @@ class TestTrader:
|
|||||||
assert res.retcode == 10009
|
assert res.retcode == 10009
|
||||||
|
|
||||||
async def test_create_order_with_stops(self):
|
async def test_create_order_with_stops(self):
|
||||||
sl = (
|
sl = (self.trader.symbol.trade_stops_level * 2 + self.trader.symbol.spread) * self.trader.symbol.point
|
||||||
self.trader.symbol.trade_stops_level * 2 + self.trader.symbol.spread
|
|
||||||
) * self.trader.symbol.point
|
|
||||||
tp = sl * self.trader.ram.risk_to_reward
|
tp = sl * self.trader.ram.risk_to_reward
|
||||||
tick = await self.trader.symbol.info_tick()
|
tick = await self.trader.symbol.info_tick()
|
||||||
sl = tick.ask - sl
|
sl = tick.ask - sl
|
||||||
tp = tick.ask + tp
|
tp = tick.ask + tp
|
||||||
await self.trader.create_order_with_stops(
|
await self.trader.create_order_with_stops(order_type=OrderType.BUY, sl=sl, tp=tp)
|
||||||
order_type=OrderType.BUY, sl=sl, tp=tp
|
|
||||||
)
|
|
||||||
res = await self.trader.order.send()
|
res = await self.trader.order.send()
|
||||||
profit = floor(await self.trader.order.calc_profit())
|
profit = floor(await self.trader.order.calc_profit())
|
||||||
loss = -floor(abs(await self.trader.order.calc_loss()))
|
loss = -floor(abs(await self.trader.order.calc_loss()))
|
||||||
|
|||||||
Reference in New Issue
Block a user