This commit is contained in:
Ichinga Samuel
2024-11-11 05:56:08 +01:00
parent 110c7c39c7
commit 72e902c4c5
58 changed files with 569 additions and 2079 deletions
+1
View File
@@ -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
-20
View File
@@ -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())
+71
View File
@@ -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
+6 -5
View File
@@ -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 |
-2
View File
@@ -1,4 +1,2 @@
line-length = 150 line-length = 150
target-version = "py311" target-version = "py311"
[format]
skip-magic-trailing-comma=true
+3 -11
View File
@@ -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
View File
@@ -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
View File
@@ -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):
+2 -12
View File
@@ -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]
+11 -41
View File
@@ -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__}"
)
+15 -60
View File
@@ -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")
+2 -6
View File
@@ -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)
+2 -6
View File
@@ -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}"
)
+2 -6
View File
@@ -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}"
)
+3 -15
View File
@@ -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
+12 -54
View File
@@ -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()
+14 -56
View File
@@ -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
View File
@@ -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)
+4 -21
View File
@@ -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)
+35 -174
View File
@@ -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
View File
@@ -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
+1 -4
View File
@@ -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
+8 -26
View File
@@ -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()
+1
View File
@@ -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
+11 -40
View File
@@ -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
View File
@@ -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
View File
@@ -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())
+9 -34
View File
@@ -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
View File
@@ -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
View File
@@ -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__
}
+5 -22
View File
@@ -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
View File
@@ -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
+3 -9
View File
@@ -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
View File
@@ -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()
+4 -9
View File
@@ -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
View File
@@ -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:
+2 -3
View File
@@ -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
View File
@@ -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):
+4 -18
View File
@@ -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
View File
@@ -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)
+4 -15
View File
@@ -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")
+6 -24
View File
@@ -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
+1 -5
View File
@@ -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())
)
+2 -8
View File
@@ -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())
)
+2 -8
View File
@@ -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)
+1 -4
View File
@@ -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]
+1 -4
View File
@@ -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]
+8 -31
View File
@@ -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())
+24 -93
View File
@@ -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):
+2 -6
View File
@@ -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)
+2 -10
View File
@@ -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
+4 -12
View File
@@ -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):
+1 -3
View File
@@ -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()
+1 -3
View File
@@ -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")
+3 -9
View File
@@ -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)
+4 -12
View File
@@ -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()))