From 72e902c4c52e481b3f79f1131c1916dbbae80de0 Mon Sep 17 00:00:00 2001 From: Ichinga Samuel Date: Mon, 11 Nov 2024 05:56:08 +0100 Subject: [PATCH] v4 --- .gitignore | 1 + check.py | 20 - docs/contrib/symbols/forex_symbol.md | 71 ++ docs/core/meta_trader.md | 11 +- ruff.toml | 2 - sample_backtest.py | 14 +- sample_bot.py | 5 +- src/aiomql/_utils.py | 33 +- .../contrib/candle_patterns/fractals.py | 14 +- src/aiomql/contrib/strategies/chaos.py | 52 +- src/aiomql/contrib/strategies/finger_trap.py | 75 +- src/aiomql/contrib/symbols/forex_symbol.py | 8 +- src/aiomql/contrib/traders/scalp_trader.py | 8 +- src/aiomql/contrib/traders/simple_trader.py | 8 +- src/aiomql/core/_core.py | 18 +- .../core/backtesting/backtest_controller.py | 16 +- .../core/backtesting/backtest_engine.py | 651 ++++-------------- src/aiomql/core/backtesting/get_data.py | 66 +- src/aiomql/core/backtesting/trades_manager.py | 70 +- src/aiomql/core/base.py | 52 +- src/aiomql/core/config.py | 25 +- src/aiomql/core/meta_backtester.py | 209 +----- src/aiomql/core/meta_trader.py | 304 ++------ src/aiomql/core/models.py | 5 +- src/aiomql/core/task_queue.py | 34 +- src/aiomql/lib/account.py | 1 + src/aiomql/lib/backtester.py | 51 +- src/aiomql/lib/bot.py | 37 +- src/aiomql/lib/candle.py | 38 +- src/aiomql/lib/executor.py | 43 +- src/aiomql/lib/history.py | 61 +- src/aiomql/lib/order.py | 41 +- src/aiomql/lib/positions.py | 27 +- src/aiomql/lib/ram.py | 1 + src/aiomql/lib/result.py | 12 +- src/aiomql/lib/sessions.py | 57 +- src/aiomql/lib/strategy.py | 13 +- src/aiomql/lib/symbol.py | 60 +- src/aiomql/lib/terminal.py | 5 +- src/aiomql/lib/ticks.py | 29 +- src/aiomql/lib/trade_records.py | 22 +- src/aiomql/lib/trader.py | 65 +- tests/backtest/conftest.py | 19 +- .../backtest/integration/test_backtesting.py | 30 +- tests/backtest/unit/test_deals_manager.py | 6 +- tests/backtest/unit/test_order_manager.py | 10 +- tests/live/conftest.py | 10 +- tests/live/integration/test_bot.py | 5 +- tests/live/integration/test_bot_sync.py | 5 +- .../live/integration/test_results_records.py | 39 +- tests/live/unit/test_backtest_engine.py | 117 +--- tests/live/unit/test_candles.py | 8 +- tests/live/unit/test_get_data.py | 12 +- tests/live/unit/test_meta_trader.py | 16 +- tests/live/unit/test_result.py | 4 +- tests/live/unit/test_sessions.py | 4 +- tests/live/unit/test_symbol.py | 12 +- tests/live/unit/test_trader.py | 16 +- 58 files changed, 569 insertions(+), 2079 deletions(-) delete mode 100644 check.py create mode 100644 docs/contrib/symbols/forex_symbol.md diff --git a/.gitignore b/.gitignore index 0f193b4..2674615 100644 --- a/.gitignore +++ b/.gitignore @@ -43,6 +43,7 @@ htmlcov/ .coverage .coverage.* .cache +.ruff_cache .pytest_cache nosetests.xml coverage.xml diff --git a/check.py b/check.py deleted file mode 100644 index cab2017..0000000 --- a/check.py +++ /dev/null @@ -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()) diff --git a/docs/contrib/symbols/forex_symbol.md b/docs/contrib/symbols/forex_symbol.md new file mode 100644 index 0000000..dee71a4 --- /dev/null +++ b/docs/contrib/symbols/forex_symbol.md @@ -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) + + + +# aiomql.contrib.symbols.forex\_symbol + + + +## 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. + + + +#### pip + +```python +@property +def pip() +``` + +Returns the pip value of the symbol. This is ten times the point value for forex symbols. + +**Returns**: + +- `float` - The pip value of the symbol. + + + +#### compute\_points + +```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 + + + +#### 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 diff --git a/docs/core/meta_trader.md b/docs/core/meta_trader.md index 12eef11..912a083 100644 --- a/docs/core/meta_trader.md +++ b/docs/core/meta_trader.md @@ -38,14 +38,15 @@ The MetaTrader Class provides an asynchronous wrapper around the MetaTrader5 API - [history\_deals\_total](#history_deals_total) - [history\_deals\_get](#history_deals_get) - + ### MetaTrader ```python -class MetaTrader(metaclass=BaseMeta) +class MetaTrader(MetaCore) ``` The MetaTrader class is a wrapper around the MetaTrader terminal. It provides methods for connecting to the MetaTrader terminal and retrieving data from it. -#### Attributes + +#### Attributes: | Name | Type | Description | Default | |-------|-------|--------------------------------------------------------|------------------------| | 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. Initializes the connection to the MetaTrader terminal. -#### Returns +#### Returns: | Type | Description | |--------------|-------------------------------------| | `MetaTrader` | An instance of the MetaTrader class | @@ -621,4 +622,4 @@ Call without parameters. Return closed deals on all symbols #### Returns | Type | Description | |--------------------|----------------------------------------------------| -| `tuple[TradeDeal]` | A tuple of closed trade deals as TradeDeal objects | \ No newline at end of file +| `tuple[TradeDeal]` | A tuple of closed trade deals as TradeDeal objects | diff --git a/ruff.toml b/ruff.toml index f60458b..a4248c3 100644 --- a/ruff.toml +++ b/ruff.toml @@ -1,4 +1,2 @@ line-length = 150 target-version = "py311" -[format] -skip-magic-trailing-comma=true diff --git a/sample_backtest.py b/sample_backtest.py index b11fff3..9479cda 100644 --- a/sample_backtest.py +++ b/sample_backtest.py @@ -12,16 +12,8 @@ from aiomql.core.backtesting import BackTestEngine async def back_tester(): config = Config() config.mode = "backtest" - logging.basicConfig( - level=logging.INFO, - format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", - ) - syms = [ - "Volatility 75 Index", - "Volatility 100 Index", - "Volatility 25 Index", - "Volatility 10 Index", - ] + logging.basicConfig(level=logging.INFO, 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] strategies = [Chaos(symbol=symbol) for symbol in symbols] start = datetime(2024, 5, 1, tzinfo=UTC) @@ -35,7 +27,7 @@ async def back_tester(): close_open_positions_on_exit=True, assign_to_config=True, preload=True, - account_info={'balance': 350} + account_info={"balance": 350}, ) backtester = BackTester(backtest_engine=back_test_engine) backtester.add_strategies(strategies=strategies) diff --git a/sample_bot.py b/sample_bot.py index e821f98..4deda7c 100644 --- a/sample_bot.py +++ b/sample_bot.py @@ -6,10 +6,7 @@ from aiomql.contrib.symbols import ForexSymbol def chaos_bot(): - logging.basicConfig( - level=logging.INFO, - format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", - ) + logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s") syms = ["Volatility 75 Index", "Volatility 100 Index", "Volatility 50 Index"] symbols = [ForexSymbol(name=sym) for sym in syms] strategies = [Chaos(symbol=symbol) for symbol in symbols] diff --git a/src/aiomql/_utils.py b/src/aiomql/_utils.py index 51745d2..23f1763 100644 --- a/src/aiomql/_utils.py +++ b/src/aiomql/_utils.py @@ -1,4 +1,5 @@ """Utility functions for aiomql.""" + import decimal import random 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()) -def backoff_decorator( - func=None, *, max_retries: int = 2, retries: int = 0, error="" -) -> callable: +def backoff_decorator(func=None, *, max_retries: int = 2, retries: int = 0, error="") -> callable: if func is None: - return partial( - backoff_decorator, max_retries=max_retries, retries=retries, error=error - ) + return partial(backoff_decorator, max_retries=max_retries, retries=retries, error=error) @wraps(func) async def wrapper(*args, **kwargs): @@ -57,17 +54,9 @@ def backoff_decorator( return wrapper -def error_handler( - func=None, *, msg="", exe=Exception, response=None, log_error_msg=True -): +def error_handler(func=None, *, msg="", exe=Exception, response=None, log_error_msg=True): if func is None: - return partial( - error_handler, - msg=msg, - exe=exe, - response=response, - log_error_msg=log_error_msg, - ) + return partial(error_handler, msg=msg, exe=exe, response=response, log_error_msg=log_error_msg) @wraps(func) async def wrapper(*args, **kwargs): @@ -82,17 +71,9 @@ def error_handler( return wrapper -def error_handler_sync( - func=None, *, msg="", exe=Exception, response=None, log_error_msg=True -): +def error_handler_sync(func=None, *, msg="", exe=Exception, response=None, log_error_msg=True): if func is None: - return partial( - error_handler, - msg=msg, - exe=exe, - response=response, - log_error_msg=log_error_msg, - ) + return partial(error_handler, msg=msg, exe=exe, response=response, log_error_msg=log_error_msg) @wraps(func) def wrapper(*args, **kwargs): diff --git a/src/aiomql/contrib/candle_patterns/fractals.py b/src/aiomql/contrib/candle_patterns/fractals.py index b5e131f..013c3d4 100644 --- a/src/aiomql/contrib/candle_patterns/fractals.py +++ b/src/aiomql/contrib/candle_patterns/fractals.py @@ -3,21 +3,11 @@ from ...lib.candle import Candle, Candles def find_bearish_fractal(candles: Candles) -> Candle | None: for i in range(len(candles) - 3, 1, -1): - if candles[i].high > max( - candles[i - 1].high, - candles[i + 1].high, - candles[i - 2].high, - candles[i + 2].high, - ): + if candles[i].high > max(candles[i - 1].high, candles[i + 1].high, candles[i - 2].high, candles[i + 2].high): return candles[i] def find_bullish_fractal(candles: Candles) -> Candle | None: for i in range(len(candles) - 3, 1, -1): - if candles[i].low < min( - candles[i - 1].low, - candles[i + 1].low, - candles[i - 2].low, - candles[i + 2].low, - ): + if candles[i].low < min(candles[i - 1].low, candles[i + 1].low, candles[i - 2].low, candles[i + 2].low): return candles[i] diff --git a/src/aiomql/contrib/strategies/chaos.py b/src/aiomql/contrib/strategies/chaos.py index 2f8685a..f19c1c9 100644 --- a/src/aiomql/contrib/strategies/chaos.py +++ b/src/aiomql/contrib/strategies/chaos.py @@ -20,70 +20,40 @@ class Chaos(Strategy): fast_ema: int slow_ema: int tracker: Tracker - parameters = { - "fast_ema": 8, - "slow_ema": 20, - "ltf": TimeFrame.M1, - "htf": TimeFrame.M2, - "lcc": 100, - "hcc": 100, - } + parameters = {"fast_ema": 8, "slow_ema": 20, "ltf": TimeFrame.M1, "htf": TimeFrame.M2, "lcc": 100, "hcc": 100} - def __init__( - self, *, symbol: ForexSymbol, params: dict = None, sessions=None, name="Chaos" - ): + def __init__(self, *, symbol: ForexSymbol, params: dict = None, sessions=None, name="Chaos"): super().__init__(symbol=symbol, params=params, sessions=sessions, name=name) self.tracker = Tracker(snooze=self.ltf.seconds) self.trader = ScalpTrader(symbol=self.symbol) async def check_trend(self): try: - candles = await self.symbol.copy_rates_from_pos( - 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 - ): + candles = await self.symbol.copy_rates_from_pos(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: self.tracker.update(new=False, order_type=None, snooze=5) return - self.tracker.update( - new=True, trend_time=current.time, last_trend_price=current.close - ) + self.tracker.update(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.fast_ema, append=True, fillna=0) - candles.rename( - inplace=True, - **{f"EMA_{self.fast_ema}": "fast", f"EMA_{self.slow_ema}": "slow"}, - ) + candles.rename(inplace=True, **{f"EMA_{self.fast_ema}": "fast", f"EMA_{self.slow_ema}": "slow"}) order_type = random.choice([OrderType.BUY, OrderType.SELL]) if order_type == OrderType.BUY: - self.tracker.update( - trend="bullish", snooze=self.htf.seconds, order_type=OrderType.BUY - ) + self.tracker.update(trend="bullish", snooze=self.htf.seconds, order_type=OrderType.BUY) else: - self.tracker.update( - trend="bearish", snooze=self.htf.seconds, order_type=OrderType.SELL - ) + self.tracker.update(trend="bearish", snooze=self.htf.seconds, order_type=OrderType.SELL) except Exception as err: logger.error(f"{err}. Failed to check trend") - self.tracker.update( - trend="ranging", snooze=self.ltf.seconds, order_type=None - ) + self.tracker.update(trend="ranging", snooze=self.ltf.seconds, order_type=None) async def trade(self): try: await self.check_trend() if self.tracker.order_type is not None: - await self.trader.place_trade( - order_type=self.tracker.order_type, parameters=self.parameters - ) + await self.trader.place_trade(order_type=self.tracker.order_type, parameters=self.parameters) self.tracker.update(order_type=None) await self.sleep(secs=self.tracker.snooze) else: await self.sleep(secs=self.tracker.snooze) except Exception as err: - logger.error( - f"{err}. Failed to trade {self.symbol.name} with {self.__class__.__name__}" - ) + logger.error(f"{err}. Failed to trade {self.symbol.name} with {self.__class__.__name__}") diff --git a/src/aiomql/contrib/strategies/finger_trap.py b/src/aiomql/contrib/strategies/finger_trap.py index 63fe1ed..29c4036 100644 --- a/src/aiomql/contrib/strategies/finger_trap.py +++ b/src/aiomql/contrib/strategies/finger_trap.py @@ -24,46 +24,23 @@ class FingerTrap(Strategy): tcc: int trader: Trader tracker: Tracker - parameters = { - "fast_ema": 8, - "slow_ema": 20, - "etf": TimeFrame.M5, - "ttf": TimeFrame.H1, - "entry_ema": 5, - "tcc": 672, - "ecc": 3360, - } + parameters = {"fast_ema": 8, "slow_ema": 20, "etf": TimeFrame.M5, "ttf": TimeFrame.H1, "entry_ema": 5, "tcc": 672, "ecc": 3360} - def __init__( - self, - *, - symbol: Symbol, - params: dict | None = None, - trader: Trader = None, - sessions: Sessions = None, - name: str = "FingerTrap", - ): + def __init__(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) self.trader = trader or SimpleTrader(symbol=self.symbol) self.tracker: Tracker = Tracker(snooze=self.ttf.seconds) async def check_trend(self): try: - candles: Candles = await self.symbol.copy_rates_from_pos( - timeframe=self.ttf, count=self.tcc - ) + candles: Candles = await self.symbol.copy_rates_from_pos(timeframe=self.ttf, count=self.tcc) if (current := candles[-1]) and current.time < self.tracker.trend_time: self.tracker.update(new=False, order_type=None) return - self.tracker.update( - new=True, trend_time=current.time, last_trend_price=current.close - ) + self.tracker.update(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.fast_ema, append=True, fillna=0) - candles.rename( - inplace=True, - **{f"EMA_{self.fast_ema}": "fast", f"EMA_{self.slow_ema}": "slow"}, - ) + candles.rename(inplace=True, **{f"EMA_{self.fast_ema}": "fast", f"EMA_{self.slow_ema}": "slow"}) fas = candles.ta_lib.above(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(): self.tracker.update(trend="bearish") else: - self.tracker.update( - trend="ranging", snooze=self.ttf.seconds, order_type=None - ) + self.tracker.update(trend="ranging", snooze=self.ttf.seconds, order_type=None) self.tracker.update(trend="bullish") # remove this line except Exception as err: - logger.error( - f"{err} for {self.symbol} in {self.__class__.__name__}.check_trend" - ) + logger.error(f"{err} for {self.symbol} in {self.__class__.__name__}.check_trend") self.tracker.update(snooze=self.ttf.seconds, order_type=None) async def confirm_trend(self): try: - candles = await self.symbol.copy_rates_from_pos( - timeframe=self.etf, count=self.ecc - ) + candles = await self.symbol.copy_rates_from_pos(timeframe=self.etf, count=self.ecc) if (current := candles[-1]) and current.time < self.tracker.entry_time: self.tracker.update(new=False, order_type=None) return - self.tracker.update( - new=True, trend_time=current.time, last_entry_price=current.close - ) + self.tracker.update(new=True, trend_time=current.time, last_entry_price=current.close) candles.ta.ema(length=self.entry_ema, append=True) candles.rename(**{f"EMA_{self.entry_ema}": "ema"}) candles["cae"] = candles.ta_lib.cross(candles.close, candles.ema) - candles["cbe"] = candles.ta_lib.cross( - candles.close, candles.ema, above=False - ) + candles["cbe"] = candles.ta_lib.cross(candles.close, candles.ema, above=False) current = candles[-1] - if ( - self.tracker.bullish and True or current.cae - ): # change True to current.cae + if self.tracker.bullish and True or current.cae: # change True to current.cae sl = find_bullish_fractal(candles).low - self.tracker.update( - snooze=self.ttf.seconds, order_type=OrderType.BUY, sl=sl - ) + self.tracker.update(snooze=self.ttf.seconds, order_type=OrderType.BUY, sl=sl) elif self.tracker.bearish and current.cbe: sl = find_bearish_fractal(candles).high - self.tracker.update( - snooze=self.ttf.seconds, order_type=OrderType.SELL, sl=sl - ) + self.tracker.update(snooze=self.ttf.seconds, order_type=OrderType.SELL, sl=sl) else: self.tracker.update(snooze=self.etf.seconds, order_type=None) except Exception as err: - logger.error( - f"{err} for {self.symbol} in {self.__class__.__name__}.confirm_trend" - ) + logger.error(f"{err} for {self.symbol} in {self.__class__.__name__}.confirm_trend") self.tracker.update(snooze=self.etf.seconds, order_type=None) async def watch_market(self): @@ -138,11 +97,7 @@ class FingerTrap(Strategy): if self.tracker.order_type is None: await self.sleep(secs=self.tracker.snooze) return - await self.trader.place_trade( - order_type=self.tracker.order_type, - parameters=self.parameters, - sl=self.tracker.sl, - ) + await self.trader.place_trade(order_type=self.tracker.order_type, parameters=self.parameters, sl=self.tracker.sl) await self.sleep(secs=self.tracker.snooze) except Exception as err: logger.error(f"{err} For {self.symbol} in {self.__class__.__name__}.trade") diff --git a/src/aiomql/contrib/symbols/forex_symbol.py b/src/aiomql/contrib/symbols/forex_symbol.py index b63abfc..05a2c5d 100644 --- a/src/aiomql/contrib/symbols/forex_symbol.py +++ b/src/aiomql/contrib/symbols/forex_symbol.py @@ -24,9 +24,7 @@ class ForexSymbol(Symbol): points = amount / (volume * self.point * self.trade_contract_size) return points - async def compute_volume_points( - self, *, amount: float, points: float, round_down: bool = False - ) -> float: + async def compute_volume_points(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. Args: @@ -37,8 +35,6 @@ class ForexSymbol(Symbol): volume = amount / (self.point * points * self.trade_contract_size) return self.round_off_volume(volume=volume, round_down=round_down) - async def compute_volume_sl( - self, *, amount: float, price: float, sl: float, round_down: bool = False - ) -> float: + async def compute_volume_sl(self, *, amount: float, price: float, sl: float, round_down: bool = False) -> float: volume = amount / (abs(price - sl) * self.trade_contract_size) return self.round_off_volume(volume=volume, round_down=round_down) diff --git a/src/aiomql/contrib/traders/scalp_trader.py b/src/aiomql/contrib/traders/scalp_trader.py index 3325136..d3a55c1 100644 --- a/src/aiomql/contrib/traders/scalp_trader.py +++ b/src/aiomql/contrib/traders/scalp_trader.py @@ -7,9 +7,7 @@ logger = getLogger(__name__) class ScalpTrader(Trader): - async def place_trade( - self, *, order_type: OrderType, volume: float = None, parameters: dict = None - ): + async def place_trade(self, *, order_type: OrderType, volume: float = None, parameters: dict = None): """Places a trade based on the order_type and a given stop_loss Args: @@ -28,6 +26,4 @@ class ScalpTrader(Trader): if res is not None: await self.record_trade(result=res, parameters=self.parameters) except Exception as err: - logger.error( - f"{err} in {self.__class__.__name__}.place_trade for {self.symbol.name}" - ) + logger.error(f"{err} in {self.__class__.__name__}.place_trade for {self.symbol.name}") diff --git a/src/aiomql/contrib/traders/simple_trader.py b/src/aiomql/contrib/traders/simple_trader.py index 846d17a..0fa2030 100644 --- a/src/aiomql/contrib/traders/simple_trader.py +++ b/src/aiomql/contrib/traders/simple_trader.py @@ -7,9 +7,7 @@ logger = getLogger(__name__) class SimpleTrader(Trader): - async def place_trade( - self, *, order_type: OrderType, sl: float, parameters: dict = None - ): + async def place_trade(self, *, order_type: OrderType, sl: float, parameters: dict = None): """Places a trade based on the order_type and a given stop_loss Args: @@ -25,6 +23,4 @@ class SimpleTrader(Trader): self.order.comment = self.parameters.get("name", self.__class__.__name__) await self.send_order() except Exception as err: - logger.error( - f"{err} in {self.__class__.__name__}.place_trade for {self.symbol.name}" - ) + logger.error(f"{err} in {self.__class__.__name__}.place_trade for {self.symbol.name}") diff --git a/src/aiomql/core/_core.py b/src/aiomql/core/_core.py index 43c3149..c5b8c7f 100644 --- a/src/aiomql/core/_core.py +++ b/src/aiomql/core/_core.py @@ -294,21 +294,9 @@ types = ( class BaseMeta(type): def __new__(mcs, cls_name, bases, cls_dict): defaults: dict = getattr(MetaTrader5, "__dict__", {}) - callables = { - f"_{key}": value - for key in core_mt5_functions - 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 - } + callables = {f"_{key}": value for key in core_mt5_functions 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 |= consts cls_dict |= types_ diff --git a/src/aiomql/core/backtesting/backtest_controller.py b/src/aiomql/core/backtesting/backtest_controller.py index 5384834..11f6026 100644 --- a/src/aiomql/core/backtesting/backtest_controller.py +++ b/src/aiomql/core/backtesting/backtest_controller.py @@ -52,24 +52,14 @@ class BackTestController: self.backtest_engine.next() while True: pending = self.wait() - if ( - pending == 0 - ): # all main tasks have been completed in the current cycle + if pending == 0: # all main tasks have been completed in the current cycle await self.backtest_engine.tracker() self.backtest_engine.next() if self.backtest_engine.cursor.time % 3600 == 0: - logger.info( - datetime.strftime( - datetime.fromtimestamp( - self.backtest_engine.cursor.time - ), - "%Y-%m-%d %H:%M:%S", - ) - ) + logger.info(datetime.strftime(datetime.fromtimestamp(self.backtest_engine.cursor.time), "%Y-%m-%d %H:%M:%S")) if self.backtest_engine.stop_testing: logger.info( - "Stop trading called in control at %s", - datetime.fromtimestamp(self.backtest_engine.cursor.time).strftime("%Y-%m-%d %H:%M:%S"), + "Stop trading called in control at %s", datetime.fromtimestamp(self.backtest_engine.cursor.time).strftime("%Y-%m-%d %H:%M:%S") ) break await self.backtest_engine.wrap_up() diff --git a/src/aiomql/core/backtesting/backtest_engine.py b/src/aiomql/core/backtesting/backtest_engine.py index 0b8ce0e..7bbfec3 100644 --- a/src/aiomql/core/backtesting/backtest_engine.py +++ b/src/aiomql/core/backtesting/backtest_engine.py @@ -25,26 +25,9 @@ from MetaTrader5 import ( ) from ..meta_trader import MetaTrader -from ..constants import ( - TimeFrame, - OrderType, - TradeAction, - AccountStopOutMode, - PositionReason, - DealType, - DealReason, - DealEntry, - OrderReason, - CopyTicks, -) +from ..constants import TimeFrame, OrderType, TradeAction, AccountStopOutMode, PositionReason, DealType, DealReason, DealEntry, OrderReason, CopyTicks -from ..._utils import ( - round_down, - round_up, - error_handler, - error_handler_sync, - async_cache, -) +from ..._utils import round_down, round_up, error_handler, error_handler_sync, async_cache from .get_data import BackTestData, GetData, Cursor from .backtest_account import BackTestAccount @@ -76,6 +59,7 @@ class BackTestEngine: preload: bool account_lock: RLock account_info: dict + def __init__( self, *, @@ -90,7 +74,7 @@ class BackTestEngine: close_open_positions_on_exit: bool = True, preload=True, assign_to_config: bool = True, - account_info: dict = None + account_info: dict = None, ): self._data = data or BackTestData() self.mt5 = MetaTrader() @@ -99,30 +83,14 @@ class BackTestEngine: self.config.backtest_engine = self self.setup_test_range(start=start, end=end, speed=speed, restart=restart) self.setup_data(restart=restart) - start, end = ( - (self.span[0], self.span[-1]) - if len(self.span) >= 2 - else ((now := datetime.now(UTC).timestamp()), now) - ) - start, end = datetime.fromtimestamp(start, tz=UTC), datetime.fromtimestamp( - end, tz=UTC - ) - self.name = ( - name or self._data.name or f"backtest_data_{start:%d_%m_%y}_{end:%d_%m_%y}" - ) + start, end = (self.span[0], self.span[-1]) if len(self.span) >= 2 else ((now := datetime.now(UTC).timestamp()), now) + start, end = datetime.fromtimestamp(start, tz=UTC), datetime.fromtimestamp(end, tz=UTC) + self.name = name or self._data.name or f"backtest_data_{start:%d_%m_%y}_{end:%d_%m_%y}" self.stop_testing = False - self.use_terminal = ( - self.config.use_terminal_for_backtesting - if use_terminal is None - else use_terminal - ) + self.use_terminal = self.config.use_terminal_for_backtesting if use_terminal is None else use_terminal self.close_open_positions_on_exit = close_open_positions_on_exit if stop_time is not None: - val = ( - stop_time.astimezone(tz=UTC) - if isinstance(stop_time, datetime) - else datetime.fromtimestamp(stop_time, tz=UTC) - ) + val = stop_time.astimezone(tz=UTC) if isinstance(stop_time, datetime) else datetime.fromtimestamp(stop_time, tz=UTC) stop_time = int(val.timestamp()) self.stop_time = stop_time self.preload = preload @@ -144,27 +112,12 @@ class BackTestEngine: def __repr__(self): return f"{self.__class__.__name__}()" - def setup_test_range( - self, - *, - start: float | datetime = None, - end: float | datetime = None, - speed: int = 60, - restart: bool = True, - ): + def setup_test_range(self, *, start: float | datetime = None, end: float | datetime = None, speed: int = 60, restart: bool = True): if self._data.span and self._data.range: start = start or self._data.span[0] end = end or self._data.span[-1] + speed - start = ( - start.astimezone(tz=UTC) - if isinstance(start, datetime) - else datetime.fromtimestamp(start, tz=UTC) - ) - end = ( - end.astimezone(tz=UTC) - if isinstance(end, datetime) - else datetime.fromtimestamp(end, tz=UTC) - ) + start = start.astimezone(tz=UTC) if isinstance(start, datetime) else datetime.fromtimestamp(start, tz=UTC) + end = end.astimezone(tz=UTC) if isinstance(end, datetime) else datetime.fromtimestamp(end, tz=UTC) span_start = int(start.timestamp()) span_end = int(end.timestamp()) self.speed = speed @@ -192,21 +145,13 @@ class BackTestEngine: orders = {} for ticket, order in self._data.orders.items(): - orders[ticket] = TradeOrder( - (order.get(k) for k in TradeOrder.__match_args__) - ) + orders[ticket] = TradeOrder((order.get(k) for k in TradeOrder.__match_args__)) self.orders = OrdersManager(data=orders) positions = {} for ticket, position in self._data.positions.items(): - positions[ticket] = TradePosition( - (position.get(k) for k in TradePosition.__match_args__) - ) - self.positions = PositionsManager( - data=positions, - open_positions=self._data.open_positions, - margins=self._data.margins, - ) + positions[ticket] = TradePosition((position.get(k) for k in TradePosition.__match_args__)) + self.positions = PositionsManager(data=positions, open_positions=self._data.open_positions, margins=self._data.margins) deals = {} for ticket, deal in self._data.deals.items(): @@ -229,11 +174,7 @@ class BackTestEngine: self.setup_data(restart=True) def go_to(self, *, time: datetime | float): - time = ( - time.astimezone(tz=UTC) - if isinstance(time, datetime) - else datetime.fromtimestamp(time, tz=UTC) - ) + time = time.astimezone(tz=UTC) if isinstance(time, datetime) else datetime.fromtimestamp(time, tz=UTC) time = int(time.timestamp()) steps = time - self.cursor.time steps = steps // self.speed @@ -253,10 +194,7 @@ class BackTestEngine: async def tracker(self): try: - pos_tasks = [ - self.check_position(ticket=ticket) - for ticket in self.positions._open_positions - ] + pos_tasks = [self.check_position(ticket=ticket) for ticket in self.positions._open_positions] await asyncio.gather(*pos_tasks) profit = sum(pos.profit for pos in self.positions.open_positions) self.update_account(profit=profit) @@ -266,41 +204,16 @@ class BackTestEngine: @error_handler_sync def save_result_to_json(self): - data = self._account.get_dict( - include={ - "balance", - "profit", - "equity", - "margin", - "margin_free", - "margin_level", - } - ) - wins = [ - position - for ticket in self.positions - if (position := self.positions.get(ticket)).profit > 0 - ] - losses = [ - position - for ticket in self.positions - if (position := self.positions.get(ticket)).profit <= 0 - ] - win = round( - sum(position.profit for position in wins), self._account.currency_digits - ) - loss = round( - sum(position.profit for position in losses), self._account.currency_digits - ) + data = self._account.get_dict(include={"balance", "profit", "equity", "margin", "margin_free", "margin_level"}) + wins = [position for ticket in self.positions if (position := self.positions.get(ticket)).profit > 0] + losses = [position for ticket in self.positions if (position := self.positions.get(ticket)).profit <= 0] + win = round(sum(position.profit for position in wins), self._account.currency_digits) + loss = round(sum(position.profit for position in losses), self._account.currency_digits) profit_factor = round(abs(win / loss), 2) if loss != 0 else 0 wins, losses, total = len(wins), len(losses), len(self.positions._data) win_percentage = round(wins / total * 100, 2) if total > 0 else 0 net_profit = round(win - abs(loss), self._account.currency_digits) - profitability = ( - net_profit / (self._account.balance - net_profit) * 100 - if net_profit != 0 - else 0 - ) + profitability = net_profit / (self._account.balance - net_profit) * 100 if net_profit != 0 else 0 profitability = round(profitability, 2) data.update( { @@ -320,10 +233,7 @@ class BackTestEngine: json.dump(data, file, indent=4) async def close_all_open(self): - tasks = [ - self.check_position(ticket=position.ticket) - for position in self.positions.open_positions - ] + tasks = [self.check_position(ticket=position.ticket) for position in self.positions.open_positions] await asyncio.gather(*tasks) for position in self.positions.open_positions: await self.close_position_manually(ticket=position.ticket) @@ -370,9 +280,7 @@ class BackTestEngine: async def get_price_tick(self, *, symbol: str, time: int) -> Tick | None: try: if self.use_terminal and self.preload: - if ( - ticks := self.preloaded_ticks.get(symbol) - ) is not None and time in ticks.index: + if (ticks := self.preloaded_ticks.get(symbol)) is not None and time in ticks.index: return Tick(ticks.loc[time]) await self.preload_ticks(symbol=symbol) tick = self.preloaded_ticks[symbol].loc[time] @@ -429,9 +337,7 @@ class BackTestEngine: if res: pos = self.positions.get(ticket) deal.update({"profit": pos.profit, "volume": pos.volume}) - self.deals[deal["ticket"]] = TradeDeal( - (deal.get(k, 0) for k in TradeDeal.__match_args__) - ) + self.deals[deal["ticket"]] = TradeDeal((deal.get(k, 0) for k in TradeDeal.__match_args__)) case OrderType.SELL: if tick.ask <= tp or tick.ask >= sl: @@ -439,23 +345,15 @@ class BackTestEngine: if res: pos = self.positions.get(ticket) deal.update({"profit": pos.profit, "volume": pos.volume}) - self.deals[deal["ticket"]] = TradeDeal( - (deal.get(k, 0) for k in TradeDeal.__match_args__) - ) + self.deals[deal["ticket"]] = TradeDeal((deal.get(k, 0) for k in TradeDeal.__match_args__)) case _: ... def check_account(self): account = self._account - level = ( - account.margin_level - if account.margin_so_mode == AccountStopOutMode.PERCENT - else account.margin_so_call - ) + level = account.margin_level if account.margin_so_mode == AccountStopOutMode.PERCENT else account.margin_so_call if level < account.margin_so_call and level != 0 and account.equity < 0: - logger.critical( - "Account has burned out!!! Please top up to continue trading" - ) + logger.critical("Account has burned out!!! Please top up to continue trading") self.stop_testing = True async def check_position(self, *, ticket: int): @@ -466,22 +364,10 @@ class BackTestEngine: ticket (int): Position ticket """ pos = self.positions[ticket] - order_type, symbol, volume, price_open, prev_profit = ( - pos.type, - pos.symbol, - pos.volume, - pos.price_open, - pos.profit, - ) + order_type, symbol, volume, price_open, prev_profit = (pos.type, pos.symbol, pos.volume, pos.price_open, pos.profit) tick = await self.get_price_tick(symbol=symbol, time=self.cursor.time) price_current = tick.bid if order_type == OrderType.BUY else tick.ask - profit = await self.order_calc_profit( - action=order_type, - symbol=symbol, - volume=volume, - price_open=price_open, - price_close=price_current, - ) + profit = await self.order_calc_profit(action=order_type, symbol=symbol, volume=volume, price_open=price_open, price_close=price_current) kwargs = dict(price_current=price_current, time_update=self.cursor.time) kwargs.update(profit=profit) if profit is not None else ... self.positions.update(ticket=pos.ticket, **kwargs) @@ -497,9 +383,7 @@ class BackTestEngine: deal_ticket = random.randint(100_000_000, 199_999_999) time = position.time_update time_msc = position.time_update_msc - order_type = ( - OrderType.BUY if position.type == OrderType.SELL else OrderType.SELL - ) + order_type = OrderType.BUY if position.type == OrderType.SELL else OrderType.SELL order = { "position_id": position.ticket, "ticket": order_ticket, @@ -555,14 +439,8 @@ class BackTestEngine: margin = self.positions.get_margin(ticket=ticket) self.positions.delete_margin(ticket=ticket) self.positions.close(ticket=ticket) - self.orders.update( - ticket=ticket, - time_done=self.cursor.time, - time_done_msc=self.cursor.time * 1000, - ) - profit = sum( - [position.profit for position in self.positions.open_positions] - ) + self.orders.update(ticket=ticket, time_done=self.cursor.time, time_done_msc=self.cursor.time * 1000) + profit = sum([position.profit for position in self.positions.open_positions]) profit = round(profit, self._account.currency_digits) self.update_account(gain=position.profit, profit=profit, margin=-margin) return True @@ -583,58 +461,29 @@ class BackTestEngine: Returns: bool: True if the stops are modified successfully, False otherwise """ - self.positions.update( - ticket=ticket, - sl=sl, - tp=tp, - time_update=self.cursor.time, - time_update_msc=self.cursor.time * 1000, - ) + self.positions.update(ticket=ticket, sl=sl, tp=tp, time_update=self.cursor.time, time_update_msc=self.cursor.time * 1000) return True - def update_account( - self, *, profit: float = None, margin: float = 0, gain: float = 0 - ): + def update_account(self, *, profit: float = None, margin: float = 0, gain: float = 0): self.account_lock.acquire() try: self._account.balance += round(gain, self._account.currency_digits) - self._account.profit = ( - round(profit, self._account.currency_digits) - if profit is not None - else self._account.profit - ) + self._account.profit = round(profit, self._account.currency_digits) if profit is not None else self._account.profit self._account.equity = self._account.balance + self._account.profit self._account.margin += round(margin, self._account.currency_digits) - self._account.margin_free = round( - self._account.equity - self._account.margin, - self._account.currency_digits, - ) - self._account.balance = round( - self._account.balance, self._account.currency_digits - ) - self._account.equity = round( - self._account.equity, self._account.currency_digits - ) - self._account.margin = round( - self._account.margin, self._account.currency_digits - ) - self._account.margin_free = round( - self._account.margin_free, self._account.currency_digits - ) - self._account.profit = round( - self._account.profit, self._account.currency_digits - ) + self._account.margin_free = round(self._account.equity - self._account.margin, self._account.currency_digits) + self._account.balance = round(self._account.balance, self._account.currency_digits) + self._account.equity = round(self._account.equity, self._account.currency_digits) + self._account.margin = round(self._account.margin, self._account.currency_digits) + self._account.margin_free = round(self._account.margin_free, self._account.currency_digits) + self._account.profit = round(self._account.profit, self._account.currency_digits) if self._account.margin == 0: self._account.margin_level = 0 else: mode = self._account.margin_so_mode level = self._account.equity / self._account.margin * 100 - self._account.margin_level = ( - level - if mode == AccountStopOutMode.PERCENT - else self._account.margin_free - ) + self._account.margin_level = level if mode == AccountStopOutMode.PERCENT else self._account.margin_free except Exception as exe: logger.critical("Error Updating Account: %s", exe) @@ -696,9 +545,7 @@ class BackTestEngine: res = pd.DataFrame(res) res.drop_duplicates(subset=["time"], keep="last", inplace=True) res.set_index("time", inplace=True, drop=False) - res = res.reindex( - self.span, copy=True, method="nearest" - ) # fill in missing values with NaN + res = res.reindex(self.span, copy=True, method="nearest") # fill in missing values with NaN prices[symbol] = res return prices @@ -725,9 +572,7 @@ class BackTestEngine: def symbols(self) -> dict[str, SymbolInfo]: symbols = {} for symbol, info in self._data.symbols.items(): - symbols[symbol] = SymbolInfo( - (info.get(key) for key in SymbolInfo.__match_args__) - ) + symbols[symbol] = SymbolInfo((info.get(key) for key in SymbolInfo.__match_args__)) return symbols @error_handler @@ -736,52 +581,28 @@ class BackTestEngine: osr = { "retcode": 10013, "comment": "Invalid request", - "request": TradeRequest( - request.get(k, (0 if k != "comment" else "")) - for k in TradeRequest.__match_args__ - ), + "request": TradeRequest(request.get(k, (0 if k != "comment" else "")) for k in TradeRequest.__match_args__), } - current_tick = await self.get_price_tick( - symbol=request.get("symbol"), time=self.cursor.time - ) + current_tick = await self.get_price_tick(symbol=request.get("symbol"), time=self.cursor.time) if current_tick is None: osr["comment"] = "Market is closed" osr["retcode"] = 10018 - return OrderSendResult( - (osr.get(k, 0) for k in OrderSendResult.__match_args__) - ) + return OrderSendResult((osr.get(k, 0) for k in OrderSendResult.__match_args__)) - trade_order = { - "external_id": "", - "comment": "", - **{k: v for k, v in request.items() if k in TradeOrder.__match_args__}, - } + trade_order = {"external_id": "", "comment": "", **{k: v for k, v in request.items() if k in TradeOrder.__match_args__}} order_type, symbol = request.get("type"), request.get("symbol", "") action, position_id = request.get("action"), request.get("position") - sl, tp, volume, symbol = ( - request.get("sl"), - request.get("tp"), - request.get("volume"), - request.get("symbol"), - ) + sl, tp, volume, symbol = (request.get("sl"), request.get("tp"), request.get("volume"), request.get("symbol")) order_type = OrderType(order_type) current_position = self.positions.get(position_id) order_ticket = random.randint(800_000_000, 899_999_999) deal_ticket = random.randint(100_000_000, 199_999_999) # closing an order by an opposite order using a position ticket and Deal action - if ( - action == TradeAction.DEAL - and current_position - and order_type.opposite == current_position.type - ): + if action == TradeAction.DEAL and current_position and order_type.opposite == current_position.type: res = await self.close_position(ticket=current_position.ticket) if res: - price_current = ( - current_tick.ask - if order_type == OrderType.BUY - else current_tick.bid - ) + price_current = current_tick.ask if order_type == OrderType.BUY else current_tick.bid trade_order.update( { "position_id": current_position.ticket, @@ -818,64 +639,30 @@ class BackTestEngine: "external_id": "", } - order = TradeOrder( - (trade_order.get(k, 0) for k in TradeOrder.__match_args__) - ) + order = TradeOrder((trade_order.get(k, 0) for k in TradeOrder.__match_args__)) self.orders[order.ticket] = order deal = TradeDeal((deal.get(k, 0) for k in TradeDeal.__match_args__)) self.deals[deal.ticket] = deal - osr.update( - { - "comment": "Request completed", - "retcode": 10009, - "order": order_ticket, - "deal": deal_ticket, - } - ) - return OrderSendResult( - (osr.get(k, 0) for k in OrderSendResult.__match_args__) - ) + osr.update({"comment": "Request completed", "retcode": 10009, "order": order_ticket, "deal": deal_ticket}) + return OrderSendResult((osr.get(k, 0) for k in OrderSendResult.__match_args__)) if action == TradeAction.SLTP and current_position: check = await self.order_check(request=request, use_terminal=use_terminal) if check.retcode != 0: - osr = { - "retcode": check.retcode, - "comment": check.comment, - "request": check.request, - } - return OrderSendResult( - (osr.get(k, 0) for k in OrderSendResult.__match_args__) - ) + osr = {"retcode": check.retcode, "comment": check.comment, "request": check.request} + return OrderSendResult((osr.get(k, 0) for k in OrderSendResult.__match_args__)) res = self.modify_stops(ticket=position_id, sl=sl, tp=tp) if res: - osr.update( - { - "comment": "Request completed", - "retcode": 10009, - "order": order_ticket, - "deal": deal_ticket, - } - ) - return OrderSendResult( - (osr.get(k, 0) for k in OrderSendResult.__match_args__) - ) + osr.update({"comment": "Request completed", "retcode": 10009, "order": order_ticket, "deal": deal_ticket}) + return OrderSendResult((osr.get(k, 0) for k in OrderSendResult.__match_args__)) if action == TradeAction.DEAL and order_type in (OrderType.BUY, OrderType.SELL): check = await self.order_check(request=request, use_terminal=use_terminal) if check.retcode != 0: - osr = { - "retcode": check.retcode, - "comment": check.comment, - "request": check.request, - } - return OrderSendResult( - (osr.get(k, 0) for k in OrderSendResult.__match_args__) - ) + osr = {"retcode": check.retcode, "comment": check.comment, "request": check.request} + return OrderSendResult((osr.get(k, 0) for k in OrderSendResult.__match_args__)) - price = ( - current_tick.ask if order_type == OrderType.BUY else current_tick.bid - ) + price = current_tick.ask if order_type == OrderType.BUY else current_tick.bid position = { "ticket": order_ticket, "symbol": symbol, @@ -932,12 +719,8 @@ class BackTestEngine: } ) - pos = TradePosition( - (position.get(k, 0) for k in TradePosition.__match_args__) - ) - order = TradeOrder( - (trade_order.get(k, 0) for k in TradeOrder.__match_args__) - ) + pos = TradePosition((position.get(k, 0) for k in TradePosition.__match_args__)) + order = TradeOrder((trade_order.get(k, 0) for k in TradeOrder.__match_args__)) deal = TradeDeal((deal.get(k, 0) for k in TradeDeal.__match_args__)) self.deals[deal_ticket] = deal self.positions[order.ticket] = pos @@ -954,23 +737,13 @@ class BackTestEngine: "deal": deal_ticket, } ) - margin = await self.order_calc_margin( - action=action, - symbol=symbol, - volume=volume, - price=price, - use_terminal=use_terminal, - ) + margin = await self.order_calc_margin(action=action, symbol=symbol, volume=volume, price=price, use_terminal=use_terminal) self.positions.set_margin(ticket=order_ticket, margin=margin) self.update_account(margin=margin) - return OrderSendResult( - (osr.get(k, 0) for k in OrderSendResult.__match_args__) - ) + return OrderSendResult((osr.get(k, 0) for k in OrderSendResult.__match_args__)) @error_handler - async def order_check( - self, *, request: dict, use_terminal: bool = False - ) -> OrderCheckResult: + async def order_check(self, *, request: dict, use_terminal: bool = False) -> OrderCheckResult: use_terminal = self.use_terminal or use_terminal ocr = { "retcode": 10013, @@ -981,69 +754,30 @@ class BackTestEngine: "margin_free": 0, "margin_level": 0, "comment": "Invalid request", - "request": TradeRequest( - request.get(k, (0 if k != "comment" else "")) - for k in TradeRequest.__match_args__ - ), + "request": TradeRequest(request.get(k, (0 if k != "comment" else "")) for k in TradeRequest.__match_args__), } - action, symbol, volume = ( - request.get("action"), - request.get("symbol"), - request.get("volume"), - ) - price, order_type, position_id = ( - request.get("price"), - request.get("type"), - request.get("position"), - ) + action, symbol, volume = (request.get("action"), request.get("symbol"), request.get("volume")) + price, order_type, position_id = (request.get("price"), request.get("type"), request.get("position")) # check margin and confirm order can go through for a deal action and buy or sell order type - if ( - action == TradeAction.DEAL - and order_type in (OrderType.BUY, OrderType.SELL) - and position_id is None - ): - margin = await self.order_calc_margin( - action=action, - symbol=symbol, - volume=volume, - price=price, - use_terminal=use_terminal, - ) + if action == TradeAction.DEAL and order_type in (OrderType.BUY, OrderType.SELL) and position_id is None: + margin = await self.order_calc_margin(action=action, symbol=symbol, volume=volume, price=price, use_terminal=use_terminal) if margin is None: - return OrderCheckResult( - (ocr.get(k, 0) for k in OrderCheckResult.__match_args__) - ) + return OrderCheckResult((ocr.get(k, 0) for k in OrderCheckResult.__match_args__)) used_margin = self._account.margin + margin free_margin = self._account.margin_free - margin - level = ( - self._account.equity / used_margin * 100 - if used_margin - else float("inf") - ) - margin_level = ( - level - if self._account.margin_so_mode == AccountStopOutMode.PERCENT - else free_margin - ) - ocr.update( - { - "margin_level": margin_level, - "margin": margin, - "margin_free": free_margin, - } - ) + level = self._account.equity / used_margin * 100 if used_margin else float("inf") + margin_level = level if self._account.margin_so_mode == AccountStopOutMode.PERCENT else free_margin + ocr.update({"margin_level": margin_level, "margin": margin, "margin_free": free_margin}) # check if the account has enough money if margin_level < self._account.margin_so_call: ocr["retcode"] = 10019 ocr["comment"] = "No money" - return OrderCheckResult( - (ocr.get(k, 0) for k in OrderCheckResult.__match_args__) - ) + return OrderCheckResult((ocr.get(k, 0) for k in OrderCheckResult.__match_args__)) # check if the stops level is valid sym = await self.get_symbol_info(symbol=symbol) @@ -1053,12 +787,8 @@ class BackTestEngine: if action == TradeAction.SLTP: pos = self.positions.get(request.get("position")) sym = await self.get_symbol_info(symbol=pos.symbol) - current_tick = sym or await self.get_price_tick( - pos.symbol, self.cursor.time - ) - current_price = ( - current_tick.bid if pos.type == OrderType.BUY else current_tick.ask - ) + current_tick = sym or await self.get_price_tick(pos.symbol, self.cursor.time) + current_price = current_tick.bid if pos.type == OrderType.BUY else current_tick.ask min_sl = min(sl, tp) dsl = abs(current_price - min_sl) / sym.point @@ -1066,46 +796,29 @@ class BackTestEngine: if dsl < tsl: ocr["retcode"] = 10016 ocr["comment"] = "Invalid stops" - return OrderCheckResult( - (ocr.get(k, 0) for k in OrderCheckResult.__match_args__) - ) + return OrderCheckResult((ocr.get(k, 0) for k in OrderCheckResult.__match_args__)) elif action == TradeAction.SLTP: ocr["comment"] = "Done" ocr["retcode"] = 0 - return OrderCheckResult( - (ocr.get(k, 0) for k in OrderCheckResult.__match_args__) - ) + return OrderCheckResult((ocr.get(k, 0) for k in OrderCheckResult.__match_args__)) if use_terminal or self.use_terminal: ocr_t = await self.mt5.order_check(request) if ocr_t.retcode in (10013, 10014): return ocr_t - elif action == TradeAction.DEAL and order_type in ( - OrderType.BUY, - OrderType.SELL, - ): + elif action == TradeAction.DEAL and order_type in (OrderType.BUY, OrderType.SELL): # check volume if volume < sym.volume_min or volume > sym.volume_max: ocr["retcode"] = 10014 ocr["comment"] = "Invalid volume" - return OrderCheckResult( - (ocr.get(k, 0) for k in OrderCheckResult.__match_args__) - ) + return OrderCheckResult((ocr.get(k, 0) for k in OrderCheckResult.__match_args__)) ocr.update( - { - "balance": self._account.balance, - "profit": self._account.profit, - "equity": self._account.equity, - "comment": "Done", - "retcode": 0, - } + {"balance": self._account.balance, "profit": self._account.profit, "equity": self._account.equity, "comment": "Done", "retcode": 0} ) - return OrderCheckResult( - (ocr.get(k, 0) for k in OrderCheckResult.__match_args__) - ) + return OrderCheckResult((ocr.get(k, 0) for k in OrderCheckResult.__match_args__)) @error_handler async def get_terminal_info(self) -> TerminalInfo: @@ -1173,19 +886,8 @@ class BackTestEngine: return SymbolInfo((info.get(key) for key in SymbolInfo.__match_args__)) @error_handler - async def get_rates_from( - self, - *, - symbol: str, - timeframe: TimeFrame, - date_from: datetime | float, - count: int, - ) -> np.ndarray: - date_from = ( - date_from.astimezone(tz=UTC) - if isinstance(date_from, datetime) - else datetime.fromtimestamp(date_from, tz=UTC) - ) + async def get_rates_from(self, *, symbol: str, timeframe: TimeFrame, date_from: datetime | float, count: int) -> np.ndarray: + date_from = date_from.astimezone(tz=UTC) if isinstance(date_from, datetime) else datetime.fromtimestamp(date_from, tz=UTC) if self.use_terminal: rates = await self.mt5.copy_rates_from(symbol, timeframe, date_from, count) return rates @@ -1194,20 +896,12 @@ class BackTestEngine: start = int(date_from.timestamp()) start = round_down(start, timeframe.seconds) rates = rates[rates.time <= start].iloc[-count:] - return np.fromiter( - (tuple(i) for i in rates.iloc), dtype=self.get_dtype(df=rates) - ) + return np.fromiter((tuple(i) for i in rates.iloc), dtype=self.get_dtype(df=rates)) @error_handler - async def get_rates_from_pos( - self, *, symbol: str, timeframe: TimeFrame, start_pos: int, count: int - ) -> np.ndarray: + async def get_rates_from_pos(self, *, symbol: str, timeframe: TimeFrame, start_pos: int, count: int) -> np.ndarray: if self.use_terminal: - current_time = ( - self.cursor.time - if start_pos == 0 - else self.cursor.time - start_pos * timeframe.seconds - ) + current_time = self.cursor.time if start_pos == 0 else self.cursor.time - start_pos * timeframe.seconds current_time = round_up(current_time, timeframe.seconds) start = datetime.fromtimestamp(current_time, tz=UTC) rates = await self.mt5.copy_rates_from(symbol, timeframe, start, count) @@ -1218,63 +912,28 @@ class BackTestEngine: # the current time rounded up to a multiple of the timeframe in seconds and then subtracted by the start_pos # multiplied by the timeframe in seconds gives the time of the last candlestick in the range when using # copy_rates_from_pos - end = ( - int(round_down(self.cursor.time, timeframe.seconds)) - - start_pos * timeframe.seconds - ) + end = int(round_down(self.cursor.time, timeframe.seconds)) - start_pos * timeframe.seconds start = end - count * timeframe.seconds rates = rates[(rates.time > start) & (rates.time <= end)] - return np.fromiter( - (tuple(i) for i in rates.iloc), dtype=self.get_dtype(df=rates) - ) + return np.fromiter((tuple(i) for i in rates.iloc), dtype=self.get_dtype(df=rates)) @error_handler - async def get_rates_range( - self, - *, - symbol: str, - timeframe: TimeFrame, - date_from: datetime | float, - date_to: datetime | float, - ) -> np.ndarray: - date_from = ( - date_from.astimezone(tz=UTC) - if isinstance(date_from, datetime) - else datetime.fromtimestamp(date_from, tz=UTC) - ) - date_to = ( - date_to.astimezone(tz=UTC) - if isinstance(date_to, datetime) - else datetime.fromtimestamp(date_to, tz=UTC) - ) + async def get_rates_range(self, *, symbol: str, timeframe: TimeFrame, date_from: datetime | float, date_to: datetime | float) -> np.ndarray: + date_from = date_from.astimezone(tz=UTC) if isinstance(date_from, datetime) else datetime.fromtimestamp(date_from, tz=UTC) + date_to = date_to.astimezone(tz=UTC) if isinstance(date_to, datetime) else datetime.fromtimestamp(date_to, tz=UTC) if self.use_terminal: - rates = await self.mt5.copy_rates_range( - symbol, timeframe, date_from, date_to - ) + rates = await self.mt5.copy_rates_range(symbol, timeframe, date_from, date_to) return rates rates = self.rates[symbol][timeframe] start = round_up(int(date_from.timestamp()), timeframe.seconds) end = round_up(int(date_to.timestamp()), timeframe.seconds) rates = rates[(rates.time >= start) & (rates.time <= end)] - return np.fromiter( - (tuple(i) for i in rates.iloc), dtype=self.get_dtype(df=rates) - ) + return np.fromiter((tuple(i) for i in rates.iloc), dtype=self.get_dtype(df=rates)) @error_handler - async def get_ticks_from( - self, - *, - symbol: str, - date_from: datetime | float, - count: int, - flags: CopyTicks = CopyTicks.ALL, - ) -> np.ndarray: - date_from = ( - date_from.astimezone(tz=UTC) - if isinstance(date_from, datetime) - else datetime.fromtimestamp(date_from, tz=UTC) - ) + async def get_ticks_from(self, *, symbol: str, date_from: datetime | float, count: int, flags: CopyTicks = CopyTicks.ALL) -> np.ndarray: + date_from = date_from.astimezone(tz=UTC) if isinstance(date_from, datetime) else datetime.fromtimestamp(date_from, tz=UTC) if self.use_terminal: ticks = await self.mt5.copy_ticks_from(symbol, date_from, count, flags) return ticks @@ -1282,29 +941,14 @@ class BackTestEngine: ticks = self.ticks[symbol] start = int(date_from.timestamp()) rates = ticks[ticks.time <= start].iloc[-count:] - return np.fromiter( - (tuple(i) for i in rates.iloc), dtype=self.get_dtype(df=rates) - ) + return np.fromiter((tuple(i) for i in rates.iloc), dtype=self.get_dtype(df=rates)) @error_handler async def get_ticks_range( - self, - *, - symbol: str, - date_from: datetime | float, - date_to: datetime | float, - flags: CopyTicks = CopyTicks.ALL, + self, *, symbol: str, date_from: datetime | float, date_to: datetime | float, flags: CopyTicks = CopyTicks.ALL ) -> np.ndarray: - date_from = ( - date_from.astimezone(tz=UTC) - if isinstance(date_from, datetime) - else datetime.fromtimestamp(date_from, tz=UTC) - ) - date_to = ( - date_to.astimezone(tz=UTC) - if isinstance(date_to, datetime) - else datetime.fromtimestamp(date_to, tz=UTC) - ) + date_from = date_from.astimezone(tz=UTC) if isinstance(date_from, datetime) else datetime.fromtimestamp(date_from, tz=UTC) + date_to = date_to.astimezone(tz=UTC) if isinstance(date_to, datetime) else datetime.fromtimestamp(date_to, tz=UTC) if self.use_terminal: ticks = await self.mt5.copy_ticks_range(symbol, date_from, date_to, flags) return ticks @@ -1313,19 +957,11 @@ class BackTestEngine: start = int(date_from.timestamp()) end = int(date_to.timestamp()) rates = ticks[(ticks.time >= start) & (ticks.time <= end)] - return np.fromiter( - (tuple(i) for i in rates.iloc), dtype=self.get_dtype(df=ticks) - ) + return np.fromiter((tuple(i) for i in rates.iloc), dtype=self.get_dtype(df=ticks)) @error_handler async def order_calc_margin( - self, - *, - action: Literal[OrderType.BUY, OrderType.SELL], - symbol: str, - volume: float, - price: float, - use_terminal: bool = None, + self, *, action: Literal[OrderType.BUY, OrderType.SELL], symbol: str, volume: float, price: float, use_terminal: bool = None ): use_terminal = use_terminal if use_terminal is not None else self.use_terminal if use_terminal: @@ -1334,41 +970,22 @@ class BackTestEngine: sym = self.symbols.get(symbol) if sym is None and self.use_terminal: sym = await self._symbol_info(symbol=symbol) - margin = (volume * sym.trade_contract_size * price) / ( - self._account.leverage / (sym.margin_initial or 1) - ) + margin = (volume * sym.trade_contract_size * price) / (self._account.leverage / (sym.margin_initial or 1)) return round(margin, self._account.currency_digits) @error_handler async def order_calc_profit( - self, - *, - action: Literal[OrderType.BUY, OrderType.SELL], - symbol: str, - volume: float, - price_open: float, - price_close: float, - use_terminal=None, + self, *, action: Literal[OrderType.BUY, OrderType.SELL], symbol: str, volume: float, price_open: float, price_close: float, use_terminal=None ): use_terminal = use_terminal if use_terminal is not None else self.use_terminal if use_terminal: - return await self.mt5.order_calc_profit( - action, symbol, volume, price_open, price_close - ) + return await self.mt5.order_calc_profit(action, symbol, volume, price_open, price_close) sym = self.symbols.get(symbol) if sym is None and self.use_terminal: sym = await self._symbol_info(symbol=symbol) - profit = ( - volume - * sym.trade_contract_size - * ( - (price_close - price_open) - if action == OrderType.BUY - else (price_open - price_close) - ) - ) + profit = volume * sym.trade_contract_size * ((price_close - price_open) if action == OrderType.BUY else (price_open - price_close)) return round(profit, self._account.currency_digits) @error_handler_sync @@ -1382,9 +999,7 @@ class BackTestEngine: return 0 @error_handler_sync - def get_orders( - self, *, symbol: str = "", group: str = "", ticket: int = None - ) -> tuple[TradeOrder, ...]: + def get_orders(self, *, symbol: str = "", group: str = "", ticket: int = None) -> tuple[TradeOrder, ...]: """ Get pending orders from the terminal history. This has to do with pending orders, which this backtester doesn't support yet. @@ -1412,9 +1027,7 @@ class BackTestEngine: return self.positions.positions_total() @error_handler_sync - def get_positions( - self, *, symbol: str = None, group: str = None, ticket: int = None - ) -> tuple[TradePosition, ...]: + def get_positions(self, *, symbol: str = None, group: str = None, ticket: int = None) -> tuple[TradePosition, ...]: """ Get open positions from the terminal history. @@ -1429,9 +1042,7 @@ class BackTestEngine: return self.positions.positions_get(ticket=ticket, symbol=symbol, group=group) @error_handler_sync - def get_history_orders_total( - self, *, date_from: datetime | float, date_to: datetime | float - ) -> int: + def get_history_orders_total(self, *, date_from: datetime | float, date_to: datetime | float) -> int: """ Get the total number of orders in the terminal history. @@ -1447,13 +1058,7 @@ class BackTestEngine: @error_handler_sync def get_history_orders( - self, - *, - date_from: datetime | float = None, - date_to: datetime | float = None, - group: str = "", - ticket: int = None, - position: int = None, + self, *, date_from: datetime | float = None, date_to: datetime | float = None, group: str = "", ticket: int = None, position: int = None ) -> tuple[TradeOrder, ...]: """ Get orders from the terminal history. @@ -1468,18 +1073,10 @@ class BackTestEngine: Returns: tuple[TradeOrder]: Orders in the history """ - return self.orders.history_orders_get( - date_from=date_from, - date_to=date_to, - group=group, - ticket=ticket, - position=position, - ) + return self.orders.history_orders_get(date_from=date_from, date_to=date_to, group=group, ticket=ticket, position=position) @error_handler_sync - def get_history_deals_total( - self, *, date_from: datetime | float, date_to: datetime | float - ) -> int: + def get_history_deals_total(self, *, date_from: datetime | float, date_to: datetime | float) -> int: """ Get the total number of deals in the terminal history. @@ -1494,13 +1091,7 @@ class BackTestEngine: @error_handler_sync def get_history_deals( - self, - *, - date_from: datetime | float = None, - date_to: datetime | float = None, - group: str = None, - position: int = None, - ticket: int = None, + self, *, date_from: datetime | float = None, date_to: datetime | float = None, group: str = None, position: int = None, ticket: int = None ) -> tuple[TradeDeal, ...]: """ Get deals from the terminal history. @@ -1515,10 +1106,4 @@ class BackTestEngine: Returns: tuple[TradeDeal, ...]: Deals in the history """ - return self.deals.history_deals_get( - date_from=date_from, - date_to=date_to, - group=group, - position=position, - ticket=ticket, - ) + return self.deals.history_deals_get(date_from=date_from, date_to=date_to, group=group, position=position, ticket=ticket) diff --git a/src/aiomql/core/backtesting/get_data.py b/src/aiomql/core/backtesting/get_data.py index e2cdf8b..7b9118e 100644 --- a/src/aiomql/core/backtesting/get_data.py +++ b/src/aiomql/core/backtesting/get_data.py @@ -58,15 +58,7 @@ class BackTestData: class GetData: data: BackTestData - def __init__( - self, - *, - start: datetime, - end: datetime, - symbols: Sequence[str], - timeframes: Sequence[TimeFrame], - name: str = "", - ): + def __init__(self, *, start: datetime, end: datetime, symbols: Sequence[str], timeframes: Sequence[TimeFrame], name: str = ""): """""" self.config = Config() self.start = start.astimezone(tz=UTC) @@ -102,14 +94,8 @@ class GetData: logger.error(f"Error: {err}") def save_data(self, *, name: str | Path = ""): - name = name or ( - 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 = name or (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 with open(name, "wb") as fo: pickle.dump(self.data, fo, protocol=pickle.HIGHEST_PROTOCOL) @@ -118,26 +104,15 @@ class GetData: if workers: self.task_queue.workers = workers - q_items = [ - QueueItem(self.get_symbols_rates), - QueueItem(self.get_symbols_ticks), - QueueItem(self.get_symbols_info), - ] + q_items = [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: - self.task_queue.add( - item=QueueItem(self.get_account_info), must_complete=True - ) + self.task_queue.add(item=QueueItem(self.get_account_info), must_complete=True) if not self.data.terminal: - self.task_queue.add( - item=QueueItem(self.get_terminal_info), must_complete=True - ) + self.task_queue.add(item=QueueItem(self.get_terminal_info), must_complete=True) if not self.data.version: 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: logger.warning("Data not fully loaded") - self.data = BackTestData( - name=self.name, span=self.span, range=self.range, fully_loaded=False - ) + self.data = BackTestData(name=self.name, span=self.span, range=self.range, fully_loaded=False) async def get_terminal_info(self): """""" @@ -179,29 +152,16 @@ class GetData: 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): """""" - [ - 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): """""" [ - self.task_queue.add( - item=QueueItem( - self.get_symbol_rates, symbol=symbol, timeframe=timeframe - ), - priority=4, - ) + self.task_queue.add(item=QueueItem(self.get_symbol_rates, symbol=symbol, timeframe=timeframe), priority=4) for symbol in self.symbols for timeframe in self.timeframes if self.data.rates.get(symbol, {}).get(timeframe) is None @@ -219,9 +179,7 @@ class GetData: @backoff_decorator async def get_symbol_ticks(self, *, symbol: str): """""" - res = await self.mt5.copy_ticks_range( - symbol, self.start, self.end, MetaTrader5.COPY_TICKS_ALL - ) + res = await self.mt5.copy_ticks_range(symbol, self.start, self.end, MetaTrader5.COPY_TICKS_ALL) if res is None: self.data.fully_loaded = False self.task_queue.stop_queue() diff --git a/src/aiomql/core/backtesting/trades_manager.py b/src/aiomql/core/backtesting/trades_manager.py index 27651e2..132f8ca 100644 --- a/src/aiomql/core/backtesting/trades_manager.py +++ b/src/aiomql/core/backtesting/trades_manager.py @@ -66,13 +66,9 @@ class PositionsManager(TradeManager): _open_positions: set[int] margins: dict[int, float] - def __init__( - self, *, data: dict = None, open_positions: set = None, margins: dict = None - ): + def __init__(self, *, data: dict = None, open_positions: set = None, margins: dict = None): super().__init__(data=data) - self._open_positions = open_positions or { - trade.ticket for trade in self._data.values() - } + self._open_positions = open_positions or {trade.ticket for trade in self._data.values()} self.margins: dict[int, float] = margins or dict() def __len__(self): @@ -113,22 +109,12 @@ class PositionsManager(TradeManager): def set_margin(self, *, ticket: int, margin: float): self.margins[ticket] = margin - def positions_get( - self, *, ticket: int = None, symbol: str = None, group: None = None - ) -> tuple[TradePosition, ...]: + def positions_get(self, *, ticket: int = None, symbol: str = None, group: None = None) -> tuple[TradePosition, ...]: if ticket: - return tuple( - position - for position in self.open_positions - if position.ticket == ticket - ) + return tuple(position for position in self.open_positions if position.ticket == ticket) if symbol: - return tuple( - position - for position in self.open_positions - if position.symbol == symbol - ) + return tuple(position for position in self.open_positions if position.symbol == symbol) if group: return self.open_positions @@ -143,33 +129,19 @@ class PositionsManager(TradeManager): @property def open_positions(self) -> tuple[TradePosition, ...]: - return tuple( - position - for position in self.values() - if position.ticket in self._open_positions - ) + return tuple(position for position in self.values() if position.ticket in self._open_positions) class OrdersManager(TradeManager): _data = dict[int, TradeOrder] - def get_orders_range( - self, *, date_from: float, date_to: float - ) -> tuple[TradeData, ...]: + def get_orders_range(self, *, date_from: float, date_to: float) -> tuple[TradeData, ...]: start = date_from.timestamp() if isinstance(date_from, datetime) else date_from end = date_to.timestamp() if isinstance(date_to, datetime) else date_to - return tuple( - order for order in self.values() if start <= order.time_setup <= end - ) + return tuple(order for order in self.values() if start <= order.time_setup <= end) def history_orders_get( - self, - *, - date_from: float | datetime = None, - date_to: float | datetime = None, - group: str = "", - ticket: int = None, - position: int = None, + self, *, date_from: float | datetime = None, date_to: float | datetime = None, group: str = "", ticket: int = None, position: int = None ) -> tuple[TradeOrder, ...]: if date_from and 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) if position: - return tuple( - order for order in self.values() if order.position_id == position - ) + return tuple(order for order in self.values() if order.position_id == position) return () - def history_orders_total( - self, *, date_from: datetime | float, date_to: datetime | float - ) -> int: + def history_orders_total(self, *, date_from: datetime | float, date_to: datetime | float) -> int: return len(self.get_orders_range(date_from=date_from, date_to=date_to)) class DealsManager(TradeManager): _data = dict[int, TradeDeal] - def get_deals_range( - self, *, date_from: float, date_to: float - ) -> tuple[TradeData, ...]: + def get_deals_range(self, *, date_from: float, date_to: float) -> tuple[TradeData, ...]: start = date_from.timestamp() if isinstance(date_from, datetime) else date_from 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) def history_deals_get( - self, - *, - date_from: float | datetime = None, - date_to: float | datetime = None, - group: str = "", - ticket: int = None, - position: int = None, + self, *, date_from: float | datetime = None, date_to: float | datetime = None, group: str = "", ticket: int = None, position: int = None ) -> tuple[TradeDeal, ...]: if date_from and date_to: deals = self.get_deals_range(date_from=date_from, date_to=date_to) @@ -226,7 +186,5 @@ class DealsManager(TradeManager): return () - def history_deals_total( - self, *, date_from: datetime | float, date_to: datetime | float - ) -> int: + def history_deals_total(self, *, date_from: datetime | float, date_to: datetime | float) -> int: return len(self.get_deals_range(date_from=date_from, date_to=date_to)) diff --git a/src/aiomql/core/base.py b/src/aiomql/core/base.py index 042514e..813f8ea 100644 --- a/src/aiomql/core/base.py +++ b/src/aiomql/core/base.py @@ -24,32 +24,14 @@ class Base: Args: **kwargs: Set instance attributes with keyword arguments. Only if they are annotated on the class body. """ - self.exclude = { - "mt5", - "config", - "exclude", - "include", - "annotations", - "class_vars", - "dict", - "_instance", - } + self.exclude = {"mt5", "config", "exclude", "include", "annotations", "class_vars", "dict", "_instance"} self.include = set() self.set_attributes(**kwargs) def __repr__(self): - 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)) - ] + 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))] args = ", ".join("%s=%s" % (i, j) for i, j in kv[:3]) - args = ( - args - if len(kv) <= 3 - else args + " ... " + ", ".join("%s=%s" % (i, j) for i, j in kv[-1:]) - ) + args = 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} def set_attributes(self, **kwargs): @@ -68,21 +50,15 @@ class Base: try: setattr(self, i, self.annotations[i](j)) except KeyError: - logger.debug( - f"Attribute {i} does not belong to class {self.__class__.__name__}" - ) + logger.debug(f"Attribute {i} does not belong to class {self.__class__.__name__}") continue except (ValueError, TypeError): - logger.debug( - f"Cannot covert object of type {type(j)} to type {self.annotations[i]}" - ) + logger.debug(f"Cannot covert object of type {type(j)} to type {self.annotations[i]}") setattr(self, i, j) except Exception as exe: - logger.debug( - f"Did not set attribute {i} on class {self.__class__.__name__} due to {exe}" - ) + logger.debug(f"Did not set attribute {i} on class {self.__class__.__name__} due to {exe}") continue @property @@ -113,11 +89,7 @@ class Base: """ exclude, include = exclude or set(), include or set() filter_ = include or set(self.dict.keys()).difference(exclude) - return { - key: value - for key, value in self.dict.items() - if key in filter_ and value is not None - } + return {key: value for key, value in self.dict.items() if key in filter_ and value is not None} @property @cache @@ -131,9 +103,7 @@ class Base: cls_dict = {} for cls in clss: cls_dict |= cls.__dict__ - return { - key: value for key, value in cls_dict.items() if key in self.annotations - } + return {key: value for key, value in cls_dict.items() if key in self.annotations} @property def dict(self) -> dict: @@ -144,11 +114,7 @@ class Base: """ try: _filter = self.exclude.difference(self.include) - return { - key: value - for key, value in (self.class_vars | self.__dict__).items() - if key not in _filter and value is not None - } + return {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: logger.warning(err) diff --git a/src/aiomql/core/config.py b/src/aiomql/core/config.py index 00c4950..2b75cf5 100644 --- a/src/aiomql/core/config.py +++ b/src/aiomql/core/config.py @@ -114,9 +114,7 @@ class Config: **kwargs: Object attributes and values as keyword arguments """ if kwargs.pop("root", None) is not None: - logger.warning( - "Tried setting root from set_attributes. Use load_config to change project root" - ) + logger.warning("Tried setting root from set_attributes. Use load_config to change project root") [setattr(self, key, value) for key, value in kwargs.items()] @staticmethod @@ -145,14 +143,7 @@ class Config: logger.debug(f"Error finding config file: {err}") return - def load_config( - self, - *, - file: str | Path = None, - filename: str = None, - root: str | Path = None, - **kwargs, - ) -> Self: + def load_config(self, *, file: str | Path = None, filename: str = None, root: str | Path = None, **kwargs) -> Self: """Load configuration settings from a file. Keyword Args: @@ -190,17 +181,9 @@ class Config: self.set_attributes(**data) if self.path: - self.path = ( - self.root / self.path - if not Path(self.path).resolve().exists() - else self.path - ) + self.path = self.root / self.path if not Path(self.path).resolve().exists() else self.path - if self.record_trades and ( - hasattr(self, "records_dir") is False - or self.records_dir is None - or root is not None - ): + if self.record_trades and (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.mkdir(parents=True, exist_ok=True) diff --git a/src/aiomql/core/meta_backtester.py b/src/aiomql/core/meta_backtester.py index 00a5ada..1f07d92 100644 --- a/src/aiomql/core/meta_backtester.py +++ b/src/aiomql/core/meta_backtester.py @@ -3,17 +3,7 @@ from logging import getLogger from typing import Literal, TypeVar from numpy import ndarray -from MetaTrader5 import ( - Tick, - SymbolInfo, - AccountInfo, - TerminalInfo, - TradeOrder, - TradePosition, - TradeDeal, - OrderCheckResult, - OrderSendResult, -) +from MetaTrader5 import Tick, SymbolInfo, AccountInfo, TerminalInfo, TradeOrder, TradePosition, TradeDeal, OrderCheckResult, OrderSendResult from .meta_trader import MetaTrader from .constants import TimeFrame, CopyTicks, OrderType @@ -49,73 +39,29 @@ class MetaBackTester(MetaTrader): return await super().last_error() async def initialize( - self, - *, - path: str = "", - login: int = 0, - password: str = "", - server: str = "", - timeout: int | None = None, - portable=False, + self, *, path: str = "", login: int = 0, password: str = "", server: str = "", timeout: int | None = None, portable=False ) -> bool: if self.config.use_terminal_for_backtesting: - return await super().initialize( - path=path, - login=login, - password=password, - server=server, - timeout=timeout, - ) + return await super().initialize(path=path, login=login, password=password, server=server, timeout=timeout) return True def initialize_sync( - self, - *, - path: str = "", - login: int = 0, - password: str = "", - server: str = "", - timeout: int | None = None, - portable=False, + self, *, path: str = "", login: int = 0, password: str = "", server: str = "", timeout: int | None = None, portable=False ) -> bool: if self.config.use_terminal_for_backtesting: - return super().initialize_sync( - path=path, - login=login, - password=password, - server=server, - timeout=timeout, - ) + return super().initialize_sync(path=path, login=login, password=password, server=server, timeout=timeout) return True - def login_sync( - self, - *, - login: int = 0, - password: str = "", - server: str = "", - timeout: int = 60000, - ) -> bool: + def login_sync(self, *, login: int = 0, password: str = "", server: str = "", timeout: int = 60000) -> bool: if self.config.use_terminal_for_backtesting: - return super().login_sync( - login=login, password=password, server=server, timeout=timeout - ) + return super().login_sync(login=login, password=password, server=server, timeout=timeout) return True - async def login( - self, - *, - login: int = 0, - password: str = "", - server: str = "", - timeout: int = 60000, - ) -> bool: + async def login(self, *, login: int = 0, password: str = "", server: str = "", timeout: int = 60000) -> bool: if self.config.use_terminal_for_backtesting: - return await super().login( - login=login, password=password, server=server, timeout=timeout - ) + return await super().login(login=login, password=password, server=server, timeout=timeout) return True async def shutdown(self) -> None: @@ -153,56 +99,28 @@ class MetaBackTester(MetaTrader): return tick @error_handler(msg="test data not available", exe=AttributeError) - async def copy_rates_from( - self, symbol: str, timeframe: TimeFrame, date_from: datetime | float, count: int - ) -> ndarray | None: - rates = await self.backtest_engine.get_rates_from( - symbol=symbol, timeframe=timeframe, date_from=date_from, count=count - ) + async def copy_rates_from(self, symbol: str, timeframe: TimeFrame, date_from: datetime | float, count: int) -> ndarray | None: + rates = await self.backtest_engine.get_rates_from(symbol=symbol, timeframe=timeframe, date_from=date_from, count=count) return rates @error_handler(msg="test data not available", exe=AttributeError) - async def copy_rates_from_pos( - self, symbol: str, timeframe: TimeFrame, start_pos: int, count: int - ) -> ndarray | None: - rates = await self.backtest_engine.get_rates_from_pos( - symbol=symbol, timeframe=timeframe, start_pos=start_pos, count=count - ) + async def copy_rates_from_pos(self, symbol: str, timeframe: TimeFrame, start_pos: int, count: int) -> ndarray | None: + rates = await self.backtest_engine.get_rates_from_pos(symbol=symbol, timeframe=timeframe, start_pos=start_pos, count=count) return rates @error_handler(msg="test data not available", exe=AttributeError) - async def copy_rates_range( - self, - 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 - ) + async def copy_rates_range(self, 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 @error_handler(msg="test data not available", exe=AttributeError) - async def copy_ticks_from( - self, symbol: str, date_from: datetime | float, count: int, flags: CopyTicks - ) -> ndarray | None: - ticks = await self.backtest_engine.get_ticks_from( - symbol=symbol, date_from=date_from, count=count, flags=flags - ) + async def copy_ticks_from(self, symbol: str, date_from: datetime | float, count: int, flags: CopyTicks) -> ndarray | None: + ticks = await self.backtest_engine.get_ticks_from(symbol=symbol, date_from=date_from, count=count, flags=flags) return ticks @error_handler(msg="test data not available", exe=AttributeError) - async def copy_ticks_range( - self, - 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 - ) + async def copy_ticks_range(self, 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 @error_handler(msg="test data not available", exe=AttributeError) @@ -210,40 +128,19 @@ class MetaBackTester(MetaTrader): return self.backtest_engine.get_orders_total() @error_handler(msg="test data not available", exe=AttributeError) - async def orders_get( - self, group: str = "", ticket: int = 0, symbol: str = "" - ) -> tuple[TradeOrder, ...] | None: - kwargs = { - key: value - for key, value in (("group", group), ("ticket", ticket), ("symbol", symbol)) - if value - } + async def orders_get(self, group: str = "", ticket: int = 0, symbol: str = "") -> 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) @error_handler(msg="test data not available", exe=AttributeError) - async def order_calc_margin( - self, action: OrderType, symbol: str, volume: float, price: float - ) -> float | None: - res = await self.backtest_engine.order_calc_margin( - action=action, symbol=symbol, volume=volume, price=price - ) + async def order_calc_margin(self, action: OrderType, symbol: str, volume: float, price: float) -> float | None: + res = await self.backtest_engine.order_calc_margin(action=action, symbol=symbol, volume=volume, price=price) return res @error_handler(msg="test data not available", exe=AttributeError) - async def order_calc_profit( - self, - action: Literal[0, 1], - symbol: str, - volume: float, - price_open: float, - price_close: float, - ) -> float | None: + async def order_calc_profit(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( - action=action, - symbol=symbol, - volume=volume, - price_open=price_open, - price_close=price_close, + action=action, symbol=symbol, volume=volume, price_open=price_open, price_close=price_close ) return profit @@ -261,66 +158,30 @@ class MetaBackTester(MetaTrader): return self.backtest_engine.get_positions_total() @error_handler(msg="test data not available", exe=AttributeError) - async def positions_get( - self, group: str = "", ticket: int = None, symbol: str = "" - ) -> tuple[TradePosition, ...] | None: - kwargs = { - key: value - for key, value in (("group", group), ("ticket", ticket), ("symbol", symbol)) - if value - } + async def positions_get(self, group: str = "", ticket: int = None, symbol: str = "") -> 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) @error_handler(msg="test data not available", exe=AttributeError) - async def history_orders_total( - self, date_from: datetime | float, date_to: datetime | float - ) -> int | None: - return self.backtest_engine.get_history_orders_total( - date_from=date_from, date_to=date_to - ) + async def history_orders_total(self, date_from: datetime | float, date_to: datetime | float) -> 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) async def history_orders_get( - self, - date_from: datetime | float = None, - date_to: datetime | float = None, - group: str = "", - ticket: int = None, - position: int = None, + self, date_from: datetime | float = None, date_to: datetime | float = None, group: str = "", ticket: int = None, position: int = None ) -> tuple[TradeOrder, ...] | None: - args = ( - ("date_from", date_from), - ("date_to", date_to), - ("group", group), - ("ticket", ticket), - ("position", position), - ) + args = (("date_from", date_from), ("date_to", date_to), ("group", group), ("ticket", ticket), ("position", position)) kwargs = {key: value for key, value in args if value} return self.backtest_engine.get_history_orders(**kwargs) @error_handler(msg="test data not available", exe=AttributeError) - async def history_deals_total( - self, date_from: datetime | float, date_to: datetime | float - ) -> int | None: - return self.backtest_engine.get_history_deals_total( - date_from=date_from, date_to=date_to - ) + async def history_deals_total(self, date_from: datetime | float, date_to: datetime | float) -> 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) async def history_deals_get( - self, - date_from: datetime | float = None, - date_to: datetime | float = None, - group: str = "", - ticket: int = None, - position: int = None, + self, date_from: datetime | float = None, date_to: datetime | float = None, group: str = "", ticket: int = None, position: int = None ) -> tuple[TradeDeal, ...] | None: - args = ( - ("date_from", date_from), - ("date_to", date_to), - ("group", group), - ("ticket", ticket), - ("position", position), - ) + args = (("date_from", date_from), ("date_to", date_to), ("group", group), ("ticket", ticket), ("position", position)) kwargs = {key: value for key, value in args if value} return self.backtest_engine.get_history_deals(**kwargs) diff --git a/src/aiomql/core/meta_trader.py b/src/aiomql/core/meta_trader.py index d99b5d2..7ca328d 100644 --- a/src/aiomql/core/meta_trader.py +++ b/src/aiomql/core/meta_trader.py @@ -5,18 +5,8 @@ from typing import Literal from pathlib import Path import numpy as np -from MetaTrader5 import ( - BookInfo, - SymbolInfo, - AccountInfo, - Tick, - TerminalInfo, - TradeOrder, - TradeDeal, - TradePosition, - OrderSendResult, - OrderCheckResult, -) +from MetaTrader5 import (BookInfo, SymbolInfo, AccountInfo, Tick, TerminalInfo, TradeOrder, TradeDeal, TradePosition, + OrderSendResult, OrderCheckResult) import MetaTrader5 as mt5 from .constants import OrderType, CopyTicks @@ -75,14 +65,7 @@ class MetaTrader(MetaCore): logger.warning(f"{error_msg}:{self.error.description}") return res - async def login( - self, - *, - login: int = 0, - password: str = "", - server: str = "", - timeout: int = 60000, - ) -> bool: + async def login(self, *, login: int = 0, password: str = "", server: str = "", timeout: int = 60000) -> bool: """ 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) password = password or acc_details.get("password", "") server = server or acc_details.get("server", "") - return await asyncio.to_thread( - self._login, login, password=password, server=server, timeout=timeout - ) + return await asyncio.to_thread(self._login, login, password=password, server=server, timeout=timeout) - def login_sync( - self, - *, - login: int = 0, - password: str = "", - server: str = "", - timeout: int = 60000, - ) -> bool: + def login_sync(self, *, login: int = 0, password: str = "", server: str = "", timeout: int = 60000) -> bool: """ Connects to the MetaTrader terminal using the specified login, password and server. @@ -131,13 +105,7 @@ class MetaTrader(MetaCore): return res async def initialize( - self, - path: str = None, - login: int = 0, - password: str = "", - server: str = "", - timeout: int | None = None, - portable=False, + self, path: str = None, login: int = 0, password: str = "", server: str = "", timeout: int | None = None, portable=False ) -> bool: """ Initializes the connection to the MetaTrader terminal. All parameters are optional. @@ -179,13 +147,7 @@ class MetaTrader(MetaCore): return res def initialize_sync( - self, - path: str = None, - login: int = 0, - password: str = "", - server: str = "", - timeout: int | None = None, - portable=False, + self, path: str = None, login: int = 0, password: str = "", server: str = "", timeout: int | None = None, portable=False ) -> bool: """ 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: """""" - api = { - "func": self._account_info, - "error_msg": "Error in obtaining account information", - } + api = {"func": self._account_info, "error_msg": "Error in obtaining account information"} res = await self._handler(api) return res async def terminal_info(self) -> TerminalInfo | None: - api = { - "func": self._terminal_info, - "error_msg": "Error in obtaining terminal information", - } + api = {"func": self._terminal_info, "error_msg": "Error in obtaining terminal information"} res = await self._handler(api) return res async def symbols_total(self) -> int: - api = { - "func": self._symbols_total, - "error_msg": "Error in obtaining total symbols.", - } + api = {"func": self._symbols_total, "error_msg": "Error in obtaining total symbols."} res = await self._handler(api) return res async def symbols_get(self, group: str = "") -> tuple[SymbolInfo] | None: kwargs = {"group": group} if group else {} - api = { - "func": self._symbols_get, - "kwargs": kwargs, - "error_msg": "Error in obtaining symbols.", - } + api = {"func": self._symbols_get, "kwargs": kwargs, "error_msg": "Error in obtaining symbols."} res = await self._handler(api) return res async def symbol_info(self, symbol: str) -> SymbolInfo | None: - api = { - "func": self._symbol_info, - "args": (symbol,), - "error_msg": f"Error in obtaining information for {symbol}", - } + api = {"func": self._symbol_info, "args": (symbol,), "error_msg": f"Error in obtaining information for {symbol}"} res = await self._handler(api) return res async def symbol_info_tick(self, symbol: str) -> Tick | None: - api = { - "func": self._symbol_info_tick, - "args": (symbol,), - "error_msg": f"Error in obtaining tick for {symbol}", - } + api = {"func": self._symbol_info_tick, "args": (symbol,), "error_msg": f"Error in obtaining tick for {symbol}"} res = await self._handler(api) return res async def symbol_select(self, symbol: str, enable: bool) -> bool: - api = { - "func": self._symbol_select, - "args": (symbol, enable), - "error_msg": f"Error in selecting {symbol}", - } + api = {"func": self._symbol_select, "args": (symbol, enable), "error_msg": f"Error in selecting {symbol}"} res = await self._handler(api) return res async def market_book_add(self, symbol: str) -> bool: - api = { - "func": self._market_book_add, - "args": (symbol,), - "error_msg": f"Error in adding {symbol} to market book", - } + api = {"func": self._market_book_add, "args": (symbol,), "error_msg": f"Error in adding {symbol} to market book"} res = await self._handler(api) return res async def market_book_get(self, symbol: str) -> tuple[BookInfo] | None: - api = { - "func": self._market_book_get, - "args": (symbol,), - "error_msg": f"Error in obtaining market depth for {symbol}", - } + api = {"func": self._market_book_get, "args": (symbol,), "error_msg": f"Error in obtaining market depth for {symbol}"} res = await self._handler(api) return res async def market_book_release(self, symbol: str) -> bool: - api = { - "func": self._market_book_release, - "args": (symbol,), - "error_msg": f"Error in releasing market depth for {symbol}", - } + api = {"func": self._market_book_release, "args": (symbol,), "error_msg": f"Error in releasing market depth for {symbol}"} res = await self._handler(api) return res - async def copy_rates_from( - self, symbol: str, timeframe: int, date_from: datetime | float, count: int - ) -> np.ndarray | None: - api = { - "func": self._copy_rates_from, - "args": (symbol, timeframe, date_from, count), - "error_msg": f"Error in obtaining rates for {symbol}", - } + async def copy_rates_from(self, symbol: str, timeframe: int, date_from: datetime | float, count: int) -> 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) return res - async def copy_rates_from_pos( - self, symbol: str, timeframe: int, start_pos: int, count: int - ) -> np.ndarray | None: + async def copy_rates_from_pos(self, symbol: str, timeframe: int, start_pos: int, count: int) -> np.ndarray | None: api = { "func": self._copy_rates_from_pos, "args": (symbol, timeframe, start_pos, count), @@ -353,93 +270,39 @@ class MetaTrader(MetaCore): res = await self._handler(api) return res - async def copy_rates_range( - self, - 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}", - } + async def copy_rates_range(self, 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) return res - async def copy_ticks_from( - self, symbol: str, date_from: datetime | float, count: int, flags: CopyTicks - ) -> np.ndarray | None: - api = { - "func": self._copy_ticks_from, - "args": (symbol, date_from, count, flags), - "error_msg": f"Error in obtaining ticks for {symbol}", - } + async def copy_ticks_from(self, symbol: str, date_from: datetime | float, count: int, flags: CopyTicks) -> 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) return res - async def copy_ticks_range( - self, - 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}", - } + async def copy_ticks_range(self, 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) return res async def orders_total(self) -> int: - api = { - "func": self._orders_total, - "error_msg": "Error in obtaining total orders.", - } + api = {"func": self._orders_total, "error_msg": "Error in obtaining total orders."} res = await self._handler(api) return res - async def orders_get( - self, group: str = "", ticket: int = 0, symbol: str = "" - ) -> tuple[TradeOrder] | None: - 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.", - } + async def orders_get(self, group: str = "", ticket: int = 0, symbol: str = "") -> tuple[TradeOrder] | None: + 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) return res - async def order_calc_margin( - self, - 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.", - } + async def order_calc_margin(self, 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) return res async def order_calc_profit( - self, - action: Literal[OrderType.BUY, OrderType.SELL], - symbol: str, - volume: float, - price_open: float, - price_close: float, + self, action: Literal[OrderType.BUY, OrderType.SELL], symbol: str, volume: float, price_open: float, price_close: float ) -> float | None: api = { "func": self._order_calc_profit, @@ -450,119 +313,50 @@ class MetaTrader(MetaCore): return res async def order_check(self, request: dict) -> OrderCheckResult: - api = { - "func": self._order_check, - "args": (request,), - "error_msg": "Error in checking order.", - } + api = {"func": self._order_check, "args": (request,), "error_msg": "Error in checking order."} res = await self._handler(api) return res async def order_send(self, request: dict) -> OrderSendResult: - api = { - "func": self._order_send, - "args": (request,), - "error_msg": "Error in sending order.", - } + api = {"func": self._order_send, "args": (request,), "error_msg": "Error in sending order."} res = await self._handler(api) return res async def positions_total(self) -> int: - api = { - "func": self._positions_total, - "error_msg": "Error in obtaining total positions.", - } + api = {"func": self._positions_total, "error_msg": "Error in obtaining total positions."} res = await self._handler(api) return res - async def positions_get( - self, group: str = "", ticket: int = None, symbol: str = "" - ) -> tuple[TradePosition] | None: - 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.", - } + async def positions_get(self, group: str = "", ticket: int = None, symbol: str = "") -> tuple[TradePosition] | None: + 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) return res - async def history_orders_total( - self, date_from: datetime | float, date_to: datetime | float - ) -> int: - api = { - "func": self._history_orders_total, - "args": (date_from, date_to), - "error_msg": "Error in obtaining total history orders.", - } + async def history_orders_total(self, date_from: datetime | float, date_to: datetime | float) -> 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) return res async def history_orders_get( - self, - date_from: datetime | float = None, - date_to: datetime | float = None, - group: str = "", - ticket: int = None, - position: int = None, + self, date_from: datetime | float = None, date_to: datetime | float = None, group: str = "", ticket: int = None, position: int = None ) -> tuple[TradeOrder] | None: - kwargs = { - key: value - for key, value in ( - ("group", group), - ("ticket", ticket), - ("position", position), - ) - if value - } + kwargs = {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) - api = { - "func": self._history_orders_get, - "args": args, - "kwargs": kwargs, - "error_msg": "Error in obtaining history orders", - } + api = {"func": self._history_orders_get, "args": args, "kwargs": kwargs, "error_msg": "Error in obtaining history orders"} res = await self._handler(api) return res - async def history_deals_total( - self, date_from: datetime | float, date_to: datetime | float - ) -> int: - api = { - "func": self._history_deals_total, - "args": (date_from, date_to), - "error_msg": "Error in obtaining total history deals", - } + async def history_deals_total(self, date_from: datetime | float, date_to: datetime | float) -> 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) return res async def history_deals_get( - self, - date_from: datetime | float = None, - date_to: datetime | float = None, - group: str = "", - ticket: int = None, - position: int = None, + self, date_from: datetime | float = None, date_to: datetime | float = None, group: str = "", ticket: int = None, position: int = None ) -> tuple[TradeDeal] | None: - kwargs = { - key: value - for key, value in ( - ("group", group), - ("ticket", ticket), - ("position", position), - ) - if value - } + kwargs = {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) - api = { - "func": self._history_deals_get, - "args": args, - "kwargs": kwargs, - "error_msg": "Error in obtaining history deals", - } + api = {"func": self._history_deals_get, "args": args, "kwargs": kwargs, "error_msg": "Error in obtaining history deals"} res = await self._handler(api) return res diff --git a/src/aiomql/core/models.py b/src/aiomql/core/models.py index 7ba1a76..0538456 100644 --- a/src/aiomql/core/models.py +++ b/src/aiomql/core/models.py @@ -354,10 +354,7 @@ class SymbolInfo(Base): name: str = "" def __repr__(self): - return "%(class)s(name=%(name)s)" % { - "class": self.__class__.__name__, - "name": self.name, - } + return "%(class)s(name=%(name)s)" % {"class": self.__class__.__name__, "name": self.name} def __str__(self): return self.name diff --git a/src/aiomql/core/task_queue.py b/src/aiomql/core/task_queue.py index 52c707b..75cd0a2 100644 --- a/src/aiomql/core/task_queue.py +++ b/src/aiomql/core/task_queue.py @@ -31,9 +31,7 @@ class QueueItem: self.task_item(*self.args, **self.kwargs) except Exception as err: - logger.error( - f"Error {err} occurred in {self.task_item.__name__} with args {self.args} and kwargs {self.kwargs}" - ) + logger.error(f"Error {err} occurred in {self.task_item.__name__} with args {self.args} and kwargs {self.kwargs}") class TaskQueue: @@ -84,9 +82,7 @@ class TaskQueue: self.queue.task_done() self.priority_tasks.discard(item) - if self.stop and ( - self.on_exit == "cancel" or len(self.priority_tasks) == 0 - ): + if self.stop and (self.on_exit == "cancel" or len(self.priority_tasks) == 0): self.cancel() break @@ -107,36 +103,26 @@ class TaskQueue: async def run(self, timeout: int = 0): start = time.perf_counter() try: - self.tasks.extend( - asyncio.create_task(self.worker()) for _ in range(self.workers) - ) + self.tasks.extend(asyncio.create_task(self.worker()) for _ in range(self.workers)) timeout = timeout or self.timeout queue_task = asyncio.create_task(self.queue.join()) if timeout: - main_task = asyncio.create_task( - asyncio.wait_for(queue_task, timeout=timeout) - ) + main_task = asyncio.create_task(asyncio.wait_for(queue_task, timeout=timeout)) else: main_task = queue_task self.tasks.append(main_task) await main_task except TimeoutError: - logger.warning( - "Timed out after %d seconds, %d tasks remaining", - time.perf_counter() - start, - self.queue.qsize(), - ) + logger.warning("Timed out after %d seconds, %d tasks remaining", time.perf_counter() - start, self.queue.qsize()) self.stop = True except asyncio.CancelledError as _: logger.warning("Main task cancelled") except Exception as err: - logger.warning( - "%s: An error occurred in %s.run", err, self.__class__.__name__ - ) + logger.warning("%s: An error occurred in %s.run", err, self.__class__.__name__) finally: await self.clean_up() @@ -149,9 +135,7 @@ class TaskQueue: async def clean_up(self): try: if self.on_exit == "complete_priority" and len(self.priority_tasks) > 0: - logger.warning( - f"Completing {len(self.priority_tasks)} priority tasks..." - ) + logger.warning(f"Completing {len(self.priority_tasks)} priority tasks...") queue_task = asyncio.create_task(self.queue.join()) self.tasks.append(queue_task) await queue_task @@ -161,9 +145,7 @@ class TaskQueue: ... except Exception as err: - logger.error( - f"%s: Error occurred in %s.clean_up", err, self.__class__.__name__ - ) + logger.error(f"%s: Error occurred in %s.clean_up", err, self.__class__.__name__) finally: self.cancel() diff --git a/src/aiomql/lib/account.py b/src/aiomql/lib/account.py index 41897ce..d0a6f7e 100644 --- a/src/aiomql/lib/account.py +++ b/src/aiomql/lib/account.py @@ -15,6 +15,7 @@ class Account(_Base, AccountInfo): Attributes: connected (bool): Status of connection to MetaTrader 5 Terminal """ + _instance: Self connected: bool diff --git a/src/aiomql/lib/backtester.py b/src/aiomql/lib/backtester.py index 75c2d74..68e330d 100644 --- a/src/aiomql/lib/backtester.py +++ b/src/aiomql/lib/backtester.py @@ -25,6 +25,7 @@ class BackTester: config (Config): Config instance mt (MetaBackTester): MetaTrader instance """ + config: Config executor: Executor mt: MetaBackTester @@ -57,18 +58,12 @@ class BackTester: self.backtest_engine.setup_account_sync() self.init_strategies_sync() if (strategies := len(self.executor.strategy_runners)) == 0: - logger.warning( - "No strategies were added to the backtester. Exiting ..." - ) + logger.warning("No strategies were added to the backtester. Exiting ...") raise Exception("No strategies added to the backtester") self.config.task_queue.worker_timeout = 5 - self.add_coroutine( - coroutine=self.config.task_queue.run, on_separate_thread=True - ) + self.add_coroutine(coroutine=self.config.task_queue.run, on_separate_thread=True) self.add_coroutine(coroutine=self.executor.exit) - self.add_coroutine( - coroutine=self.backtest_controller.control, on_separate_thread=True - ) + self.add_coroutine(coroutine=self.backtest_controller.control, on_separate_thread=True) parties = strategies + 1 self.backtest_controller.set_parties(parties=parties) except Exception as err: @@ -92,31 +87,19 @@ class BackTester: await self.backtest_engine.setup_account() await self.init_strategies() if (strategies := len(self.executor.strategy_runners)) == 0: - logger.warning( - "No strategies were added to the backtester. Exiting ..." - ) + logger.warning("No strategies were added to the backtester. Exiting ...") raise Exception("No strategies added to the backtester") self.config.task_queue.worker_timeout = 5 - self.add_coroutine( - coroutine=self.config.task_queue.run, on_separate_thread=True - ) + self.add_coroutine(coroutine=self.config.task_queue.run, on_separate_thread=True) self.add_coroutine(coroutine=self.executor.exit) - self.add_coroutine( - coroutine=self.backtest_controller.control, on_separate_thread=True - ) + self.add_coroutine(coroutine=self.backtest_controller.control, on_separate_thread=True) parties = strategies + 1 self.backtest_controller.set_parties(parties=parties) except Exception as err: logger.error(f"{err}. Backtester initialization failed") raise SystemExit - def add_coroutine( - self, - *, - coroutine: Callable[..., ...] | Coroutine, - on_separate_thread=False, - **kwargs, - ): + def add_coroutine(self, *, coroutine: Callable[..., ...] | Coroutine, on_separate_thread=False, **kwargs): """Add a coroutine to the executor. Args: @@ -127,9 +110,7 @@ class BackTester: Returns: """ - self.executor.add_coroutine( - coroutine=coroutine, kwargs=kwargs, on_separate_thread=on_separate_thread - ) + self.executor.add_coroutine(coroutine=coroutine, kwargs=kwargs, on_separate_thread=on_separate_thread) def execute(self): """Execute the bot.""" @@ -161,14 +142,7 @@ class BackTester: """ [self.add_strategy(strategy=strategy) for strategy in strategies] - def add_strategy_all( - self, - *, - strategy: Type[Strategy], - params: dict | None = None, - symbols: list[Symbol] = None, - **kwargs, - ): + def add_strategy_all(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. Keyword Args: @@ -177,10 +151,7 @@ class BackTester: symbols (list): A list of symbols to run the strategy on **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: """Initialize a single strategy. This method is called internally by the bot.""" diff --git a/src/aiomql/lib/bot.py b/src/aiomql/lib/bot.py index c10e678..cf9dca2 100644 --- a/src/aiomql/lib/bot.py +++ b/src/aiomql/lib/bot.py @@ -21,6 +21,7 @@ class Bot: config (Config): Config instance mt (MetaTrader): MetaTrader instance """ + config: Config executor: Executor mt: MetaTrader @@ -61,9 +62,7 @@ class Bot: raise Exception("Unable to sign in to MetaTrader 5 Terminal") logger.info("Login Successful") await self.init_strategies() - self.add_coroutine( - coroutine=self.config.task_queue.run, on_separate_thread=True - ) + self.add_coroutine(coroutine=self.config.task_queue.run, on_separate_thread=True) self.add_coroutine(coroutine=self.executor.exit) if len(self.executor.strategy_runners) == 0: @@ -87,9 +86,7 @@ class Bot: raise Exception("Unable to sign in to MetaTrader 5 Terminal") logger.info("Login Successful") self.init_strategies_sync() - self.add_coroutine( - coroutine=self.config.task_queue.run, on_separate_thread=True - ) + self.add_coroutine(coroutine=self.config.task_queue.run, on_separate_thread=True) self.add_coroutine(coroutine=self.executor.exit) if len(self.executor.strategy_runners) == 0: @@ -107,13 +104,7 @@ class Bot: """ self.executor.add_function(function=function, kwargs=kwargs) - def add_coroutine( - self, - *, - coroutine: Callable[..., ...] | Coroutine, - on_separate_thread=False, - **kwargs, - ): + def add_coroutine(self, *, coroutine: Callable[..., ...] | Coroutine, on_separate_thread=False, **kwargs): """Add a coroutine to the executor. Args: @@ -124,9 +115,7 @@ class Bot: Returns: """ - self.executor.add_coroutine( - coroutine=coroutine, kwargs=kwargs, on_separate_thread=on_separate_thread - ) + self.executor.add_coroutine(coroutine=coroutine, kwargs=kwargs, on_separate_thread=on_separate_thread) def execute(self): """Execute the bot using asyncio.run""" @@ -157,14 +146,7 @@ class Bot: """ [self.add_strategy(strategy=strategy) for strategy in strategies] - def add_strategy_all( - self, - *, - strategy: Type[Strategy], - params: dict | None = None, - symbols: list[Symbol] = None, - **kwargs, - ): + def add_strategy_all(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. Keyword Args: @@ -173,10 +155,7 @@ class Bot: symbols (list): A list of symbols to run the strategy on **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: """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: info = info._asdict() info["swap_rollover3days"] = info.get("swap_rollover3days", 0) % 7 - info['select'] = select + info["select"] = select tick = Tick(**tick._asdict()) strategy.symbol.tick = tick strategy.symbol.set_attributes(**info) diff --git a/src/aiomql/lib/candle.py b/src/aiomql/lib/candle.py index 87ea65d..623e242 100644 --- a/src/aiomql/lib/candle.py +++ b/src/aiomql/lib/candle.py @@ -1,4 +1,5 @@ """Candle and Candles classes for handling bars from the MetaTrader 5 terminal.""" + import time from typing import Type, Self, Iterable from logging import getLogger @@ -45,9 +46,7 @@ class Candle: **kwargs: Candle attributes and values as keyword arguments. """ if not all(i in kwargs for i in ["open", "high", "low", "close"]): - raise ValueError( - "Candle must be instantiated with open, high, low and close prices" - ) + raise ValueError("Candle must be instantiated with open, high, low and close prices") self.time = kwargs.pop("time", time.monotonic_ns()) self.Index = kwargs.pop("Index", 0) self.real_volume = kwargs.pop("real_volume", 0) @@ -56,18 +55,15 @@ class Candle: self.set_attributes(**kwargs) def __repr__(self): - return ( - "%(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, - "high": self.high, - "low": self.low, - "close": self.close, - "time": self.time, - "Index": self.Index, - } - ) + return "%(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, + "high": self.high, + "low": self.low, + "close": self.close, + "time": self.time, + "Index": self.Index, + } def __eq__(self, other: Self): return self.time == other.time @@ -170,13 +166,7 @@ class Candles: timeframe: TimeFrame _data: DataFrame - def __init__( - self, - *, - data: DataFrame | Self | Iterable, - flip=False, - candle_class: Candle = None, - ): + def __init__(self, *, data: DataFrame | Self | Iterable, flip=False, candle_class: Candle = None): """A container class of Candle objects in chronological order. Args: @@ -236,9 +226,7 @@ class Candles: if item == "Index": return Series(self._data.index) - raise AttributeError( - f"Attribute {item} not defined on class {self.__class__.__name__}" - ) + raise AttributeError(f"Attribute {item} not defined on class {self.__class__.__name__}") def __iter__(self): return (self.Candle(**row._asdict()) for row in self._data.itertuples()) diff --git a/src/aiomql/lib/executor.py b/src/aiomql/lib/executor.py index 7087215..30cbac2 100644 --- a/src/aiomql/lib/executor.py +++ b/src/aiomql/lib/executor.py @@ -19,6 +19,7 @@ class 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 """ + executor: ThreadPoolExecutor tasks: list[asyncio.Task] config: Config @@ -36,18 +37,10 @@ class Executor: kwargs = kwargs or {} self.functions[function] = kwargs - def add_coroutine( - self, - *, - coroutine: Callable | Coroutine, - kwargs: dict = None, - on_separate_thread=False, - ): + def add_coroutine(self, *, coroutine: Callable | Coroutine, kwargs: dict = None, on_separate_thread=False): kwargs = kwargs or {} coroutine = coroutine(**kwargs) - self.coroutines.append( - coroutine - ) if on_separate_thread is False else self.coroutine_threads.append(coroutine) + self.coroutines.append(coroutine) if on_separate_thread is False else self.coroutine_threads.append(coroutine) def add_strategies(self, *, strategies: tuple[Strategy]): """Add multiple strategies at once @@ -85,9 +78,7 @@ class Executor: async def create_coroutines_task(self): """""" - task = asyncio.create_task( - asyncio.gather(*self.coroutines, return_exceptions=True) - ) + task = asyncio.create_task(asyncio.gather(*self.coroutines, return_exceptions=True)) self.tasks.append(task) await task @@ -116,9 +107,7 @@ class Executor: start = asyncio.get_event_loop().time() try: while self.config.shutdown is False and self.config.force_shutdown is False: - if self.timeout is not None and self.timeout < ( - asyncio.get_event_loop().time() - start - ): + if self.timeout is not None and self.timeout < (asyncio.get_event_loop().time() - start): self.config.shutdown = True for strategy in self.strategy_runners: @@ -140,25 +129,11 @@ class Executor: Notes: No matter the number specified, the executor will always use a minimum of 5 workers. """ - workers_ = ( - len(self.strategy_runners) - + len(self.functions) - + len(self.coroutine_threads) - + 2 - ) + workers_ = len(self.strategy_runners) + len(self.functions) + len(self.coroutine_threads) + 2 workers = max(workers, workers_) with ThreadPoolExecutor(max_workers=workers) as executor: self.executor = executor - [ - self.executor.submit(self.run_strategy, strategy) - for strategy in self.strategy_runners - ] - [ - 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_strategy, strategy) for strategy in self.strategy_runners] + [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) diff --git a/src/aiomql/lib/history.py b/src/aiomql/lib/history.py index 4e1a024..240b128 100644 --- a/src/aiomql/lib/history.py +++ b/src/aiomql/lib/history.py @@ -34,14 +34,7 @@ class History: total_orders: int group: str - def __init__( - self, - *, - date_from: datetime | float, - date_to: datetime | float, - group: str = "", - use_utc: bool = True, - ): + def __init__(self, *, date_from: datetime | float, date_to: datetime | float, group: str = "", use_utc: bool = True): """ Args: 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.mt5 = MetaTrader() if self.config.mode != "backtest" else MetaBackTester() - date_from = ( - date_from - if isinstance(date_from, datetime) - else datetime.fromtimestamp(date_from) - ) - date_to = ( - date_to - if isinstance(date_to, datetime) - else datetime.fromtimestamp(date_to) - ) + date_from = date_from 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_to = date_to.astimezone(pytz.UTC) if use_utc else date_to self.group = group @@ -74,9 +59,7 @@ class History: async def initialize(self): """Get history deals and orders""" - deals, orders = await asyncio.gather( - self.get_deals(), self.get_orders(), return_exceptions=True -) + deals, orders = await asyncio.gather(self.get_deals(), self.get_orders(), return_exceptions=True) self.deals = deals if isinstance(deals, tuple) else () self.orders = orders if isinstance(orders, tuple) else () self.total_deals = len(self.deals) @@ -89,9 +72,7 @@ class History: Returns: tuple[TradeDeal, ...]: A list of trade deals """ - deals = await self.mt5.history_deals_get( - date_from=self.date_from, date_to=self.date_to, group=self.group - ) + deals = await self.mt5.history_deals_get(date_from=self.date_from, date_to=self.date_to, group=self.group) if deals is not None: return tuple(TradeDeal(**deal._asdict()) for deal in deals) logger.warning(f"Failed to get deals") @@ -107,12 +88,7 @@ class History: Returns: tuple[TradeDeal]: A tuple of all deals with the order ticket """ - return tuple( - sorted( - (deal for deal in self.deals if deal.order == ticket), - key=lambda x: x.time_msc, - ) - ) + return tuple(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, ...]: """ @@ -123,12 +99,7 @@ class History: Returns: tuple[TradeDeal]: A tuple of all deals with the position ticket """ - return tuple( - sorted( - (deal for deal in self.deals if deal.position_id == position), - key=lambda x: x.time_msc, - ) - ) + return tuple(sorted((deal for deal in self.deals if deal.position_id == position), key=lambda x: x.time_msc)) @backoff_decorator async def get_orders(self) -> tuple[TradeOrder, ...]: @@ -137,9 +108,7 @@ class History: Returns: list[TradeOrder]: A list of trade orders """ - orders = await self.mt5.history_orders_get( - date_from=self.date_from, date_to=self.date_to, group=self.group - ) + orders = await self.mt5.history_orders_get(date_from=self.date_from, date_to=self.date_to, group=self.group) if orders is not None: 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, ...]: """filter orders by ticket""" - return tuple( - sorted( - (order for order in self.orders if order.ticket == ticket), - key=lambda x: x.time_done_msc, - ) - ) + return tuple(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, ...]: """filter orders by position""" - return tuple( - sorted( - (order for order in self.orders if order.position_id == position), - key=lambda x: x.time_done_msc, - ) - ) + return tuple(sorted((order for order in self.orders if order.position_id == position), key=lambda x: x.time_done_msc)) diff --git a/src/aiomql/lib/order.py b/src/aiomql/lib/order.py index b4af659..56c06f2 100644 --- a/src/aiomql/lib/order.py +++ b/src/aiomql/lib/order.py @@ -25,12 +25,7 @@ class Order(_Base, TradeRequest): type_time (OrderTime.DAY): Order time type_filling (OrderFilling.FOK): Order filling """ - kwargs = { - "action": TradeAction.DEAL, - "type_time": OrderTime.DAY, - "type_filling": OrderFilling.FOK, - **kwargs, - } + kwargs = {"action": TradeAction.DEAL, "type_time": OrderTime.DAY, "type_filling": OrderFilling.FOK, **kwargs} super().__init__(**kwargs) async def orders_total(self): @@ -111,9 +106,7 @@ class Order(_Base, TradeRequest): Returns: float: Returns float value if successful """ - res = await self.mt5.order_calc_margin( - self.type, self.symbol, self.volume, self.price - ) + res = await self.mt5.order_calc_margin(self.type, self.symbol, self.volume, self.price) return res @error_handler(response=0, log_error_msg=False) @@ -124,16 +117,8 @@ class Order(_Base, TradeRequest): float: Returns float value if successful None: If not successful """ - action, symbol, volume, price_open, price_close = ( - self.type, - self.symbol, - self.volume, - self.price, - self.tp, - ) - res = await self.mt5.order_calc_profit( - action, symbol, volume, price_open, price_close - ) + action, symbol, volume, price_open, price_close = (self.type, self.symbol, self.volume, self.price, self.tp) + res = await self.mt5.order_calc_profit(action, symbol, volume, price_open, price_close) return res @error_handler(response=0, log_error_msg=False) @@ -144,23 +129,11 @@ class Order(_Base, TradeRequest): float: Returns float value if successful None: If not successful """ - action, symbol, volume, price_open, price_close = ( - self.type, - self.symbol, - self.volume, - self.price, - self.sl, - ) - res = await self.mt5.order_calc_profit( - action, symbol, volume, price_open, price_close - ) + action, symbol, volume, price_open, price_close = (self.type, self.symbol, self.volume, self.price, self.sl) + res = await self.mt5.order_calc_profit(action, symbol, volume, price_open, price_close) return res @property def request(self) -> dict: """Return the order request as a dictionary.""" - return { - key: value - for key, value in self.dict.items() - if key in self.mt5.TradeRequest.__match_args__ - } + return {key: value for key, value in self.dict.items() if key in self.mt5.TradeRequest.__match_args__} diff --git a/src/aiomql/lib/positions.py b/src/aiomql/lib/positions.py index c30c699..30709af 100644 --- a/src/aiomql/lib/positions.py +++ b/src/aiomql/lib/positions.py @@ -1,4 +1,5 @@ """Handle Open positions.""" + import asyncio from logging import getLogger @@ -70,9 +71,7 @@ class Positions: return tuple(TradePosition(**pos._asdict()) for pos in (positions or ())) @staticmethod - async def close( - *, ticket: int, symbol: str, price: float, volume: float, order_type: OrderType - ) -> OrderSendResult: + async def close(*, 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. Args: @@ -82,14 +81,7 @@ class Positions: volume (float): Volume to close. order_type (OrderType): Order type. """ - order = Order( - action=TradeAction.DEAL, - price=price, - position=ticket, - symbol=symbol, - volume=volume, - type=order_type.opposite, - ) + order = Order(action=TradeAction.DEAL, price=price, position=ticket, symbol=symbol, volume=volume, type=order_type.opposite) return await order.send() async def close_position_by_ticket(self, *, ticket: int) -> OrderSendResult | None: @@ -127,14 +119,5 @@ class Positions: int: Return number of positions closed. """ positions = self.positions or await self.get_positions() - results = await asyncio.gather( - *(self.close_position(position=position) for position in positions), - return_exceptions=True, - ) - return len( - [ - res - for res in results - if (isinstance(res, OrderSendResult) and res.retcode == 10009) - ] - ) + results = await asyncio.gather(*(self.close_position(position=position) for position in positions), return_exceptions=True) + return len([res for res in results if (isinstance(res, OrderSendResult) and res.retcode == 10009)]) diff --git a/src/aiomql/lib/ram.py b/src/aiomql/lib/ram.py index 9ea22a6..9656de6 100644 --- a/src/aiomql/lib/ram.py +++ b/src/aiomql/lib/ram.py @@ -1,4 +1,5 @@ """Risk Assessment and Management""" + from .account import Account from .positions import Positions diff --git a/src/aiomql/lib/result.py b/src/aiomql/lib/result.py index 98495ae..c8a8d10 100644 --- a/src/aiomql/lib/result.py +++ b/src/aiomql/lib/result.py @@ -35,12 +35,8 @@ class Result: self.name = name or self.parameters.get("name", "Trades") def get_data(self) -> dict: - res = self.result.get_dict( - exclude={"retcode", "comment", "retcode_external", "request_id", "request"} - ) - return ( - self.parameters | res | {"actual_profit": 0, "closed": False, "win": False} - ) + res = self.result.get_dict(exclude={"retcode", "comment", "retcode_external", "request_id", "request"}) + return self.parameters | res | {"actual_profit": 0, "closed": False, "win": False} async def save(self, *, trade_record_mode: Literal["csv", "json"] = None): """Record trade results as a csv or json file @@ -71,9 +67,7 @@ class Result: headers.update(data.keys()) read_file.close() with file.open("w", newline="") as write_file: - writer = csv.DictWriter( - write_file, fieldnames=headers, restval=None, extrasaction="ignore" - ) + writer = csv.DictWriter(write_file, fieldnames=headers, restval=None, extrasaction="ignore") writer.writeheader() writer.writerows(rows) except Exception as err: diff --git a/src/aiomql/lib/sessions.py b/src/aiomql/lib/sessions.py index 8c5d063..b33b6c5 100644 --- a/src/aiomql/lib/sessions.py +++ b/src/aiomql/lib/sessions.py @@ -23,12 +23,7 @@ def delta(obj: time) -> timedelta: Args: obj (datetime.time): A datetime.time object. """ - return timedelta( - hours=obj.hour, - minutes=obj.minute, - seconds=obj.second, - microseconds=obj.microsecond, - ) + return timedelta(hours=obj.hour, minutes=obj.minute, seconds=obj.second, microseconds=obj.microsecond) async def backtest_sleep(secs): @@ -58,9 +53,7 @@ class Session: *, start: int | time, end: int | time, - on_start: Literal[ - "close_all", "close_win", "close_loss", "custom_start" - ] = None, + on_start: Literal["close_all", "close_win", "close_loss", "custom_start"] = None, on_end: Literal["close_all", "close_win", "close_loss", "custom_end"] = None, custom_start: 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. name (str): A name for the session. Default is a combination of start and end. """ - self.start = ( - start.replace(tzinfo=UTC) - if isinstance(start, time) - else time(hour=start, tzinfo=UTC) - ) + self.start = 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.on_start = on_start self.on_end = on_end @@ -112,9 +101,7 @@ class Session: now = ( datetime.now(tz=UTC).time() if self.config.mode == "live" - else datetime.fromtimestamp( - self.config.backtest_engine.cursor.time, tz=UTC - ).time() + else datetime.fromtimestamp(self.config.backtest_engine.cursor.time, tz=UTC).time() ) return now in self @@ -133,13 +120,7 @@ class Session: return Duration(hours=hours, minutes=minutes, seconds=seconds) async def close_positions(self, *, positions: tuple[TradePosition, ...]): - results = asyncio.gather( - *( - self.positions_manager.close_position(position=position) - for position in positions - ), - return_exceptions=True, - ) + results = asyncio.gather(*(self.positions_manager.close_position(position=position) for position in positions), return_exceptions=True) closed = pending = 0 for result in results: if isinstance(result, OrderSendResult) and result.retcode == 10009: @@ -155,16 +136,12 @@ class Session: async def close_win(self): open_positions = await self.positions_manager.get_positions() - positions = tuple( - position for position in open_positions if position.profit >= 0 - ) + positions = tuple(position for position in open_positions if position.profit >= 0) await self.close_positions(positions=positions) async def close_loss(self): open_positions = await self.positions_manager.get_positions() - positions = tuple( - position for position in open_positions if position.profit < 0 - ) + positions = tuple(position for position in open_positions if position.profit < 0) await self.close_positions(positions=positions) async def action(self, *, action): @@ -198,9 +175,7 @@ class Session: def until(self): """Get the seconds until the session starts from the current time in seconds.""" if self.config.mode == "backtest": - now = datetime.fromtimestamp( - self.config.backtest_engine.cursor.time, tz=UTC - ).time() + now = datetime.fromtimestamp(self.config.backtest_engine.cursor.time, tz=UTC).time() secs = (delta(self.start) - delta(now)).seconds else: secs = (delta(self.start) - delta(datetime.now(tz=UTC).time())).seconds @@ -237,11 +212,7 @@ class Sessions: moment = ( moment or datetime.now(tz=UTC).time() if self.config.mode == "live" - else ( - datetime.fromtimestamp( - self.config.backtest_engine.cursor.time, tz=UTC - ).time() - ) + else (datetime.fromtimestamp(self.config.backtest_engine.cursor.time, tz=UTC).time()) ) for session in self.sessions: if moment in session: @@ -260,11 +231,7 @@ class Sessions: moment = ( moment or datetime.now(tz=UTC).time() if self.config.mode == "live" - else ( - datetime.fromtimestamp( - self.config.backtest_engine.cursor.time, tz=UTC - ).time() - ) + else (datetime.fromtimestamp(self.config.backtest_engine.cursor.time, tz=UTC).time()) ) for session in self.sessions: if delta(moment) < delta(session.start): @@ -287,9 +254,7 @@ class Sessions: return if self.config.mode == "backtest": - now = datetime.fromtimestamp( - self.config.backtest_engine.cursor.time, tz=UTC - ).time() + now = datetime.fromtimestamp(self.config.backtest_engine.cursor.time, tz=UTC).time() else: now = datetime.now(tz=UTC).time() diff --git a/src/aiomql/lib/strategy.py b/src/aiomql/lib/strategy.py index b4f6dc5..4f078bf 100644 --- a/src/aiomql/lib/strategy.py +++ b/src/aiomql/lib/strategy.py @@ -1,4 +1,5 @@ """The base class for creating strategies.""" + import asyncio from time import time from typing import TypeVar @@ -46,9 +47,7 @@ class Strategy(ABC): backtest_controller = BackTestController current_session = Session - def __init__( - self, *, symbol: Symbol, params: dict = None, sessions: Sessions = None, name="" - ): + def __init__(self, *, symbol: Symbol, params: dict = None, sessions: Sessions = None, name=""): """Initiate the parameters dict and add name and symbol fields. 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["name"] = self.name self.running = True - self.sessions = sessions or Sessions( - sessions=[Session(start=0, end=dtime(hour=23, minute=59, second=59))] - ) + self.sessions = sessions or Sessions(sessions=[Session(start=0, end=dtime(hour=23, minute=59, second=59))]) self.config = Config() self.mt5 = MetaTrader() if self.config.mode != "backtest" else MetaBackTester() self.backtest_controller = BackTestController() @@ -165,9 +162,7 @@ class Strategy(ABC): async def backtest_strategy(self): """Backtest the strategy.""" async with self as _: - logger.info( - "Testing %s strategy on %s with Backtester", self.name, self.symbol.name - ) + logger.info("Testing %s strategy on %s with Backtester", self.name, self.symbol.name) while self.running: try: await self.sessions.check() diff --git a/src/aiomql/lib/symbol.py b/src/aiomql/lib/symbol.py index 7bf1cc1..a67f944 100644 --- a/src/aiomql/lib/symbol.py +++ b/src/aiomql/lib/symbol.py @@ -1,4 +1,5 @@ """Symbol class for handling a financial instrument.""" + from datetime import datetime from logging import getLogger @@ -157,10 +158,7 @@ class Symbol(_Base, SymbolInfo): if check := self.volume_min <= volume <= self.volume_max: return check, volume else: - return ( - check, - self.volume_min if volume <= self.volume_min else self.volume_max, - ) + return (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: """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: """Convert the amount to the quote currency of the symbol.""" if self.currency_profit != self.account.currency: - amount = await self.convert_currency( - amount=amount, - from_currency=self.account.currency, - to_currency=self.currency_profit, - ) + amount = await self.convert_currency(amount=amount, from_currency=self.account.currency, to_currency=self.currency_profit) return amount async def compute_volume(self) -> float: @@ -194,9 +188,7 @@ class Symbol(_Base, SymbolInfo): """ return self.volume_min - async def convert_currency( - self, *, amount: float, from_currency: str, to_currency: str - ) -> float: + async def convert_currency(self, *, amount: float, from_currency: str, to_currency: str) -> float: """Convert a given amount from one currency to the other. Args: amount: Amount to convert @@ -215,14 +207,10 @@ class Symbol(_Base, SymbolInfo): if tick is not None: return round(amount / tick.ask, 2) except Exception as err: - logger.warning( - f"{err}: Currency conversion failed: Unable to convert {amount} in {quote} to {base}" - ) + logger.warning(f"{err}: Currency conversion failed: Unable to convert {amount} in {quote} to {base}") @backoff_decorator - async def copy_rates_from( - self, *, timeframe: TimeFrame, date_from: datetime | int, count: int = 500 - ) -> Candles: + async def copy_rates_from(self, *, timeframe: TimeFrame, date_from: datetime | int, count: int = 500) -> Candles: """ 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}.") @backoff_decorator - async def copy_rates_from_pos( - self, *, timeframe: TimeFrame, count: int = 500, start_position: int = 0 - ) -> Candles: + async def copy_rates_from_pos(self, *, timeframe: TimeFrame, count: int = 500, start_position: int = 0) -> Candles: """Get bars from the MetaTrader 5 terminal starting from the specified index. Args: @@ -265,21 +251,13 @@ class Symbol(_Base, SymbolInfo): Raises: ValueError: If request was unsuccessful and None was returned """ - rates = await self.mt5.copy_rates_from_pos( - self.name, timeframe, start_position, count - ) + rates = await self.mt5.copy_rates_from_pos(self.name, timeframe, start_position, count) if rates is not None: return Candles(data=rates) raise ValueError(f"Could not get rates for {self.name}.") @backoff_decorator - async def copy_rates_range( - self, - *, - timeframe: TimeFrame, - date_from: datetime | int, - date_to: datetime | int, - ) -> Candles: + async def copy_rates_range(self, *, timeframe: TimeFrame, date_from: datetime | int, date_to: datetime | int) -> Candles: """Get bars in the specified date range from the MetaTrader 5 terminal. Args: @@ -299,21 +277,13 @@ class Symbol(_Base, SymbolInfo): Raises: ValueError: If request was unsuccessful and None was returned """ - rates = await self.mt5.copy_rates_range( - symbol=self.name, timeframe=timeframe, date_from=date_from, date_to=date_to - ) + rates = await self.mt5.copy_rates_range(symbol=self.name, timeframe=timeframe, date_from=date_from, date_to=date_to) if rates is not None: return Candles(data=rates) raise ValueError(f"Could not get rates for {self.name}.") @backoff_decorator - async def copy_ticks_from( - self, - *, - date_from: datetime | int, - count: int = 100, - flags: CopyTicks = CopyTicks.ALL, - ) -> Ticks: + async def copy_ticks_from(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. @@ -336,13 +306,7 @@ class Symbol(_Base, SymbolInfo): raise ValueError(f"Could not get ticks for {self.name}.") @backoff_decorator - async def copy_ticks_range( - self, - *, - date_from: datetime | int, - date_to: datetime | int, - flags: CopyTicks = CopyTicks.ALL, - ) -> Ticks: + async def copy_ticks_range(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. Args: diff --git a/src/aiomql/lib/terminal.py b/src/aiomql/lib/terminal.py index bf61159..d380c87 100644 --- a/src/aiomql/lib/terminal.py +++ b/src/aiomql/lib/terminal.py @@ -1,4 +1,5 @@ """Terminal related functions and properties""" + from typing import NamedTuple from logging import getLogger @@ -7,9 +8,7 @@ from ..core.base import _Base logger = getLogger(__name__) -Version = NamedTuple( - "Version", (("version", str), ("build", int), ("release_date", str)) -) +Version = NamedTuple("Version", (("version", str), ("build", int), ("release_date", str))) class Terminal(_Base, TerminalInfo): diff --git a/src/aiomql/lib/ticks.py b/src/aiomql/lib/ticks.py index 9b494d4..8179e02 100644 --- a/src/aiomql/lib/ticks.py +++ b/src/aiomql/lib/ticks.py @@ -38,27 +38,22 @@ class Tick: """Initialize the Tick class. Set attributes from keyword arguments. bid, ask, last and volume must be present""" if not all(key in kwargs for key in ["bid", "ask", "last", "volume"]): - raise ValueError( - "bid, ask, last and volume, time must be present in the keyword arguments" - ) + raise ValueError("bid, ask, last and volume, time must be present in the keyword arguments") self.Index = kwargs.pop("Index", 0) self.time = kwargs.pop("time", time.monotonic()) self.time_msc = int(self.time * 1000) self.set_attributes(**kwargs) def __repr__(self): - return ( - "%(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, - "bid": self.bid, - "ask": self.ask, - "last": self.last, - "volume": self.volume, - "Index": self.Index, - } - ) + return "%(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, + "bid": self.bid, + "ask": self.ask, + "last": self.last, + "volume": self.volume, + "Index": self.Index, + } def __eq__(self, other: Self): return self.time == other.time @@ -148,9 +143,7 @@ class Ticks: def __getattr__(self, item): if item in list(self._data.columns.values): return self._data[item] - raise AttributeError( - f"Attribute {item} not defined on class {self.__class__.__name__}" - ) + raise AttributeError(f"Attribute {item} not defined on class {self.__class__.__name__}") def __getitem__(self, index) -> Tick | Self: if isinstance(index, slice): diff --git a/src/aiomql/lib/trade_records.py b/src/aiomql/lib/trade_records.py index 650a0b0..fcfd214 100644 --- a/src/aiomql/lib/trade_records.py +++ b/src/aiomql/lib/trade_records.py @@ -73,12 +73,7 @@ class TradeRecords: rows = await self.update_rows(rows=rows) with open(file, mode="w", newline="") as fw: - writer = csv.DictWriter( - fw, - fieldnames=reader.fieldnames, - extrasaction="ignore", - restval=None, - ) + writer = csv.DictWriter(fw, fieldnames=reader.fieldnames, extrasaction="ignore", restval=None) writer.writeheader() writer.writerows(rows) except Exception as err: @@ -119,12 +114,7 @@ class TradeRecords: deals = [ deal for deal in deals - if ( - deal.order != deal.position_id - and deal.position_id == order - and deal.entry == 1 - and deal.position_id not in position_ids - ) + if (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) deal = deals[-1] @@ -157,16 +147,12 @@ class TradeRecords: async def update_csv_records(self): """Update csv trade records in the records_dir folder.""" - records = [ - self.read_update_csv(file=record) for record in self.get_csv_records() - ] + records = [self.read_update_csv(file=record) for record in self.get_csv_records()] await asyncio.gather(*records) async def update_json_records(self): """Update json trade records in the records_dir folder.""" - records = [ - self.read_update_json(file=record) for record in self.get_json_records() - ] + records = [self.read_update_json(file=record) for record in self.get_json_records()] await asyncio.gather(*records) async def update_csv_record(self, *, file: Path | str): diff --git a/src/aiomql/lib/trader.py b/src/aiomql/lib/trader.py index 2bce467..b814427 100644 --- a/src/aiomql/lib/trader.py +++ b/src/aiomql/lib/trader.py @@ -60,17 +60,11 @@ class Trader(ABC): sl, tp = pips, pips * (risk_to_reward or self.ram.risk_to_reward) price = self.order.price if self.order.type == OrderType.BUY: - self.order.sl, self.order.tp = round(price - sl, self.symbol.digits), round( - price + tp, self.symbol.digits - ) + self.order.sl, self.order.tp = round(price - sl, self.symbol.digits), round(price + tp, self.symbol.digits) elif self.order.type == OrderType.SELL: - self.order.sl, self.order.tp = round(price + sl, self.symbol.digits), round( - price - tp, self.symbol.digits - ) + self.order.sl, self.order.tp = round(price + sl, self.symbol.digits), round(price - tp, self.symbol.digits) - def set_trade_stop_levels_points( - self, *, points: float, risk_to_reward: float = None - ): + def set_trade_stop_levels_points(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. 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 if self.order.type == OrderType.BUY: - self.order.sl, self.order.tp = round(price - sl, self.symbol.digits), round( - price + tp, digits - ) + self.order.sl, self.order.tp = round(price - sl, self.symbol.digits), round(price + tp, digits) elif self.order.type == OrderType.SELL: - self.order.sl, self.order.tp = round(price + sl, self.symbol.digits), round( - price - tp, digits - ) + self.order.sl, self.order.tp = round(price + sl, self.symbol.digits), round(price - tp, digits) - async def create_order_with_stops( - self, - *, - order_type: OrderType, - sl: float, - tp: float, - amount_to_risk: float = None, - ): + async def create_order_with_stops(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 calculate the volume. @@ -115,18 +98,9 @@ class Trader(ABC): tick = await self.symbol.info_tick() price = tick.ask if order_type == OrderType.BUY else tick.bid volume = await self.symbol.compute_volume_sl(amount=amount, price=price, sl=sl) - self.order.set_attributes( - sl=sl, tp=tp, volume=volume, price=price, type=order_type - ) + self.order.set_attributes(sl=sl, tp=tp, volume=volume, price=price, type=order_type) - async def create_order_with_sl( - self, - *, - order_type: OrderType, - sl: float, - amount_to_risk: float = None, - risk_to_reward: float = None, - ): + async def create_order_with_sl(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. @@ -146,18 +120,9 @@ class Trader(ABC): dtp = dsl * (risk_to_reward or self.ram.risk_to_reward) tp = price + dtp if order_type == OrderType.BUY else price - dtp volume = await self.symbol.compute_volume_sl(amount=amount, price=price, sl=sl) - self.order.set_attributes( - sl=sl, tp=tp, volume=volume, price=price, type=order_type - ) + self.order.set_attributes(sl=sl, tp=tp, volume=volume, price=price, type=order_type) - async def create_order_with_points( - self, - *, - order_type: OrderType, - points: float, - amount_to_risk: float = None, - risk_to_reward: float = None, - ): + async def create_order_with_points(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. Args: @@ -218,9 +183,7 @@ class Trader(ABC): return result @error_handler - async def record_trade( - self, *, result: OrderSendResult, parameters: dict = None, name: str = "" - ): + async def record_trade(self, *, result: OrderSendResult, parameters: dict = None, name: str = ""): """Record the trade in csv or json. Args: result (OrderSendResult): Result of the order send @@ -232,11 +195,7 @@ class Trader(ABC): params = {**parameters} or {} profit = await self.order.calc_profit() params["expected_profit"] = profit - date = ( - datetime.now(tz=UTC) - if self.config.mode == "live" - else datetime.fromtimestamp(self.config.backtest_engine.cursor.time, tz=UTC) - ) + date = 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") res = Result(result=result, parameters=params, name=name) self.config.task_queue.add(item=QueueItem(res.save), must_complete=True) diff --git a/tests/backtest/conftest.py b/tests/backtest/conftest.py index b4952e2..4463682 100644 --- a/tests/backtest/conftest.py +++ b/tests/backtest/conftest.py @@ -32,11 +32,7 @@ async def close_all_positions(): positions = await mt.positions_get() tasks = [] for position in positions: - order_type = ( - mt.ORDER_TYPE_BUY - if position.type == mt.ORDER_TYPE_SELL - else mt.ORDER_TYPE_SELL - ) + order_type = mt.ORDER_TYPE_BUY if position.type == mt.ORDER_TYPE_SELL else mt.ORDER_TYPE_SELL req = { "action": mt.TRADE_ACTION_DEAL, "symbol": position.symbol, @@ -54,9 +50,7 @@ async def close_all_positions(): @pytest.fixture(scope="package", autouse=True) async def config(request): Path("tests/backtest/configs").mkdir(exist_ok=True) - with open("aiomql.json", "r") as fh, open( - "tests/backtest/configs/test2.json", "w" - ) as fh1, open("tests/backtest/test.json", "w") as fh2: + with open("aiomql.json", "r") as fh, open("tests/backtest/configs/test2.json", "w") as fh1, open("tests/backtest/test.json", "w") as fh2: data = json.load(fh) data["mode"] = "backtest" json.dump(data, fh1, indent=2) @@ -77,19 +71,14 @@ async def mt(): @pytest.fixture(scope="package") async def period(): - return { - "start": datetime(2024, 2, 1, hour=8, tzinfo=UTC), - "end": datetime(2024, 2, 7, hour=16, tzinfo=UTC), - } + return {"start": datetime(2024, 2, 1, hour=8, tzinfo=UTC), "end": datetime(2024, 2, 7, hour=16, tzinfo=UTC)} @pytest.fixture(scope="package") async def backtest_engine(period): start = period["start"] end = period["end"] - return BackTestEngine( - start=start, end=end, name="backtest_data", assign_to_config=True, preload=False - ) + return BackTestEngine(start=start, end=end, name="backtest_data", assign_to_config=True, preload=False) @pytest.fixture(scope="function") diff --git a/tests/backtest/integration/test_backtesting.py b/tests/backtest/integration/test_backtesting.py index ef722fd..03c854c 100644 --- a/tests/backtest/integration/test_backtesting.py +++ b/tests/backtest/integration/test_backtesting.py @@ -27,9 +27,7 @@ async def make_buy_sell_orders(): return {"buy": Order(**buy_req), "sell": Order(**sell_req)} -def test_trade_mode( - config, backtest_engine, history, positions, order_sell, order_buy, btc_usd -): +def test_trade_mode(config, backtest_engine, history, positions, order_sell, order_buy, btc_usd): assert config.mode == "backtest" assert isinstance(backtest_engine, BackTestEngine) 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): await backtest_engine.setup_account(balance=100) so_margin = await backtest_engine.order_calc_margin( - action=order_sell.action, - volume=order_sell.volume, - symbol=order_sell.symbol, - price=order_sell.price, + action=order_sell.action, volume=order_sell.volume, symbol=order_sell.symbol, price=order_sell.price ) bo_margin = await backtest_engine.order_calc_margin( - action=order_buy.action, - volume=order_buy.volume, - symbol=order_buy.symbol, - price=order_buy.price, + action=order_buy.action, volume=order_buy.volume, symbol=order_buy.symbol, price=order_buy.price ) total_margin = so_margin + bo_margin await backtest_engine.order_send(request=order_sell.request) await backtest_engine.order_send(request=order_buy.request) # noinspection PyTestUnpassedFixture - assert ( - backtest_engine.positions.margin - == total_margin - == backtest_engine._account.margin - ) + assert backtest_engine.positions.margin == total_margin == backtest_engine._account.margin 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) bo_profit = deal[-1].profit assert len(all_pos) == 1 - assert ( - backtest_engine.positions.margin - == backtest_engine._account.margin - == backtest_engine.positions.margins[so.order] - ) + assert backtest_engine.positions.margin == backtest_engine._account.margin == backtest_engine.positions.margins[so.order] profit = sum([pos.profit for pos in all_pos]) n_balance = backtest_engine._account.balance 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_profit = backtest_engine._account.profit tdata = GetData.load_data(name=config.backtest_dir / f"{backtest_engine.name}.pkl") - new_bte = BackTestEngine( - data=tdata, restart=False, assign_to_config=False, preload=False - ) + new_bte = BackTestEngine(data=tdata, restart=False, assign_to_config=False, preload=False) assert new_bte._account.balance == last_balance assert new_bte._account.equity == last_equity assert new_bte._account.profit == last_profit diff --git a/tests/backtest/unit/test_deals_manager.py b/tests/backtest/unit/test_deals_manager.py index 4f3d13a..ba69b33 100644 --- a/tests/backtest/unit/test_deals_manager.py +++ b/tests/backtest/unit/test_deals_manager.py @@ -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) assert len(deals) <= 2 orders = backtest_engine.deals.get_deals_range(date_from=start, date_to=end) - assert ( - len(orders) - == backtest_engine.deals.history_deals_total(date_from=start, date_to=end) - == len(backtest_engine.deals._data.keys()) - ) + assert len(orders) == backtest_engine.deals.history_deals_total(date_from=start, date_to=end) == len(backtest_engine.deals._data.keys()) diff --git a/tests/backtest/unit/test_order_manager.py b/tests/backtest/unit/test_order_manager.py index 0be0bf3..b5d577e 100644 --- a/tests/backtest/unit/test_order_manager.py +++ b/tests/backtest/unit/test_order_manager.py @@ -1,7 +1,5 @@ # noinspection PyTestUnpassedFixture -async def test_orders_manager( - backtest_engine, sell_order, buy_order, period, positions -): +async def test_orders_manager(backtest_engine, sell_order, buy_order, period, positions): backtest_engine.reset(clear_data=True) await backtest_engine.setup_account(balance=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) assert len(orders) <= 2 orders = backtest_engine.orders.get_orders_range(date_from=start, date_to=end) - assert ( - len(orders) - == backtest_engine.orders.history_orders_total(date_from=start, date_to=end) - == len(backtest_engine.orders._data.keys()) - ) + assert len(orders) == backtest_engine.orders.history_orders_total(date_from=start, date_to=end) == len(backtest_engine.orders._data.keys()) diff --git a/tests/live/conftest.py b/tests/live/conftest.py index 5f48369..4ff4fb4 100644 --- a/tests/live/conftest.py +++ b/tests/live/conftest.py @@ -29,11 +29,7 @@ async def close_all_positions(): positions = await mt.positions_get() tasks = [] for position in positions: - order_type = ( - mt.ORDER_TYPE_BUY - if position.type == mt.ORDER_TYPE_SELL - else mt.ORDER_TYPE_SELL - ) + order_type = mt.ORDER_TYPE_BUY if position.type == mt.ORDER_TYPE_SELL else mt.ORDER_TYPE_SELL req = { "action": mt.TRADE_ACTION_DEAL, "symbol": position.symbol, @@ -51,9 +47,7 @@ async def close_all_positions(): @pytest.fixture(scope="package", autouse=True) async def config(request): Path("tests/live/configs").mkdir(exist_ok=True) - with open("aiomql.json", "r") as fh, open( - "tests/live/configs/test2.json", "w" - ) as fh1, open("tests/live/test.json", "w") as fh2: + with open("aiomql.json", "r") as fh, open("tests/live/configs/test2.json", "w") as fh1, open("tests/live/test.json", "w") as fh2: data = json.load(fh) json.dump(data, fh1, indent=2) json.dump(data, fh2, indent=2) diff --git a/tests/live/integration/test_bot.py b/tests/live/integration/test_bot.py index bbc679d..af471d4 100644 --- a/tests/live/integration/test_bot.py +++ b/tests/live/integration/test_bot.py @@ -6,10 +6,7 @@ from aiomql.contrib.symbols import ForexSymbol async def test_bot(): - logging.basicConfig( - level=logging.INFO, - format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", - ) + logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s") syms = ["BTCUSD", "SOLUSD", "ETHUSD"] symbols = [ForexSymbol(name=sym) for sym in syms] strategies = [Chaos(symbol=symbol, name="test_chaos") for symbol in symbols] diff --git a/tests/live/integration/test_bot_sync.py b/tests/live/integration/test_bot_sync.py index 9bf8a75..b955be2 100644 --- a/tests/live/integration/test_bot_sync.py +++ b/tests/live/integration/test_bot_sync.py @@ -6,10 +6,7 @@ from aiomql.contrib.symbols import ForexSymbol def test_bot_sync(): - logging.basicConfig( - level=logging.INFO, - format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", - ) + logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s") syms = ["BTCUSD", "SOLUSD", "ETHUSD"] symbols = [ForexSymbol(name=sym) for sym in syms] strategies = [Chaos(symbol=symbol, name="test_chaos") for symbol in symbols] diff --git a/tests/live/integration/test_results_records.py b/tests/live/integration/test_results_records.py index 9278bcb..e7a69cf 100644 --- a/tests/live/integration/test_results_records.py +++ b/tests/live/integration/test_results_records.py @@ -36,13 +36,7 @@ class TestRecordsAndResults: async def sell(self, mt): sym = "BTCUSD" sym_info = await mt.symbol_info(sym) - return { - "action": mt.TRADE_ACTION_DEAL, - "symbol": sym, - "volume": sym_info.volume_min, - "type": mt.ORDER_TYPE_SELL, - "price": sym_info.bid, - } + return {"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) async def setup(self, sell, buy, mt): @@ -50,24 +44,11 @@ class TestRecordsAndResults: buy_res_2 = await mt.order_send(buy) sell_res = await mt.order_send(sell) sell_res_2 = await mt.order_send(sell) - buy_res = 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" - ) - 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"), - ) + buy_res = 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") + 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() def test_records_dir(self): @@ -90,9 +71,7 @@ class TestRecordsAndResults: async def test_json_records(self): json_records = self.trade_records.get_json_records() - matched_recs = [ - record for record in json_records if record.match("test_result.json") - ] + matched_recs = [record for record in json_records if record.match("test_result.json")] assert len(matched_recs) == 1 record = matched_recs[0] record_data = json.load(record.open()) @@ -106,9 +85,7 @@ class TestRecordsAndResults: async def test_csv_records(self): csv_records = self.trade_records.get_csv_records() - matched_recs = [ - record for record in csv_records if record.match("test_result.csv") - ] + matched_recs = [record for record in csv_records if record.match("test_result.csv")] assert len(matched_recs) == 1 record = matched_recs[0] record_data = DictReader(record.open()) diff --git a/tests/live/unit/test_backtest_engine.py b/tests/live/unit/test_backtest_engine.py index 9b83a03..22255f0 100644 --- a/tests/live/unit/test_backtest_engine.py +++ b/tests/live/unit/test_backtest_engine.py @@ -14,40 +14,20 @@ class TestBackTestEngine: def setup_class(cls): cls.start = datetime(2024, 2, 1) cls.end = datetime(2024, 2, 7) - cls.g_data = GetData( - start=cls.start, - 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 - ) + cls.g_data = GetData(start=cls.start, 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") async def bte2(self): await self.g_data.get_data() - bte2 = BackTestEngine( - start=self.start, - end=self.end, - data=self.g_data.data, - use_terminal=False, - preload=False, - ) + bte2 = BackTestEngine(start=self.start, end=self.end, data=self.g_data.data, use_terminal=False, preload=False) await bte2.setup_account(balance=100) return bte2 @pytest.fixture(scope="class") async def sell_order(self): sym = await self.bte.get_symbol_info(symbol="BTCUSD") - request = { - "type": OrderType.SELL, - "symbol": "BTCUSD", - "volume": sym.volume_min, - "price": sym.bid, - "action": TradeAction.DEAL, - } + request = {"type": OrderType.SELL, "symbol": "BTCUSD", "volume": sym.volume_min, "price": sym.bid, "action": TradeAction.DEAL} return request @pytest.fixture(scope="class") @@ -67,16 +47,11 @@ class TestBackTestEngine: } return request - def modify_stops(self, order): - ... + def modify_stops(self, order): ... def test_span_and_range(self): - assert self.bte.range == range( - 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.range == range(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 len(self.bte.span) == len(self.bte.range) def test_cursor(self): @@ -158,16 +133,12 @@ class TestBackTestEngine: async def test_get_rates_from(self): start = datetime(2024, 2, 3, 12, 43, tzinfo=UTC) - rates = await self.bte.get_rates_from( - symbol="BTCUSD", timeframe=TimeFrame.H1, date_from=start, count=24 - ) + rates = await self.bte.get_rates_from(symbol="BTCUSD", timeframe=TimeFrame.H1, date_from=start, count=24) assert len(rates) == 24 async def test_get_rates_from_2(self, bte2): start = datetime(2024, 2, 3, 12, 12, tzinfo=UTC) - rates = await bte2.get_rates_from( - symbol="BTCUSD", timeframe=TimeFrame.H1, date_from=start, count=24 - ) + rates = await bte2.get_rates_from(symbol="BTCUSD", timeframe=TimeFrame.H1, date_from=start, count=24) assert len(rates) == 24 async def test_get_rates_from_pos(self): @@ -175,51 +146,37 @@ class TestBackTestEngine: self.bte.go_to(time=now) tf = TimeFrame.H2 start_pos = 2 - rates = await self.bte.get_rates_from_pos( - symbol="BTCUSD", timeframe=tf, start_pos=start_pos, count=24 - ) + rates = await self.bte.get_rates_from_pos(symbol="BTCUSD", timeframe=tf, start_pos=start_pos, count=24) assert len(rates) == 24 - assert int(rates[-1][0]) == round_down( - int(now.replace(hour=now.hour - start_pos).timestamp()), tf.seconds - ) + assert int(rates[-1][0]) == round_down(int(now.replace(hour=now.hour - start_pos).timestamp()), tf.seconds) async def test_get_rates_from_pos2(self, bte2): now = datetime(2024, 2, 4, 12, 15, tzinfo=UTC) bte2.go_to(time=now) tf = TimeFrame.H1 start_pos = 2 - rates = await bte2.get_rates_from_pos( - 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 - ) + rates = await bte2.get_rates_from_pos(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_up(int(now.timestamp()), tf.seconds) - start_pos * tf.seconds assert len(rates) == 24 async def test_get_rates_range(self): start = datetime(2024, 2, 3, 12, tzinfo=UTC) end = datetime(2024, 2, 4, 18, tzinfo=UTC) - rates = await self.bte.get_rates_range( - symbol="BTCUSD", timeframe=TimeFrame.H1, date_from=start, date_to=end - ) + rates = await self.bte.get_rates_range(symbol="BTCUSD", timeframe=TimeFrame.H1, date_from=start, date_to=end) assert len(rates) == 31 assert int(rates[-1][0]) == int(end.timestamp()) async def test_get_rates_range2(self, bte2): start = datetime(2024, 2, 3, 12, tzinfo=UTC) end = datetime(2024, 2, 4, 18, tzinfo=UTC) - rates = await bte2.get_rates_range( - symbol="BTCUSD", timeframe=TimeFrame.H1, date_from=start, date_to=end - ) + rates = await bte2.get_rates_range(symbol="BTCUSD", timeframe=TimeFrame.H1, date_from=start, date_to=end) assert len(rates) == 31 assert int(rates[-1][0]) == int(end.timestamp()) async def test_get_ticks_from(self): start = datetime(2024, 2, 3, 12, tzinfo=UTC) - ticks = await self.bte.get_ticks_from( - symbol="BTCUSD", date_from=start, count=24 - ) + ticks = await self.bte.get_ticks_from(symbol="BTCUSD", date_from=start, count=24) assert len(ticks) == 24 async def test_get_ticks_from2(self, bte2): @@ -230,23 +187,15 @@ class TestBackTestEngine: async def test_get_ticks_range(self): start = datetime(2024, 2, 3, 12, tzinfo=UTC) end = datetime(2024, 2, 3, 15, tzinfo=UTC) - ticks = await self.bte.get_ticks_range( - symbol="BTCUSD", date_from=start, date_to=end - ) - approx_total = ( - end - start - ).total_seconds() // 2 # assuming 2 ticks per second at least + ticks = await self.bte.get_ticks_range(symbol="BTCUSD", date_from=start, date_to=end) + approx_total = (end - start).total_seconds() // 2 # assuming 2 ticks per second at least assert len(ticks) >= approx_total async def test_get_ticks_range2(self, bte2): start = datetime(2024, 2, 3, 12, tzinfo=UTC) end = datetime(2024, 2, 3, 15, tzinfo=UTC) - ticks = await bte2.get_ticks_range( - symbol="BTCUSD", date_from=start, date_to=end - ) - approx_total = ( - end - start - ).total_seconds() // 2 # assuming 2 ticks per second at least + ticks = await bte2.get_ticks_range(symbol="BTCUSD", date_from=start, date_to=end) + approx_total = (end - start).total_seconds() // 2 # assuming 2 ticks per second at least assert len(ticks) >= approx_total async def test_price_tick(self, bte2): @@ -287,22 +236,14 @@ class TestBackTestEngine: tp = sym_info.ask + dsl profit = await self.bte.order_calc_profit( - action=OrderType.BUY, - symbol=sym, - volume=sym_info.volume_min, - price_open=sym_info.ask, - price_close=tp, + action=OrderType.BUY, symbol=sym, volume=sym_info.volume_min, price_open=sym_info.ask, price_close=tp ) assert profit > 0 sym_info2 = await bte2.get_symbol_info(symbol=sym) dsl2 = (sym_info2.trade_stops_level + sym_info2.spread) * 2 * sym_info2.point tp2 = sym_info2.ask + dsl2 profit2 = await bte2.order_calc_profit( - action=OrderType.BUY, - symbol=sym, - volume=sym_info2.volume_min, - price_open=sym_info2.ask, - price_close=tp2, + action=OrderType.BUY, symbol=sym, volume=sym_info2.volume_min, price_open=sym_info2.ask, price_close=tp2 ) assert ceil(profit) == ceil(profit2) @@ -314,20 +255,10 @@ class TestBackTestEngine: bte2.go_to(time=moment) sym = "BTCUSD" sym_info = await self.bte.get_symbol_info(symbol=sym) - margin = await self.bte.order_calc_margin( - action=OrderType.SELL, - symbol=sym, - volume=sym_info.volume_min, - price=sym_info.bid, - ) + margin = await self.bte.order_calc_margin(action=OrderType.SELL, symbol=sym, volume=sym_info.volume_min, price=sym_info.bid) assert margin > 0 sym_info2 = await self.bte.get_symbol_info(symbol=sym) - margin2 = await bte2.order_calc_margin( - action=OrderType.SELL, - symbol=sym, - volume=sym_info2.volume_min, - price=sym_info2.bid, - ) + margin2 = await bte2.order_calc_margin(action=OrderType.SELL, symbol=sym, volume=sym_info2.volume_min, price=sym_info2.bid) assert margin2 > 0 async def test_order_check(self, buy_order, sell_order): diff --git a/tests/live/unit/test_candles.py b/tests/live/unit/test_candles.py index f8825ca..934068a 100644 --- a/tests/live/unit/test_candles.py +++ b/tests/live/unit/test_candles.py @@ -11,12 +11,8 @@ from aiomql.core.constants import TimeFrame class TestCandle: @classmethod def setup_class(cls): - cls.bullish_candle = Candle( - 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.bullish_candle = Candle(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) def test_repr(self): repr_str = repr(self.bearish_candle) diff --git a/tests/live/unit/test_get_data.py b/tests/live/unit/test_get_data.py index 8aed8e8..1adc1b4 100644 --- a/tests/live/unit/test_get_data.py +++ b/tests/live/unit/test_get_data.py @@ -14,13 +14,7 @@ class TestGetData: cls.end = datetime(2024, 2, 2, tzinfo=UTC) cls.symbols = ["BTCUSD", "ETHUSD"] cls.timeframes = [TimeFrame.H1, TimeFrame.H2] - cls.g_data = GetData( - start=cls.start, - end=cls.end, - symbols=cls.symbols, - timeframes=cls.timeframes, - name="test_data", - ) + cls.g_data = GetData(start=cls.start, end=cls.end, symbols=cls.symbols, timeframes=cls.timeframes, name="test_data") @pytest.fixture(scope="class", autouse=True) async def get_data(self): @@ -34,9 +28,7 @@ class TestGetData: assert self.g_data.timeframes == set(self.timeframes) assert self.g_data.name == "test_data" assert self.g_data.range == range(int((self.end - self.start).total_seconds())) - assert self.g_data.span == range( - int(self.start.timestamp()), int(self.end.timestamp()) - ) + assert self.g_data.span == range(int(self.start.timestamp()), int(self.end.timestamp())) async def test_get_data(self): assert self.g_data.data.fully_loaded is True diff --git a/tests/live/unit/test_meta_trader.py b/tests/live/unit/test_meta_trader.py index 01becec..1f9fe16 100644 --- a/tests/live/unit/test_meta_trader.py +++ b/tests/live/unit/test_meta_trader.py @@ -111,19 +111,13 @@ class TestMetaTrader: assert res.shape[0] == 10 async def test_copy_ticks_from(self): - res = await self.mt.copy_ticks_from( - self.symbol, self.start, 10, self.mt.COPY_TICKS_ALL - ) + res = await self.mt.copy_ticks_from(self.symbol, self.start, 10, self.mt.COPY_TICKS_ALL) assert res is not None assert res.shape[0] == 10 async def test_copy_ticks_range(self): - res = await self.mt.copy_ticks_range( - 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 - ) + res = await self.mt.copy_ticks_range(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) assert res is not None assert res.shape[0] == res2.shape[0] @@ -149,9 +143,7 @@ class TestMetaTrader: price_open = buy_order["price"] price_close = buy_order["tp"] type_ = buy_order["type"] - res = await self.mt.order_calc_profit( - type_, self.symbol, volume, price_open, price_close - ) + res = await self.mt.order_calc_profit(type_, self.symbol, volume, price_open, price_close) assert isinstance(res, float) async def test_order_check(self, buy_order): diff --git a/tests/live/unit/test_result.py b/tests/live/unit/test_result.py index c1b7be0..747bafc 100644 --- a/tests/live/unit/test_result.py +++ b/tests/live/unit/test_result.py @@ -36,9 +36,7 @@ class TestResult: async def test_json(self, order_results): res1, res2 = order_results - await asyncio.gather( - res1.save(trade_record_mode="json"), res2.save(trade_record_mode="json") - ) + await asyncio.gather(res1.save(trade_record_mode="json"), res2.save(trade_record_mode="json")) assert res1.config.records_dir.exists() record = res1.config.records_dir / f"{res1.name}.json" assert record.exists() diff --git a/tests/live/unit/test_sessions.py b/tests/live/unit/test_sessions.py index 97ca8cd..4c838bf 100644 --- a/tests/live/unit/test_sessions.py +++ b/tests/live/unit/test_sessions.py @@ -15,9 +15,7 @@ class TestSessions: def make_session(self): 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") - start, end = time(hour=0, tzinfo=UTC), time( - hour=23, minute=59, second=59, tzinfo=UTC - ) + start, end = time(hour=0, tzinfo=UTC), time(hour=23, minute=59, second=59, tzinfo=UTC) 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) over_night = Session(start=18, end=end, name="OverNight", on_end="close_all") diff --git a/tests/live/unit/test_symbol.py b/tests/live/unit/test_symbol.py index 7b4eb9e..40a049f 100644 --- a/tests/live/unit/test_symbol.py +++ b/tests/live/unit/test_symbol.py @@ -35,19 +35,13 @@ class TestSymbol: async def test_rates(self, btc): start = datetime(year=2023, month=10, day=5) end = start + timedelta(hours=9) - rates_from = await btc.copy_rates_from( - timeframe=btc.mt5.TIMEFRAME_H1, date_from=start, count=10 - ) + rates_from = await btc.copy_rates_from(timeframe=btc.mt5.TIMEFRAME_H1, date_from=start, count=10) assert isinstance(rates_from, Candles) assert len(rates_from) == 10 - rates_from_pos = await btc.copy_rates_from_pos( - timeframe=btc.mt5.TIMEFRAME_H1, count=10, start_position=0 - ) + rates_from_pos = await btc.copy_rates_from_pos(timeframe=btc.mt5.TIMEFRAME_H1, count=10, start_position=0) assert isinstance(rates_from_pos, Candles) assert len(rates_from_pos) == 10 - rates_range = await btc.copy_rates_range( - timeframe=btc.mt5.TIMEFRAME_H1, date_from=start, date_to=end - ) + rates_range = await btc.copy_rates_range(timeframe=btc.mt5.TIMEFRAME_H1, date_from=start, date_to=end) assert isinstance(rates_range, Candles) assert len(rates_range) == 10 ticks_from = await btc.copy_ticks_from(date_from=start, count=10) diff --git a/tests/live/unit/test_trader.py b/tests/live/unit/test_trader.py index 3fe9907..08f54f7 100644 --- a/tests/live/unit/test_trader.py +++ b/tests/live/unit/test_trader.py @@ -27,9 +27,7 @@ class TestTrader: assert res.retcode == 10009 async def test_create_order_with_sl(self): - sl = ( - self.trader.symbol.trade_stops_level * 2 + self.trader.symbol.spread - ) * self.trader.symbol.point + sl = (self.trader.symbol.trade_stops_level * 2 + self.trader.symbol.spread) * self.trader.symbol.point tick = await self.trader.symbol.info_tick() sl = tick.bid + 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): points = self.trader.symbol.trade_stops_level * 2 + self.trader.symbol.spread - await self.trader.create_order_with_points( - order_type=OrderType.BUY, points=points - ) + await self.trader.create_order_with_points(order_type=OrderType.BUY, points=points) res = await self.trader.order.send() profit = floor(await self.trader.order.calc_profit()) loss = -floor(abs(await self.trader.order.calc_loss())) @@ -57,16 +53,12 @@ class TestTrader: assert res.retcode == 10009 async def test_create_order_with_stops(self): - sl = ( - self.trader.symbol.trade_stops_level * 2 + self.trader.symbol.spread - ) * self.trader.symbol.point + sl = (self.trader.symbol.trade_stops_level * 2 + self.trader.symbol.spread) * self.trader.symbol.point tp = sl * self.trader.ram.risk_to_reward tick = await self.trader.symbol.info_tick() sl = tick.ask - sl tp = tick.ask + tp - await self.trader.create_order_with_stops( - order_type=OrderType.BUY, sl=sl, tp=tp - ) + await self.trader.create_order_with_stops(order_type=OrderType.BUY, sl=sl, tp=tp) res = await self.trader.order.send() profit = floor(await self.trader.order.calc_profit()) loss = -floor(abs(await self.trader.order.calc_loss()))