diff --git a/README.md b/README.md
index a4cea5f..731ed69 100644
--- a/README.md
+++ b/README.md
@@ -3,6 +3,7 @@


+
### Installation
```bash
pip install aiomql
@@ -11,16 +12,17 @@ pip install aiomql
### Key Features
- Asynchronous Python Library For MetaTrader5
- Asynchronous Bot Building Framework
-- Build bots for trading in different financial markets using a bot factory
+- Build bots for trading in different financial markets.
- Use threadpool executors to run multiple strategies on multiple instruments concurrently
- Records and keep track of trades and strategies in csv files.
- Helper classes for Bot Building. Easy to use and extend.
- Compatible with pandas-ta.
- Sample Pre-Built strategies
-- Visualization of charts using matplotlib and mplfinance
-- Manage Trading periods using Sessions
+- Specify and Manage Trading Sessions
- Risk Management
+- Backtesting Engine
- Run multiple bots concurrently with different accounts from the same broker or different brokers
+- Easy to use and very accurate backtesting engine
### As an asynchronous MetaTrader5 Libray
```python
@@ -31,8 +33,14 @@ from aiomql import MetaTrader
async def main():
mt5 = MetaTrader()
- await mt5.initialize()
- await mt5.login(123456, '*******', 'Broker-Server')
+ res = await mt5.initialize(login=31288540, password='nwa0#anaEze', server='Deriv-Demo')
+ if not res:
+ print('Unable to login and initialize')
+ return
+ # get account information
+ acc = await mt5.account_info()
+ print(acc)
+ # get symbols
symbols = await mt5.symbols_get()
print(symbols)
@@ -40,55 +48,157 @@ asyncio.run(main())
```
### As a Bot Building FrameWork using a Sample Strategy
-***The following code is a sample bot that uses the FingerTrap strategy from the library.\
-It assumes that you have a config file in the same directory as the script.\
-The config file should be named aiomql.json and should contain the login details for your account.\
-It demonstrates the use of sessions and risk management.\
-Sessions allows you to specify the trading period for a strategy. You can also set an action to be performed at the end of a session.\
-Risk Management allows you to manage the risk of a strategy. You can set the risk per trade and the risk to reward ratio.\
-The trader class handles the placing of orders and risk management. It is an attribute of the strategy class.***
+Aiomql allows you to focus on building trading strategies and not worry about the underlying infrastructure.
+It provides a simple and easy to use framework for building bots with rich features and functionalities.
+
```python
from datetime import time
import logging
-from aiomql import Bot, ForexSymbol, FingerTrap, Session, Sessions, RAM, SimpleTrader, TimeFrame
+from aiomql import Bot, ForexSymbol, FingerTrap, Session, Sessions, RAM, SimpleTrader, TimeFrame, Chaos
logging.basicConfig(level=logging.INFO)
def build_bot():
bot = Bot()
-
- # create sessions for the strategies
- london = Session(name='London', start=8, end=time(hour=15, minute=30), on_end='close_all')
- new_york = Session(name='New York', start=13, end=time(hour=20, minute=30))
- tokyo = Session(name='Tokyo', start=23, end=time(hour=6, minute=30))
-
# configure the parameters and the trader for a strategy
- params = {'trend_candles_count': 500, 'fast_period': 8, 'slow_period': 34, 'etf': TimeFrame.M5}
- gbpusd = ForexSymbol(name='GBPUSD')
- st1 = FingerTrap(symbol=gbpusd, params=params, trader=SimpleTrader(symbol=gbpusd, ram=RAM(risk=0.05, risk_to_reward=2)),
- sessions=Sessions(london, new_york))
+ params = {'fast_period': 8, 'slow_period': 34, 'etf': TimeFrame.M5}
+ symbols = ['GBPUSD', 'AUDUSD', 'USDCAD', 'EURGBP', 'EURUSD']
+ symbols = [ForexSymbol(name=sym) for sym in symbols]
+ strategies = [FingerTrap(symbol=sym, params=params)for sym in symbols]
+ bot.add_strategies(strategies)
- # use the default for the other strategies
- st2 = FingerTrap(symbol=ForexSymbol(name='AUDUSD'), sessions=Sessions(tokyo, new_york))
- st3 = FingerTrap(symbol=ForexSymbol(name='USDCAD'), sessions=Sessions(new_york))
- st4 = FingerTrap(symbol=ForexSymbol(name='USDJPY'), sessions=Sessions(tokyo))
- st5 = FingerTrap(symbol=ForexSymbol(name='EURGBP'), sessions=Sessions(london))
+ # create a strategy that uses sessions
+ # sessions are used to specify the trading hours for a particular market
+ # the strategy will only trade during the specified sessions
+ london = Session(name='London', start=time(8, 0), end=time(16, 0))
+ new_york = Session(name='New York', start=time(13, 0), end=time(21, 0))
+ tokyo = Session(name='Tokyo', start=time(0, 0), end=time(8, 0))
- # sessions are not required
- st6 = FingerTrap(symbol=ForexSymbol(name='EURUSD'))
-
- # add strategies to the bot
- bot.add_strategies([st1, st2, st3, st4, st5, st6])
+ sessions = Sessions(sessions=[london, new_york, tokyo])
+ jpy_strategy = Chaos(symbol=ForexSymbol(name='USDJPY'), sessions=sessions)
+ bot.add_strategy(strategy=jpy_strategy)
bot.execute()
# run the bot
build_bot()
```
+
+### Backtesting
+Aiomql provides a very accurate backtesting engine that allows you to test your trading strategies before deploying
+them in the market. The backtest engine prioritizes accuracy over speed, but allows you to increase the speed
+as desired. It is very easy to use and provides a lot of flexibility. The backtester is designed to run strategies
+seamlessly without need for modification of the strategy code. When running in backtest mode all the classes that
+needs to know if they are running in backtest mode will be able to do so and adjust their behavior accordingly.
+
+```python
+from aiomql import MetaBackTester, BackTestEngine, MetaTrader
+import logging
+from datetime import datetime, UTC
+
+from aiomql.lib.backtester import BackTester
+from aiomql.core import Config
+from aiomql.contrib.strategies import FingerTrap
+from aiomql.contrib.symbols import ForexSymbol
+from aiomql.core.backtesting import BackTestEngine
+
+
+def back_tester():
+ 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"]
+ symbols = [ForexSymbol(name=sym) for sym in syms]
+ strategies = [FingerTrap(symbol=symbol) for symbol in symbols]
+
+ # create start time and end time for the backtest
+ start = datetime(2024, 5, 1, tzinfo=UTC)
+ stop_time = datetime(2024, 5, 2, tzinfo=UTC)
+ end = datetime(2024, 5, 7, tzinfo=UTC)
+
+ # create a backtest engine
+ back_test_engine = BackTestEngine(start=start, end=end, speed=3600, stop_time=stop_time,
+ close_open_positions_on_exit=True, assign_to_config=True, preload=True,
+ account_info={"balance": 350})
+ # add it to the backtester
+ backtester = BackTester(backtest_engine=back_test_engine)
+ # add strategies to the backtester
+ backtester.add_strategies(strategies=strategies)
+ backtester.execute()
+
+
+back_tester()
+```
+
+### Writing a Custom Strategy
+Aiomql provides a simple and easy to use framework for building trading strategies. You can easily extend the
+framework to build your own custom strategies. Below is an example of a simple strategy that buys when the fast
+moving average crosses above the slow moving average and sells when the fast moving average crosses below the slow
+moving average.
+
+```python
+# emaxover.py
+from aiomql import Strategy, ForexSymbol, TimeFrame, Tracker, OrderType, Sessions, Trader, ScalpTrader
+
+
+class EMAXOver(Strategy):
+ ttf: TimeFrame # time frame for the strategy
+ tcc: int # how many candles to consider
+ fast_ema: int # fast moving average period
+ slow_ema: int # slow moving average period
+ tracker: Tracker # tracker to keep track of strategy state
+ interval: TimeFrame # intervals to check for entry and exit signals
+ timeout: int # timeout after placing an order in seconds
+
+ # default parameters for the strategy
+ # they are set as attributes. You can override them in the constructor via the params argument.
+ parameters = {'ttf': TimeFrame.H1, 'tcc': 3000, 'fast_ema': 34, 'slow_ema': 55, 'interval': TimeFrame.M15,
+ 'timeout': 3 * 60 * 60}
+
+ def __init__(self, *, symbol: ForexSymbol, params: dict | None = None, trader: Trader = None,
+ sessions: Sessions = None, name: str = "EMAXOver"):
+ super().__init__(symbol=symbol, params=params, sessions=sessions, name=name)
+ self.tracker = Tracker(snooze=self.interval.seconds)
+ self.trader = trader or ScalpTrader(symbol=self.symbol)
+
+ async def find_entry(self):
+ # get the candles
+ candles = await self.symbol.copy_rates_from_pos(timeframe=self.ttf, start_position=0, count=self.tcc)
+
+ # get the fast moving average
+ candles.ta.ema(length=self.fast_ema, append=True)
+ # get the slow moving average
+ candles.ta.ema(length=self.slow_ema, append=True)
+ # rename the columns
+ candles.rename(**{f"EMA_{self.fast_ema}": "fast_ema", f"EMA_{self.slow_ema}": "slow_ema"}, inplace=True)
+
+ # check for crossovers
+ # fast above slow
+ fas = candles.ta_lib.cross(candles.fast_ema, candles.slow_ema, above=True)
+ # fast below slow
+ fbs = candles.ta_lib.cross(candles.fast_ema, candles.slow_ema, above=False)
+
+ ## check for entry signals in the current candle
+ if fas.iloc[-1]:
+ self.tracker.update(order_type=OrderType.BUY, snooze=self.timeout)
+ elif fbs.iloc[-1]:
+ self.tracker.update(order_type=OrderType.SELL, snooze=self.timeout)
+ else:
+ self.tracker.update(order_type=None, snooze=self.interval.seconds)
+
+ async def trade(self):
+ await self.find_entry()
+ if self.tracker.order_type is None:
+ await self.sleep(secs=self.tracker.snooze)
+ else:
+ await self.trader.place_trade(order_type=self.tracker.order_type, parameters=self.parameters)
+ await self.delay(secs=self.tracker.snooze)
+
+```
+
## API Documentation
-see [API Documentation](https://github.com/Ichinga-Samuel/aiomql/tree/master/docs) for more details
+see [API Documentation](docs) for more details
## Contributing
Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.
@@ -96,5 +206,7 @@ Pull requests are welcome. For major changes, please open an issue first to disc
## Support
Feeling generous, like the package or want to see it become a more mature package?
-Consider supporting the project by buying me a coffee.\
+Consider supporting the project by buying me a coffee.
+
+
[](https://www.buymeacoffee.com/ichingasamuel)
diff --git a/backtesting/backtest_data_01_05_24_06_05_24.json b/backtesting/backtest_data_01_05_24_06_05_24.json
index 6b09c90..17d4152 100644
--- a/backtesting/backtest_data_01_05_24_06_05_24.json
+++ b/backtesting/backtest_data_01_05_24_06_05_24.json
@@ -1,17 +1,17 @@
{
- "balance": 434.73,
+ "balance": 221.44,
"profit": 0,
- "equity": 434.73,
+ "equity": 221.44,
"margin": 0.0,
- "margin_free": 434.73,
+ "margin_free": 221.44,
"margin_level": 0,
- "wins": 97,
- "losses": 110,
- "total": 207,
- "win_percentage": 46.86,
- "win": 1101.92,
- "loss": -1017.19,
- "net_profit": 84.73,
- "profit_factor": 1.08,
- "profitability": 24.21
+ "wins": 11,
+ "losses": 54,
+ "total": 65,
+ "win_percentage": 16.92,
+ "win": 85.57,
+ "loss": -214.13,
+ "net_profit": -128.56,
+ "profit_factor": 0.4,
+ "profitability": -36.73
}
\ No newline at end of file
diff --git a/docs/lib/backtester.md b/docs/lib/backtester.md
new file mode 100644
index 0000000..1206d1a
--- /dev/null
+++ b/docs/lib/backtester.md
@@ -0,0 +1,185 @@
+
+
+# backtester
+
+
+
+## BackTester Objects
+
+```python
+class BackTester()
+```
+
+The bot class. Create a bot instance to run your strategies.
+
+**Attributes**:
+
+- `executor` - The default thread executor.
+- `config` _Config_ - Config instance
+- `mt` _MetaBackTester_ - MetaTrader instance
+
+
+
+#### initialize\_sync
+
+```python
+def initialize_sync()
+```
+
+Prepares the bot by signing in to the trading account and initializing the symbols for the trading session.
+Starts the global task queue.
+
+**Raises**:
+
+ SystemExit if sign in was not successful
+
+
+
+#### initialize
+
+```python
+async def initialize()
+```
+
+Prepares the bot by signing in to the trading account and initializing the symbols for the trading session.
+Starts the global task queue.
+
+**Raises**:
+
+ SystemExit if sign in was not successful
+
+
+
+#### add\_coroutine
+
+```python
+def add_coroutine(*,
+ coroutine: Callable[..., ...] | Coroutine,
+ on_separate_thread=False,
+ **kwargs)
+```
+
+Add a coroutine to the executor.
+
+**Arguments**:
+
+- `coroutine` _Coroutine_ - A coroutine to be executed
+- `on_separate_thread` _bool_ - Run the coroutine
+- `**kwargs` _dict_ - keyword arguments for the coroutine
+
+
+
+
+#### execute
+
+```python
+def execute()
+```
+
+Execute the bot.
+
+
+
+#### start
+
+```python
+async def start()
+```
+
+Initialize the bot and execute it. Similar to calling `execute` method but is a coroutine.
+
+
+
+#### add\_strategy
+
+```python
+def add_strategy(*, strategy: Strategy)
+```
+
+Add a strategy to the list of strategies.
+An added strategy will only run if it's symbol was successfully initialized and it is added to the executor.
+
+**Arguments**:
+
+- `strategy` _Strategy_ - A Strategy instance to run on bot
+
+
+**Notes**:
+
+ Make sure the symbol has been added to the market
+
+
+
+#### add\_strategies
+
+```python
+def add_strategies(*, strategies: Iterable[Strategy])
+```
+
+Add multiple strategies at the same time
+
+**Arguments**:
+
+- `strategies` - A list of strategies
+
+
+
+#### add\_strategy\_all
+
+```python
+def add_strategy_all(*,
+ 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.
+
+**Arguments**:
+
+- `strategy` _Strategy_ - Strategy class
+- `params` _dict_ - A dictionary of parameters for the strategy
+- `symbols` _list_ - A list of symbols to run the strategy on
+- `**kwargs` - Additional keyword arguments for the strategy
+
+
+
+#### init\_strategy
+
+```python
+async def init_strategy(*, strategy: Strategy) -> bool
+```
+
+Initialize a single strategy. This method is called internally by the bot.
+
+
+
+#### init\_strategy\_sync
+
+```python
+def init_strategy_sync(*, strategy: Strategy) -> bool
+```
+
+Initialize a single strategy. This method is called internally by the bot.
+
+
+
+#### init\_strategies
+
+```python
+async def init_strategies()
+```
+
+Initialize the symbols for the current trading session. This method is called internally by the bot.
+
+
+
+#### init\_strategies\_sync
+
+```python
+def init_strategies_sync()
+```
+
+Initialize the symbols for the current trading session. This method is called internally by the bot.
+
diff --git a/docs/lib/strategy.md b/docs/lib/strategy.md
index dabdae5..1aedc8a 100644
--- a/docs/lib/strategy.md
+++ b/docs/lib/strategy.md
@@ -3,8 +3,9 @@ The base class for creating strategies.
## Table of Contents
- [Strategy](#strategy.strategy)
-- [\_\_init\_\_](#strategy.__init__)
+- [\__init\__](#strategy.__init__)
- [sleep](#strategy.sleep)
+- [delay](#strategy.delay)
- [live_sleep](#strategy.live_sleep)
- [backtest_sleep](#strategy.backtest_sleep)
- [run_strategy](#strategy.run_strategy)
@@ -54,8 +55,7 @@ Initiate the parameters dict and add name and symbol fields. Use class name as s
### sleep
```python
-@staticmethod
-async def sleep(secs: float)
+async def sleep(*, secs: float)
```
Sleep for the needed amount of seconds in between requests to the terminal.
computes the accurate amount of time needed to sleep ensuring that the next request is made at the start of
@@ -68,6 +68,14 @@ This method calls the `live_sleep` method during live trading or `backtest_sleep
| `secs` | `float` | The time in seconds. Usually the timeframe you are trading on. | None |
+
+### delay
+```python
+async def delay(*, secs: float)
+```
+Sleep for the needed amount of seconds specified in the parameter.
+
+
### live_sleep
```python
diff --git a/examples/emaxover.py b/examples/emaxover.py
new file mode 100644
index 0000000..8d8dc8e
--- /dev/null
+++ b/examples/emaxover.py
@@ -0,0 +1,55 @@
+from aiomql import Strategy, ForexSymbol, TimeFrame, Tracker, OrderType, Sessions, Trader, ScalpTrader
+
+
+class EMAXOver(Strategy):
+ ttf: TimeFrame # time frame for the strategy
+ tcc: int # how many candles to consider
+ fast_ema: int # fast moving average period
+ slow_ema: int # slow moving average period
+ tracker: Tracker # tracker to keep track of strategy state
+ interval: TimeFrame # intervals to check for entry and exit signals
+ timeout: int # timeout after placing an order in seconds
+
+ # default parameters for the strategy
+ # they are set as attributes. You can override them in the constructor via the params argument.
+ parameters = {'ttf': TimeFrame.H1, 'tcc': 3000, 'fast_ema': 34, 'slow_ema': 55, 'interval': TimeFrame.M15,
+ 'timeout': 3 * 60 * 60}
+
+ def __init__(self, *, symbol: ForexSymbol, params: dict | None = None, trader: Trader = None,
+ sessions: Sessions = None, name: str = "EMAXOver"):
+ super().__init__(symbol=symbol, params=params, sessions=sessions, name=name)
+ self.tracker = Tracker(snooze=self.interval.seconds)
+ self.trader = trader or ScalpTrader(symbol=self.symbol)
+
+ async def find_entry(self):
+ # get the candles
+ candles = await self.symbol.copy_rates_from_pos(timeframe=self.ttf, start_position=0, count=self.tcc)
+
+ # get the fast moving average
+ candles.ta.ema(length=self.fast_ema, append=True)
+ # get the slow moving average
+ candles.ta.ema(length=self.slow_ema, append=True)
+ # rename the columns
+ candles.rename(**{f"EMA_{self.fast_ema}": "fast_ema", f"EMA_{self.slow_ema}": "slow_ema"}, inplace=True)
+
+ # check for crossovers
+ # fast above slow
+ fas = candles.ta_lib.cross(candles.fast_ema, candles.slow_ema, above=True)
+ # fast below slow
+ fbs = candles.ta_lib.cross(candles.fast_ema, candles.slow_ema, above=False)
+
+ ## check for entry signals in the current candle
+ if fas.iloc[-1]:
+ self.tracker.update(order_type=OrderType.BUY, snooze=self.timeout)
+ elif fbs.iloc[-1]:
+ self.tracker.update(order_type=OrderType.SELL, snooze=self.timeout)
+ else:
+ self.tracker.update(order_type=None, snooze=self.interval.seconds)
+
+ async def trade(self):
+ await self.find_entry()
+ if self.tracker.order_type is None:
+ await self.sleep(secs=self.tracker.snooze)
+ else:
+ await self.trader.place_trade(order_type=self.tracker.order_type, parameters=self.parameters)
+ await self.delay(secs=self.tracker.snooze)
diff --git a/examples/xover_bot.py b/examples/xover_bot.py
new file mode 100644
index 0000000..9169731
--- /dev/null
+++ b/examples/xover_bot.py
@@ -0,0 +1,19 @@
+import logging
+
+from aiomql import Bot, ForexSymbol
+
+from emaxover import EMAXOver
+
+logging.basicConfig(level=logging.INFO)
+
+
+def x_bot():
+ syms = ["EURUSD", "GBPUSD", "USDJPY"]
+ symbols = [ForexSymbol(name=sym) for sym in syms]
+ strategies = [EMAXOver(symbol=symbol) for symbol in symbols]
+ bot = Bot()
+ bot.add_strategies(strategies=strategies)
+ bot.execute()
+
+
+x_bot()
diff --git a/ruff.toml b/ruff.toml
deleted file mode 100644
index a4248c3..0000000
--- a/ruff.toml
+++ /dev/null
@@ -1,2 +0,0 @@
-line-length = 150
-target-version = "py311"
diff --git a/sample_backtest.py b/sample_backtester.py
similarity index 58%
rename from sample_backtest.py
rename to sample_backtester.py
index 9479cda..2aab074 100644
--- a/sample_backtest.py
+++ b/sample_backtester.py
@@ -1,37 +1,28 @@
-import asyncio
import logging
from datetime import datetime, UTC
from aiomql.lib.backtester import BackTester
from aiomql.core import Config
-from aiomql.contrib.strategies import Chaos
+from aiomql.contrib.strategies import FingerTrap
from aiomql.contrib.symbols import ForexSymbol
from aiomql.core.backtesting import BackTestEngine
-async def back_tester():
- config = Config()
- config.mode = "backtest"
+def back_tester():
+ 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"]
symbols = [ForexSymbol(name=sym) for sym in syms]
- strategies = [Chaos(symbol=symbol) for symbol in symbols]
+ strategies = [FingerTrap(symbol=symbol) for symbol in symbols]
start = datetime(2024, 5, 1, tzinfo=UTC)
stop_time = datetime(2024, 5, 2, tzinfo=UTC)
end = datetime(2024, 5, 7, tzinfo=UTC)
- back_test_engine = BackTestEngine(
- start=start,
- end=end,
- speed=3600,
- stop_time=stop_time,
- close_open_positions_on_exit=True,
- assign_to_config=True,
- preload=True,
- account_info={"balance": 350},
- )
+ back_test_engine = BackTestEngine(start=start, end=end, speed=3600, stop_time=stop_time,
+ close_open_positions_on_exit=True, assign_to_config=True, preload=True,
+ account_info={"balance": 350})
backtester = BackTester(backtest_engine=back_test_engine)
backtester.add_strategies(strategies=strategies)
- await backtester.start()
+ backtester.execute()
-asyncio.run(back_tester())
+back_tester()
diff --git a/sample_bot.py b/sample_bot.py
index 4deda7c..74a6fe1 100644
--- a/sample_bot.py
+++ b/sample_bot.py
@@ -1,19 +1,18 @@
import logging
from aiomql.lib.bot import Bot
-from aiomql.contrib.strategies import Chaos
+from aiomql.contrib.strategies import FingerTrap
from aiomql.contrib.symbols import ForexSymbol
-def chaos_bot():
+def sample_bot():
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]
+ strategies = [FingerTrap(symbol=symbol) for symbol in symbols]
bot = Bot()
- bot.executor.timeout = 10
bot.add_strategies(strategies=strategies)
bot.execute()
-chaos_bot()
+sample_bot()
diff --git a/setup.py b/setup.py
index 6068493..a59fd83 100644
--- a/setup.py
+++ b/setup.py
@@ -1,3 +1,3 @@
from setuptools import setup
-setup()
+setup
diff --git a/src/aiomql/contrib/__init__.py b/src/aiomql/contrib/__init__.py
index b573057..a39d3d9 100644
--- a/src/aiomql/contrib/__init__.py
+++ b/src/aiomql/contrib/__init__.py
@@ -1,3 +1,5 @@
from .strategies import *
from .candle_patterns import *
from .symbols import *
+from .utils import *
+from .traders import *
diff --git a/src/aiomql/contrib/strategies/chaos.py b/src/aiomql/contrib/strategies/chaos.py
index f19c1c9..d67529a 100644
--- a/src/aiomql/contrib/strategies/chaos.py
+++ b/src/aiomql/contrib/strategies/chaos.py
@@ -30,7 +30,11 @@ class Chaos(Strategy):
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:
+ 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)
diff --git a/src/aiomql/contrib/strategies/finger_trap.py b/src/aiomql/contrib/strategies/finger_trap.py
index 29c4036..888714d 100644
--- a/src/aiomql/contrib/strategies/finger_trap.py
+++ b/src/aiomql/contrib/strategies/finger_trap.py
@@ -19,14 +19,18 @@ class FingerTrap(Strategy):
fast_ema: int
slow_ema: int
entry_ema: int
- parameters: dict
ecc: int
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}
- def __init__(self, *, symbol: Symbol, params: dict | None = None, trader: Trader = None, sessions: Sessions = None, name: str = "FingerTrap"):
+ # The default parameters for the strategy. You can override these in the constructor.
+ # via the `params` argument.
+ parameters = {"fast_ema": 8, "slow_ema": 20, "etf": TimeFrame.M5, "ttf": TimeFrame.H1,
+ "entry_ema": 5, "tcc": 720, "ecc": 8640}
+
+ 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)
@@ -37,6 +41,7 @@ class FingerTrap(Strategy):
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)
candles.ta.ema(length=self.slow_ema, append=True, fillna=0)
candles.ta.ema(length=self.fast_ema, append=True, fillna=0)
@@ -46,14 +51,17 @@ class FingerTrap(Strategy):
fbs = candles.ta_lib.below(candles.fast, candles.slow)
caf = candles.ta_lib.above(candles.close, candles.fast)
cbf = candles.ta_lib.below(candles.close, candles.fast)
- current = candles[-2]
- if fas.iloc[-1] and caf.iloc[-1] and current.is_bullish():
+
+ if fas.iloc[-1] and caf.iloc[-1]:
self.tracker.update(trend="bullish")
- elif fbs.iloc[-1] and cbf.iloc[-1] and current.is_bearish():
+
+ elif fbs.iloc[-1] and cbf.iloc[-1]:
self.tracker.update(trend="bearish")
+
else:
self.tracker.update(trend="ranging", snooze=self.ttf.seconds, order_type=None)
- self.tracker.update(trend="bullish") # remove this line
+
+ self.tracker.update(trend="bullish") #Todo remove this line
except Exception as err:
logger.error(f"{err} for {self.symbol} in {self.__class__.__name__}.check_trend")
self.tracker.update(snooze=self.ttf.seconds, order_type=None)
@@ -64,16 +72,18 @@ class FingerTrap(Strategy):
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, entry_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)
current = candles[-1]
- if self.tracker.bullish and True or current.cae: # change True to current.cae
+
+ if True or self.tracker.bullish and current.cae:
sl = find_bullish_fractal(candles).low
self.tracker.update(snooze=self.ttf.seconds, order_type=OrderType.BUY, sl=sl)
- elif self.tracker.bearish and current.cbe:
+ elif True or 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)
else:
@@ -84,11 +94,10 @@ class FingerTrap(Strategy):
async def watch_market(self):
await self.check_trend()
- if not self.tracker.ranging:
+ if self.tracker.ranging is False:
await self.confirm_trend()
async def trade(self):
- logger.info(f"Trading {self.symbol}")
try:
await self.watch_market()
if self.tracker.new is False:
@@ -97,7 +106,8 @@ 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 b4c2ac7..7366aec 100644
--- a/src/aiomql/contrib/symbols/forex_symbol.py
+++ b/src/aiomql/contrib/symbols/forex_symbol.py
@@ -17,7 +17,7 @@ class ForexSymbol(Symbol):
def compute_points(self, *, amount: float, volume: float) -> float:
"""Compute the number of points required for a trade. Given the amount and the volume of the trade.
-
+
Args:
amount (float): Amount to trade
volume (float): Volume to trade
@@ -38,13 +38,13 @@ class ForexSymbol(Symbol):
async def compute_volume_sl(self, *, amount: float, price: float, sl: float, round_down: bool = False) -> float:
"""Compute the volume required for a trade. Given the amount, the price and the stop loss.
-
+
Args:
amount (float): Amount to trade
price (float): The price of the trade
sl (float): The stop loss of the trade
round_down (bool): round down the computed volume to the nearest step default to False
-
+
Returns:
float: The volume required for the trade
"""
diff --git a/src/aiomql/contrib/traders/__init__.py b/src/aiomql/contrib/traders/__init__.py
index 07af2a2..d2fde22 100644
--- a/src/aiomql/contrib/traders/__init__.py
+++ b/src/aiomql/contrib/traders/__init__.py
@@ -1 +1,2 @@
from .simple_trader import SimpleTrader
+from .scalp_trader import ScalpTrader
diff --git a/src/aiomql/contrib/traders/simple_trader.py b/src/aiomql/contrib/traders/simple_trader.py
index d0308f1..0b7a22e 100644
--- a/src/aiomql/contrib/traders/simple_trader.py
+++ b/src/aiomql/contrib/traders/simple_trader.py
@@ -8,8 +8,8 @@ logger = getLogger(__name__)
class SimpleTrader(Trader):
async def place_trade(self, *, order_type: OrderType, sl: float, parameters: dict = None):
- """Places a trade based on the order_type and a given stop_loss. The volume is based on the amount to risk which is
- calculated using the Risk Assessment Management instance.
+ """Places a trade based on the order_type and a given stop_loss. The volume is based on the amount to risk which is
+ calculated using the Risk Assessment Management instance.
Args:
order_type (OrderType): The order_type
diff --git a/src/aiomql/core/backtesting/backtest_account.py b/src/aiomql/core/backtesting/backtest_account.py
index f6734bc..86c9e06 100644
--- a/src/aiomql/core/backtesting/backtest_account.py
+++ b/src/aiomql/core/backtesting/backtest_account.py
@@ -7,6 +7,7 @@ from ..constants import AccountTradeMode, AccountMarginMode, AccountStopOutMode
@dataclass
class BackTestAccount:
"""Account data for backtesting"""
+
login: int = 0
trade_mode: AccountTradeMode = AccountTradeMode.DEMO
leverage: float = 1
@@ -40,7 +41,7 @@ class BackTestAccount:
def get_dict(self, exclude: set = None, include: set = None):
"""Returns a dictionary of the account data. Using the exclude and include arguments, you can filter the data
-
+
Args:
exclude (set): A set of keys to exclude
include (set): A set of keys to include
diff --git a/src/aiomql/core/backtesting/backtest_controller.py b/src/aiomql/core/backtesting/backtest_controller.py
index 995d80c..34eadf6 100644
--- a/src/aiomql/core/backtesting/backtest_controller.py
+++ b/src/aiomql/core/backtesting/backtest_controller.py
@@ -23,12 +23,12 @@ class BackTestController:
tasks (list[Task]): The tasks that are being run
barrier (Barrier): The barrier for synchronizing the tasks
"""
+
_instance: Self
config: Config
tasks: list[Task]
barrier: Barrier
-
def __new__(cls, *args, **kwargs):
if not hasattr(cls, "_instance"):
cls._instance = super().__new__(cls)
@@ -76,15 +76,20 @@ class BackTestController:
while True:
pending = self.wait()
# all main tasks have been completed in the current cycle
- if pending == 0:
+ if pending == 0:
await self.backtest_engine.tracker()
self.backtest_engine.next()
# gives an output every 6 hours
if self.backtest_engine.cursor.time % (3600 * 6) == 0:
- logger.info(datetime.strftime(datetime.fromtimestamp(self.backtest_engine.cursor.time), "%Y-%m-%d %H:%M:%S"))
+ 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 c4ad453..8a6f566 100644
--- a/src/aiomql/core/backtesting/backtest_engine.py
+++ b/src/aiomql/core/backtesting/backtest_engine.py
@@ -25,7 +25,18 @@ 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
@@ -105,7 +116,7 @@ class BackTestEngine:
name (str, optional): The name of the backtest. Defaults to "". If not provided,
it is generated from the start and end times.
-
+
stop_time (float | datetime, optional): The time to stop the backtest. Defaults to None.
If a float is passed, it is assumed to be a timestamp. If not given it is assumed to be the end of the backtest range.
@@ -154,14 +165,20 @@ 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 = (
+ (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.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
@@ -183,20 +200,21 @@ 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
+ ):
"""Setup the test range for the backtest engine. This is used to set the range of the backtest and the speed
at which it runs.
-
+
Args:
start (float | datetime, optional): The start time of the backtest. Defaults to None. If a float is passed,
it is assumed to be a timestamp.
-
+
end (float | datetime, optional): The end time of the backtest. Defaults to None. If a float is passed,
it is assumed to be a timestamp.
-
+
speed (int, optional): The speed of the backtest. Defaults to 60.
-
+
restart (bool, optional): Whether to restart the backtest. Defaults to True.
This is useful when resuming a backtest using a saved BackTestData.
"""
@@ -229,7 +247,7 @@ class BackTestEngine:
Args:
restart (bool, optional): Whether to restart the data. Defaults to True.
- """
+ """
if restart is True:
self.orders = OrdersManager()
self.positions = PositionsManager()
@@ -245,7 +263,9 @@ class BackTestEngine:
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)
+ self.positions = PositionsManager(
+ data=positions, open_positions=self._data.open_positions, margins=self._data.margins
+ )
deals = {}
for ticket, deal in self._data.deals.items():
@@ -261,7 +281,7 @@ class BackTestEngine:
@property
def data(self):
"""The BackTestData instance used for the backtest. If not provided, a new instance is created,
- and the data is made persistent when the backtest is stopped."""
+ and the data is made persistent when the backtest is stopped."""
return self._data
def reset(self, clear_data: bool = False):
@@ -346,7 +366,7 @@ class BackTestEngine:
@error_handler
async def wrap_up(self):
"""Wraps up the backtest. This is called at the end of testing to save the results and close all open
- positions."""
+ positions."""
if self.close_open_positions_on_exit:
await self.close_all_open()
self.save_result_to_json()
@@ -484,10 +504,18 @@ 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)
@@ -582,7 +610,9 @@ 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):
@@ -597,10 +627,14 @@ class BackTestEngine:
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.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)
@@ -766,7 +800,11 @@ class BackTestEngine:
osr["retcode"] = 10018
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"))
@@ -820,7 +858,9 @@ class BackTestEngine:
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})
+ 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:
@@ -830,7 +870,9 @@ class BackTestEngine:
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})
+ 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):
@@ -914,7 +956,9 @@ 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__))
@@ -949,7 +993,9 @@ class BackTestEngine:
# 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)
+ 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__))
@@ -1002,7 +1048,13 @@ class BackTestEngine:
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__))
@@ -1117,7 +1169,9 @@ 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:
+ async def get_rates_from(
+ self, *, symbol: str, timeframe: TimeFrame, date_from: datetime | float, count: int
+ ) -> np.ndarray:
"""Get rates from a specific date to the current date. Used by the backtester to get rates for a symbol
Args:
@@ -1129,7 +1183,11 @@ class BackTestEngine:
Returns:
np.ndarray: An array of rates
"""
- date_from = date_from.astimezone(tz=UTC) if isinstance(date_from, datetime) else datetime.fromtimestamp(date_from, tz=UTC)
+ 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
@@ -1171,8 +1229,9 @@ class BackTestEngine:
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:
+ async def get_rates_range(
+ self, *, symbol: str, timeframe: TimeFrame, date_from: datetime | float, date_to: datetime | float
+ ) -> np.ndarray:
"""Get rates within a specific date range. Used by the backtester to get rates for a symbol
Args:
@@ -1184,8 +1243,14 @@ class BackTestEngine:
Returns:
np.ndarray: An array of rates
"""
- 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:
rates = await self.mt5.copy_rates_range(symbol, timeframe, date_from, date_to)
return rates
@@ -1197,8 +1262,9 @@ class BackTestEngine:
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:
+ async def get_ticks_from(
+ self, *, symbol: str, date_from: datetime | float, count: int, flags: CopyTicks = CopyTicks.ALL
+ ) -> np.ndarray:
"""Get a specified number of ticks counting from a specific date.
Args:
symbol (str): The symbol to get ticks for
@@ -1209,7 +1275,11 @@ class BackTestEngine:
Returns:
np.ndarray: An array of ticks
"""
- date_from = date_from.astimezone(tz=UTC) if isinstance(date_from, datetime) else datetime.fromtimestamp(date_from, tz=UTC)
+ 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
@@ -1234,8 +1304,14 @@ class BackTestEngine:
Returns:
np.ndarray: An array of ticks
"""
- 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
@@ -1248,8 +1324,14 @@ class BackTestEngine:
@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,
+ ):
"""Calculate the margin required for a trade.
Args:
@@ -1275,8 +1357,15 @@ class BackTestEngine:
@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,
+ ):
"""
Calculate the profit for a trade.
@@ -1300,7 +1389,11 @@ class BackTestEngine:
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
@@ -1368,8 +1461,14 @@ 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) -> tuple[TradeOrder, ...]:
+ 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.
Args:
@@ -1382,8 +1481,9 @@ 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:
@@ -1400,8 +1500,14 @@ 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) -> tuple[TradeDeal, ...]:
+ 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.
Args:
@@ -1414,5 +1520,6 @@ 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 61ff7ff..57f1bf7 100644
--- a/src/aiomql/core/backtesting/get_data.py
+++ b/src/aiomql/core/backtesting/get_data.py
@@ -19,6 +19,7 @@ logger = getLogger(__name__)
class Cursor(NamedTuple):
"""A cursor to iterate over the data. Marks the current position."""
+
index: int
time: int
@@ -45,6 +46,7 @@ class BackTestData:
margins (dict): The margins data.
fully_loaded (bool): A flag to indicate if the data is fully loaded
"""
+
name: str = ""
terminal: dict[str, [str | int | bool | float]] = field(default_factory=dict)
version: tuple[int, int, str] = (0, 0, "")
@@ -93,10 +95,12 @@ class GetData:
mt5 (MetaTrader): The MetaTrader5 instance.
task_queue (TaskQueue): The task queue to handle the requests.
"""
+
data: BackTestData
- def __init__(self, *, start: datetime, end: datetime, symbols: Iterable[str],
- timeframes: Iterable[TimeFrame], name: str = ""):
+ def __init__(
+ self, *, start: datetime, end: datetime, symbols: Iterable[str], timeframes: Iterable[TimeFrame], name: str = ""
+ ):
"""
Get the backtesting data from the MetaTrader5 terminal.
@@ -170,7 +174,11 @@ 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]
@@ -214,12 +222,18 @@ class GetData:
self.data.set_attrs(account=res)
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):
[
diff --git a/src/aiomql/core/backtesting/trades_manager.py b/src/aiomql/core/backtesting/trades_manager.py
index 94ffdca..2f5f2da 100644
--- a/src/aiomql/core/backtesting/trades_manager.py
+++ b/src/aiomql/core/backtesting/trades_manager.py
@@ -42,6 +42,7 @@ class TradeManager(Generic[TradeData]):
>>> pos in manager
False
"""
+
_data: dict[int, TradeData]
def __init__(self, *, data: dict = None):
@@ -112,6 +113,7 @@ class PositionsManager(TradeManager):
_open_positions (set[int]): The open positions.
margins (dict[int, float]): The margins of the open positions.
"""
+
_data: dict[int, TradePosition]
_open_positions: set[int]
margins: dict[int, float]
@@ -240,8 +242,9 @@ class PositionsManager(TradeManager):
class OrdersManager(TradeManager):
"""Managers orders data during a backtest. It is a subclass of TradeManager. It manages access to the historical
- orders data
+ orders data
"""
+
_data = dict[int, TradeOrder]
def get_orders_range(self, *, date_from: float, date_to: float) -> tuple[TradeData, ...]:
@@ -258,8 +261,15 @@ class OrdersManager(TradeManager):
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)
- def history_orders_get(self, *, date_from: float | datetime = None, date_to: float | datetime = None,
- group: str = "", ticket: int = None, position: int = None) -> tuple[TradeOrder, ...]:
+ def history_orders_get(
+ self,
+ *,
+ date_from: float | datetime = None,
+ date_to: float | datetime = None,
+ group: str = "",
+ ticket: int = None,
+ position: int = None,
+ ) -> tuple[TradeOrder, ...]:
"""Get historical orders. Given the start and end date of the range, the group, ticket, or position of the
orders.
@@ -272,7 +282,7 @@ class OrdersManager(TradeManager):
Returns:
tuple[TradeOrder, ...]: The historical orders.
- """
+ """
if date_from and date_to:
orders = self.get_orders_range(date_from=date_from, date_to=date_to)
if group:
@@ -314,8 +324,15 @@ class DealsManager(TradeManager):
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) -> tuple[TradeDeal, ...]:
+ def history_deals_get(
+ self,
+ *,
+ date_from: float | datetime = None,
+ date_to: float | datetime = None,
+ group: str = "",
+ ticket: int = None,
+ position: int = None,
+ ) -> tuple[TradeDeal, ...]:
"""History deals get. Given the start and end date of the range, the group, ticket, or position of the deals.
Args:
diff --git a/src/aiomql/core/base.py b/src/aiomql/core/base.py
index 6b2bee5..cccd875 100644
--- a/src/aiomql/core/base.py
+++ b/src/aiomql/core/base.py
@@ -35,7 +35,11 @@ class Base:
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:])
return "%(class)s(%(args)s)" % {"class": self.__class__.__name__, "args": args}
@@ -120,7 +124,11 @@ 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)
@@ -129,6 +137,7 @@ class _Base(Base):
"""Base class that provides access to the MetaTrader and Config classes as well as the MetaBackTester class for
backtesting mode.
"""
+
def __init__(self, **kwargs):
self.config = Config()
self.mt5 = MetaTrader() if self.config.mode != "backtest" else MetaBackTester()
diff --git a/src/aiomql/core/config.py b/src/aiomql/core/config.py
index 9429712..a4b774c 100644
--- a/src/aiomql/core/config.py
+++ b/src/aiomql/core/config.py
@@ -46,6 +46,7 @@ class Config:
is provided, this includes the config file, the records_dir and the backtest_dir attributes.
The root directory is used to locate the config file and to set the records_dir and backtest_dir attributes.
"""
+
login: int
trade_record_mode: Literal["csv", "json"]
password: str
@@ -190,7 +191,9 @@ class Config:
if 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 14cfd7e..a749d7a 100644
--- a/src/aiomql/core/meta_backtester.py
+++ b/src/aiomql/core/meta_backtester.py
@@ -3,7 +3,17 @@ 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
@@ -43,7 +53,14 @@ 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)
@@ -51,7 +68,14 @@ class MetaBackTester(MetaTrader):
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)
@@ -103,28 +127,46 @@ 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:
+ 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)
@@ -142,7 +184,9 @@ class MetaBackTester(MetaTrader):
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
)
@@ -162,7 +206,9 @@ 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:
+ 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)
@@ -172,9 +218,20 @@ class MetaBackTester(MetaTrader):
@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)
@@ -184,8 +241,19 @@ class MetaBackTester(MetaTrader):
@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 63f0384..4f10745 100644
--- a/src/aiomql/core/meta_trader.py
+++ b/src/aiomql/core/meta_trader.py
@@ -5,8 +5,18 @@ from typing import Literal, Self
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
@@ -105,7 +115,13 @@ 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.
@@ -147,7 +163,13 @@ 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.
@@ -227,7 +249,11 @@ class MetaTrader(MetaCore):
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
@@ -242,22 +268,40 @@ class MetaTrader(MetaCore):
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
@@ -270,18 +314,36 @@ 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
@@ -296,13 +358,24 @@ class MetaTrader(MetaCore):
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,
@@ -334,29 +407,57 @@ class MetaTrader(MetaCore):
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."}
+ 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}
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"}
+ 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}
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/task_queue.py b/src/aiomql/core/task_queue.py
index c9d381d..9786cfc 100644
--- a/src/aiomql/core/task_queue.py
+++ b/src/aiomql/core/task_queue.py
@@ -27,8 +27,9 @@ class QueueItem:
if asyncio.iscoroutinefunction(self.task_item):
await 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"
- f" {self.kwargs}")
+ logger.error(
+ f"Error {err} occurred in {self.task_item.__name__} with args {self.args} and kwargs" f" {self.kwargs}"
+ )
class TaskQueue:
@@ -57,9 +58,16 @@ class TaskQueue:
- `priority_tasks` (set): A set to store the QueueItems that must complete before the queue stops.
"""
- def __init__(self, size: int = 0, workers: int = 10, timeout: int = None, queue: asyncio.Queue = None,
- on_exit: Literal["cancel", "complete_priority"] = "complete_priority",
- mode: Literal["finite", "infinite"] = "infinite", worker_timeout: int = 60):
+ def __init__(
+ self,
+ size: int = 0,
+ workers: int = 10,
+ timeout: int = None,
+ queue: asyncio.Queue = None,
+ on_exit: Literal["cancel", "complete_priority"] = "complete_priority",
+ mode: Literal["finite", "infinite"] = "infinite",
+ worker_timeout: int = 60,
+ ):
self.queue = queue or asyncio.PriorityQueue(maxsize=size)
self.workers = workers
self.tasks = []
@@ -140,7 +148,9 @@ class TaskQueue:
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 _:
diff --git a/src/aiomql/lib/backtester.py b/src/aiomql/lib/backtester.py
index 68e330d..33c2638 100644
--- a/src/aiomql/lib/backtester.py
+++ b/src/aiomql/lib/backtester.py
@@ -142,7 +142,9 @@ 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:
diff --git a/src/aiomql/lib/bot.py b/src/aiomql/lib/bot.py
index cf9dca2..e61002d 100644
--- a/src/aiomql/lib/bot.py
+++ b/src/aiomql/lib/bot.py
@@ -1,4 +1,5 @@
import asyncio
+import time
from concurrent.futures import ProcessPoolExecutor
from typing import Type, Iterable, Callable, Coroutine
import logging
@@ -66,7 +67,9 @@ class Bot:
self.add_coroutine(coroutine=self.executor.exit)
if len(self.executor.strategy_runners) == 0:
- logger.warning("No strategies were added to the bot")
+ logger.warning("No strategies were added to the bot. Exiting after 10 seconds")
+ await asyncio.sleep(10)
+ raise SystemExit
except Exception as err:
logger.error("%s: Bot initialization failed", err)
raise SystemExit
@@ -90,7 +93,8 @@ class Bot:
self.add_coroutine(coroutine=self.executor.exit)
if len(self.executor.strategy_runners) == 0:
- logger.warning("No strategies were added to the bot")
+ logger.warning("No strategies were added to the bot. Exiting after 10 seconds")
+ time.sleep(10)
except Exception as err:
logger.error("%s: Bot initialization failed", err)
raise SystemExit
@@ -146,7 +150,9 @@ 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:
diff --git a/src/aiomql/lib/candle.py b/src/aiomql/lib/candle.py
index 80bd836..f6fa323 100644
--- a/src/aiomql/lib/candle.py
+++ b/src/aiomql/lib/candle.py
@@ -55,15 +55,18 @@ 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
diff --git a/src/aiomql/lib/history.py b/src/aiomql/lib/history.py
index 240b128..d3c9381 100644
--- a/src/aiomql/lib/history.py
+++ b/src/aiomql/lib/history.py
@@ -34,7 +34,9 @@ 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
@@ -122,4 +124,6 @@ class History:
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/positions.py b/src/aiomql/lib/positions.py
index 30709af..4c9e934 100644
--- a/src/aiomql/lib/positions.py
+++ b/src/aiomql/lib/positions.py
@@ -81,7 +81,14 @@ 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:
@@ -119,5 +126,7 @@ 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)
+ 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/sessions.py b/src/aiomql/lib/sessions.py
index b33b6c5..259918f 100644
--- a/src/aiomql/lib/sessions.py
+++ b/src/aiomql/lib/sessions.py
@@ -120,7 +120,10 @@ 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:
diff --git a/src/aiomql/lib/strategy.py b/src/aiomql/lib/strategy.py
index 4f078bf..b5f84c4 100644
--- a/src/aiomql/lib/strategy.py
+++ b/src/aiomql/lib/strategy.py
@@ -117,11 +117,15 @@ class Strategy(ABC):
else:
await self.live_sleep(secs=secs)
- async def backtest_sleep(self, *, secs: float):
+ async def delay(self, *, secs: float):
+ """Sleep for the input amount of seconds"""
+ if self.config.mode == "backtest":
+ await self._backtest_sleep(secs=secs)
+ else:
+ await asyncio.sleep(secs)
+
+ async def _backtest_sleep(self, *, secs: float):
try:
- _time = self.config.backtest_engine.cursor.time
- mod = _time % secs
- secs = secs - mod if mod != 0 else mod
if self.backtest_controller.parties == 2:
steps = int(secs) // self.config.backtest_engine.speed
steps = max(steps, 1)
@@ -134,6 +138,21 @@ class Strategy(ABC):
self.backtest_controller.wait()
else:
self.backtest_controller.wait()
+ except Exception as err:
+ self.backtest_controller.wait()
+ logger.error("Error: %s in backtest_sleep", err)
+
+ async def backtest_sleep(self, *, secs: float):
+ """Sleep for the needed amount of seconds in between requests to the terminal.
+
+ Args:
+ secs (float): The time in seconds. Usually the timeframe you are trading on.
+ """
+ try:
+ _time = self.config.backtest_engine.cursor.time
+ mod = _time % secs
+ secs = secs - mod if mod != 0 else mod
+ await self._backtest_sleep(secs=secs)
except Exception as err:
logger.error("Error: %s in backtest_sleep", err)
diff --git a/src/aiomql/lib/symbol.py b/src/aiomql/lib/symbol.py
index a67f944..8599477 100644
--- a/src/aiomql/lib/symbol.py
+++ b/src/aiomql/lib/symbol.py
@@ -175,7 +175,9 @@ 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:
@@ -257,7 +259,9 @@ class Symbol(_Base, SymbolInfo):
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:
@@ -277,13 +281,17 @@ 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.
@@ -306,7 +314,9 @@ 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/ticks.py b/src/aiomql/lib/ticks.py
index 8179e02..820ea80 100644
--- a/src/aiomql/lib/ticks.py
+++ b/src/aiomql/lib/ticks.py
@@ -45,15 +45,18 @@ class Tick:
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
diff --git a/src/aiomql/lib/trade_records.py b/src/aiomql/lib/trade_records.py
index fcfd214..dff325d 100644
--- a/src/aiomql/lib/trade_records.py
+++ b/src/aiomql/lib/trade_records.py
@@ -114,7 +114,12 @@ 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]
diff --git a/src/aiomql/lib/trader.py b/src/aiomql/lib/trader.py
index b814427..477d633 100644
--- a/src/aiomql/lib/trader.py
+++ b/src/aiomql/lib/trader.py
@@ -82,7 +82,9 @@ class Trader(ABC):
elif self.order.type == OrderType.SELL:
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.
@@ -100,7 +102,9 @@ class Trader(ABC):
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)
- 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.
@@ -122,7 +126,9 @@ class Trader(ABC):
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)
- 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:
@@ -167,18 +173,18 @@ class Trader(ABC):
return check
if check.retcode != 0:
- logger.warning(f"Invalid order for due to {check.comment}")
+ logger.warning("Invalid order %s, for due to %s", self.symbol, check.comment)
return check
async def send_order(self) -> OrderSendResult | None:
"""Send the order to the broker."""
result = await self.order.send()
if result is None:
- logger.warning(f"{self.order.mt5.error}: Failed to place order.")
+ logger.warning("%s: Failed to place order.", self.order.mt5.error)
return result
if result.retcode != 10009:
- logger.warning(f"Unable to place order for due to {result.comment}")
+ logger.warning("Unable to place order for %s due to %s", self.symbol, result.comment)
return result
return result
@@ -195,7 +201,11 @@ 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 4463682..47218d7 100644
--- a/tests/backtest/conftest.py
+++ b/tests/backtest/conftest.py
@@ -50,7 +50,9 @@ 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)
diff --git a/tests/backtest/integration/test_backtesting.py b/tests/backtest/integration/test_backtesting.py
index 03c854c..d850391 100644
--- a/tests/backtest/integration/test_backtesting.py
+++ b/tests/backtest/integration/test_backtesting.py
@@ -112,7 +112,11 @@ 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
diff --git a/tests/backtest/unit/test_deals_manager.py b/tests/backtest/unit/test_deals_manager.py
index ba69b33..4f3d13a 100644
--- a/tests/backtest/unit/test_deals_manager.py
+++ b/tests/backtest/unit/test_deals_manager.py
@@ -21,4 +21,8 @@ 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 b5d577e..e2969c9 100644
--- a/tests/backtest/unit/test_order_manager.py
+++ b/tests/backtest/unit/test_order_manager.py
@@ -21,4 +21,8 @@ async def test_orders_manager(backtest_engine, sell_order, buy_order, period, po
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 4ff4fb4..c0f51d8 100644
--- a/tests/live/conftest.py
+++ b/tests/live/conftest.py
@@ -47,7 +47,9 @@ 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_results_records.py b/tests/live/integration/test_results_records.py
index e7a69cf..df09953 100644
--- a/tests/live/integration/test_results_records.py
+++ b/tests/live/integration/test_results_records.py
@@ -36,7 +36,13 @@ 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):
@@ -48,7 +54,12 @@ class TestRecordsAndResults:
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 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):
diff --git a/tests/live/unit/test_backtest_engine.py b/tests/live/unit/test_backtest_engine.py
index 22255f0..9b04376 100644
--- a/tests/live/unit/test_backtest_engine.py
+++ b/tests/live/unit/test_backtest_engine.py
@@ -14,7 +14,13 @@ 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.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")
@@ -27,7 +33,13 @@ class TestBackTestEngine:
@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")
@@ -47,7 +59,8 @@ 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)
@@ -255,10 +268,14 @@ 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_get_data.py b/tests/live/unit/test_get_data.py
index 1adc1b4..551baf2 100644
--- a/tests/live/unit/test_get_data.py
+++ b/tests/live/unit/test_get_data.py
@@ -14,7 +14,9 @@ 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):