This commit is contained in:
Ichinga Samuel
2024-11-16 09:26:10 +01:00
parent 2e7aa73aec
commit 1109d78218
45 changed files with 1110 additions and 271 deletions
+147 -35
View File
@@ -3,6 +3,7 @@
![GitHub issues](https://img.shields.io/github/issues/ichinga-samuel/aiomql?style=plastic)
![PyPI](https://img.shields.io/pypi/v/aiomql)
### 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.
[!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/ichingasamuel)
@@ -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
}
+185
View File
@@ -0,0 +1,185 @@
<a id="backtester"></a>
# backtester
<a id="backtester.BackTester"></a>
## 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
<a id="backtester.BackTester.initialize_sync"></a>
#### 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
<a id="backtester.BackTester.initialize"></a>
#### 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
<a id="backtester.BackTester.add_coroutine"></a>
#### 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
<a id="backtester.BackTester.execute"></a>
#### execute
```python
def execute()
```
Execute the bot.
<a id="backtester.BackTester.start"></a>
#### start
```python
async def start()
```
Initialize the bot and execute it. Similar to calling `execute` method but is a coroutine.
<a id="backtester.BackTester.add_strategy"></a>
#### 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
<a id="backtester.BackTester.add_strategies"></a>
#### add\_strategies
```python
def add_strategies(*, strategies: Iterable[Strategy])
```
Add multiple strategies at the same time
**Arguments**:
- `strategies` - A list of strategies
<a id="backtester.BackTester.add_strategy_all"></a>
#### 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
<a id="backtester.BackTester.init_strategy"></a>
#### init\_strategy
```python
async def init_strategy(*, strategy: Strategy) -> bool
```
Initialize a single strategy. This method is called internally by the bot.
<a id="backtester.BackTester.init_strategy_sync"></a>
#### init\_strategy\_sync
```python
def init_strategy_sync(*, strategy: Strategy) -> bool
```
Initialize a single strategy. This method is called internally by the bot.
<a id="backtester.BackTester.init_strategies"></a>
#### init\_strategies
```python
async def init_strategies()
```
Initialize the symbols for the current trading session. This method is called internally by the bot.
<a id="backtester.BackTester.init_strategies_sync"></a>
#### init\_strategies\_sync
```python
def init_strategies_sync()
```
Initialize the symbols for the current trading session. This method is called internally by the bot.
+11 -3
View File
@@ -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
<a id="strategy.sleep"></a>
### 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 |
<a id="strategy.delay"></a>
### delay
```python
async def delay(*, secs: float)
```
Sleep for the needed amount of seconds specified in the parameter.
<a id="strategy.live_sleep"></a>
### live_sleep
```python
+55
View File
@@ -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)
+19
View File
@@ -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()
-2
View File
@@ -1,2 +0,0 @@
line-length = 150
target-version = "py311"
+9 -18
View File
@@ -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()
+4 -5
View File
@@ -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()
+1 -1
View File
@@ -1,3 +1,3 @@
from setuptools import setup
setup()
setup
+2
View File
@@ -1,3 +1,5 @@
from .strategies import *
from .candle_patterns import *
from .symbols import *
from .utils import *
from .traders import *
+5 -1
View File
@@ -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)
+23 -13
View File
@@ -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")
+3 -3
View File
@@ -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
"""
+1
View File
@@ -1 +1,2 @@
from .simple_trader import SimpleTrader
from .scalp_trader import ScalpTrader
+2 -2
View File
@@ -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
@@ -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
@@ -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()
+156 -49
View File
@@ -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
)
+21 -7
View File
@@ -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):
[
+23 -6
View File
@@ -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:
+11 -2
View File
@@ -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()
+4 -1
View File
@@ -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)
+86 -18
View File
@@ -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)
+126 -25
View File
@@ -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
+16 -6
View File
@@ -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 _:
+3 -1
View File
@@ -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:
+9 -3
View File
@@ -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:
+12 -9
View File
@@ -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
+6 -2
View File
@@ -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)
)
+11 -2
View File
@@ -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)])
+4 -1
View File
@@ -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:
+23 -4
View File
@@ -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)
+15 -5
View File
@@ -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:
+12 -9
View File
@@ -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
+6 -1
View File
@@ -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]
+17 -7
View File
@@ -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)
+3 -1
View File
@@ -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)
@@ -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
+5 -1
View File
@@ -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())
)
+5 -1
View File
@@ -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())
)
+3 -1
View File
@@ -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)
+13 -2
View File
@@ -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):
+22 -5
View File
@@ -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):
+3 -1
View File
@@ -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):