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) ![GitHub issues](https://img.shields.io/github/issues/ichinga-samuel/aiomql?style=plastic)
![PyPI](https://img.shields.io/pypi/v/aiomql) ![PyPI](https://img.shields.io/pypi/v/aiomql)
### Installation ### Installation
```bash ```bash
pip install aiomql pip install aiomql
@@ -11,16 +12,17 @@ pip install aiomql
### Key Features ### Key Features
- Asynchronous Python Library For MetaTrader5 - Asynchronous Python Library For MetaTrader5
- Asynchronous Bot Building Framework - 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 - Use threadpool executors to run multiple strategies on multiple instruments concurrently
- Records and keep track of trades and strategies in csv files. - Records and keep track of trades and strategies in csv files.
- Helper classes for Bot Building. Easy to use and extend. - Helper classes for Bot Building. Easy to use and extend.
- Compatible with pandas-ta. - Compatible with pandas-ta.
- Sample Pre-Built strategies - Sample Pre-Built strategies
- Visualization of charts using matplotlib and mplfinance - Specify and Manage Trading Sessions
- Manage Trading periods using Sessions
- Risk Management - Risk Management
- Backtesting Engine
- Run multiple bots concurrently with different accounts from the same broker or different brokers - 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 ### As an asynchronous MetaTrader5 Libray
```python ```python
@@ -31,8 +33,14 @@ from aiomql import MetaTrader
async def main(): async def main():
mt5 = MetaTrader() mt5 = MetaTrader()
await mt5.initialize() res = await mt5.initialize(login=31288540, password='nwa0#anaEze', server='Deriv-Demo')
await mt5.login(123456, '*******', 'Broker-Server') 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() symbols = await mt5.symbols_get()
print(symbols) print(symbols)
@@ -40,55 +48,157 @@ asyncio.run(main())
``` ```
### As a Bot Building FrameWork using a Sample Strategy ### As a Bot Building FrameWork using a Sample Strategy
***The following code is a sample bot that uses the FingerTrap strategy from the library.\ Aiomql allows you to focus on building trading strategies and not worry about the underlying infrastructure.
It assumes that you have a config file in the same directory as the script.\ It provides a simple and easy to use framework for building bots with rich features and functionalities.
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.***
```python ```python
from datetime import time from datetime import time
import logging 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) logging.basicConfig(level=logging.INFO)
def build_bot(): def build_bot():
bot = 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 # configure the parameters and the trader for a strategy
params = {'trend_candles_count': 500, 'fast_period': 8, 'slow_period': 34, 'etf': TimeFrame.M5} params = {'fast_period': 8, 'slow_period': 34, 'etf': TimeFrame.M5}
gbpusd = ForexSymbol(name='GBPUSD') symbols = ['GBPUSD', 'AUDUSD', 'USDCAD', 'EURGBP', 'EURUSD']
st1 = FingerTrap(symbol=gbpusd, params=params, trader=SimpleTrader(symbol=gbpusd, ram=RAM(risk=0.05, risk_to_reward=2)), symbols = [ForexSymbol(name=sym) for sym in symbols]
sessions=Sessions(london, new_york)) strategies = [FingerTrap(symbol=sym, params=params)for sym in symbols]
bot.add_strategies(strategies)
# use the default for the other strategies # create a strategy that uses sessions
st2 = FingerTrap(symbol=ForexSymbol(name='AUDUSD'), sessions=Sessions(tokyo, new_york)) # sessions are used to specify the trading hours for a particular market
st3 = FingerTrap(symbol=ForexSymbol(name='USDCAD'), sessions=Sessions(new_york)) # the strategy will only trade during the specified sessions
st4 = FingerTrap(symbol=ForexSymbol(name='USDJPY'), sessions=Sessions(tokyo)) london = Session(name='London', start=time(8, 0), end=time(16, 0))
st5 = FingerTrap(symbol=ForexSymbol(name='EURGBP'), sessions=Sessions(london)) 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 sessions = Sessions(sessions=[london, new_york, tokyo])
st6 = FingerTrap(symbol=ForexSymbol(name='EURUSD')) jpy_strategy = Chaos(symbol=ForexSymbol(name='USDJPY'), sessions=sessions)
bot.add_strategy(strategy=jpy_strategy)
# add strategies to the bot
bot.add_strategies([st1, st2, st3, st4, st5, st6])
bot.execute() bot.execute()
# run the bot # run the bot
build_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 ## 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 ## Contributing
Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change. 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 ## Support
Feeling generous, like the package or want to see it become a more mature package? 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) [!["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, "profit": 0,
"equity": 434.73, "equity": 221.44,
"margin": 0.0, "margin": 0.0,
"margin_free": 434.73, "margin_free": 221.44,
"margin_level": 0, "margin_level": 0,
"wins": 97, "wins": 11,
"losses": 110, "losses": 54,
"total": 207, "total": 65,
"win_percentage": 46.86, "win_percentage": 16.92,
"win": 1101.92, "win": 85.57,
"loss": -1017.19, "loss": -214.13,
"net_profit": 84.73, "net_profit": -128.56,
"profit_factor": 1.08, "profit_factor": 0.4,
"profitability": 24.21 "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 ## Table of Contents
- [Strategy](#strategy.strategy) - [Strategy](#strategy.strategy)
- [\_\_init\_\_](#strategy.__init__) - [\__init\__](#strategy.__init__)
- [sleep](#strategy.sleep) - [sleep](#strategy.sleep)
- [delay](#strategy.delay)
- [live_sleep](#strategy.live_sleep) - [live_sleep](#strategy.live_sleep)
- [backtest_sleep](#strategy.backtest_sleep) - [backtest_sleep](#strategy.backtest_sleep)
- [run_strategy](#strategy.run_strategy) - [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> <a id="strategy.sleep"></a>
### sleep ### sleep
```python ```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. 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 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 | | `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> <a id="strategy.live_sleep"></a>
### live_sleep ### live_sleep
```python ```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 import logging
from datetime import datetime, UTC from datetime import datetime, UTC
from aiomql.lib.backtester import BackTester from aiomql.lib.backtester import BackTester
from aiomql.core import Config 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.contrib.symbols import ForexSymbol
from aiomql.core.backtesting import BackTestEngine from aiomql.core.backtesting import BackTestEngine
async def back_tester(): def back_tester():
config = Config() Config(mode="backtest")
config.mode = "backtest"
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s") logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s")
syms = ["Volatility 75 Index", "Volatility 100 Index", "Volatility 25 Index", "Volatility 10 Index"] syms = ["Volatility 75 Index", "Volatility 100 Index", "Volatility 25 Index", "Volatility 10 Index"]
symbols = [ForexSymbol(name=sym) for sym in syms] symbols = [ForexSymbol(name=sym) for sym in syms]
strategies = [Chaos(symbol=symbol) for symbol in symbols] strategies = [FingerTrap(symbol=symbol) for symbol in symbols]
start = datetime(2024, 5, 1, tzinfo=UTC) start = datetime(2024, 5, 1, tzinfo=UTC)
stop_time = datetime(2024, 5, 2, tzinfo=UTC) stop_time = datetime(2024, 5, 2, tzinfo=UTC)
end = datetime(2024, 5, 7, tzinfo=UTC) end = datetime(2024, 5, 7, tzinfo=UTC)
back_test_engine = BackTestEngine( back_test_engine = BackTestEngine(start=start, end=end, speed=3600, stop_time=stop_time,
start=start, close_open_positions_on_exit=True, assign_to_config=True, preload=True,
end=end, account_info={"balance": 350})
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 = BackTester(backtest_engine=back_test_engine)
backtester.add_strategies(strategies=strategies) 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 import logging
from aiomql.lib.bot import Bot from aiomql.lib.bot import Bot
from aiomql.contrib.strategies import Chaos from aiomql.contrib.strategies import FingerTrap
from aiomql.contrib.symbols import ForexSymbol 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") logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s")
syms = ["Volatility 75 Index", "Volatility 100 Index", "Volatility 50 Index"] syms = ["Volatility 75 Index", "Volatility 100 Index", "Volatility 50 Index"]
symbols = [ForexSymbol(name=sym) for sym in syms] symbols = [ForexSymbol(name=sym) for sym in syms]
strategies = [Chaos(symbol=symbol) for symbol in symbols] strategies = [FingerTrap(symbol=symbol) for symbol in symbols]
bot = Bot() bot = Bot()
bot.executor.timeout = 10
bot.add_strategies(strategies=strategies) bot.add_strategies(strategies=strategies)
bot.execute() bot.execute()
chaos_bot() sample_bot()
+1 -1
View File
@@ -1,3 +1,3 @@
from setuptools import setup from setuptools import setup
setup() setup
+2
View File
@@ -1,3 +1,5 @@
from .strategies import * from .strategies import *
from .candle_patterns import * from .candle_patterns import *
from .symbols 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): async def check_trend(self):
try: try:
candles = await self.symbol.copy_rates_from_pos(timeframe=self.htf, count=self.hcc) 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) self.tracker.update(new=False, order_type=None, snooze=5)
return return
self.tracker.update(new=True, trend_time=current.time, last_trend_price=current.close) self.tracker.update(new=True, trend_time=current.time, last_trend_price=current.close)
+23 -13
View File
@@ -19,14 +19,18 @@ class FingerTrap(Strategy):
fast_ema: int fast_ema: int
slow_ema: int slow_ema: int
entry_ema: int entry_ema: int
parameters: dict
ecc: int ecc: int
tcc: int tcc: int
trader: Trader trader: Trader
tracker: Tracker 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) super().__init__(symbol=symbol, params=params, sessions=sessions, name=name)
self.trader = trader or SimpleTrader(symbol=self.symbol) self.trader = trader or SimpleTrader(symbol=self.symbol)
self.tracker: Tracker = Tracker(snooze=self.ttf.seconds) self.tracker: Tracker = Tracker(snooze=self.ttf.seconds)
@@ -37,6 +41,7 @@ class FingerTrap(Strategy):
if (current := candles[-1]) and current.time < self.tracker.trend_time: if (current := candles[-1]) and current.time < self.tracker.trend_time:
self.tracker.update(new=False, order_type=None) self.tracker.update(new=False, order_type=None)
return return
self.tracker.update(new=True, trend_time=current.time, last_trend_price=current.close) self.tracker.update(new=True, trend_time=current.time, last_trend_price=current.close)
candles.ta.ema(length=self.slow_ema, append=True, fillna=0) candles.ta.ema(length=self.slow_ema, append=True, fillna=0)
candles.ta.ema(length=self.fast_ema, append=True, fillna=0) candles.ta.ema(length=self.fast_ema, append=True, fillna=0)
@@ -46,14 +51,17 @@ class FingerTrap(Strategy):
fbs = candles.ta_lib.below(candles.fast, candles.slow) fbs = candles.ta_lib.below(candles.fast, candles.slow)
caf = candles.ta_lib.above(candles.close, candles.fast) caf = candles.ta_lib.above(candles.close, candles.fast)
cbf = candles.ta_lib.below(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") 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") self.tracker.update(trend="bearish")
else: else:
self.tracker.update(trend="ranging", snooze=self.ttf.seconds, order_type=None) self.tracker.update(trend="ranging", snooze=self.ttf.seconds, order_type=None)
self.tracker.update(trend="bullish") # remove this line
self.tracker.update(trend="bullish") #Todo remove this line
except Exception as err: except Exception as err:
logger.error(f"{err} for {self.symbol} in {self.__class__.__name__}.check_trend") logger.error(f"{err} for {self.symbol} in {self.__class__.__name__}.check_trend")
self.tracker.update(snooze=self.ttf.seconds, order_type=None) 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: if (current := candles[-1]) and current.time < self.tracker.entry_time:
self.tracker.update(new=False, order_type=None) self.tracker.update(new=False, order_type=None)
return return
self.tracker.update(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.ta.ema(length=self.entry_ema, append=True)
candles.rename(**{f"EMA_{self.entry_ema}": "ema"}) candles.rename(**{f"EMA_{self.entry_ema}": "ema"})
candles["cae"] = candles.ta_lib.cross(candles.close, candles.ema) candles["cae"] = candles.ta_lib.cross(candles.close, candles.ema)
candles["cbe"] = candles.ta_lib.cross(candles.close, candles.ema, above=False) candles["cbe"] = candles.ta_lib.cross(candles.close, candles.ema, above=False)
current = candles[-1] 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 sl = find_bullish_fractal(candles).low
self.tracker.update(snooze=self.ttf.seconds, order_type=OrderType.BUY, sl=sl) self.tracker.update(snooze=self.ttf.seconds, order_type=OrderType.BUY, sl=sl)
elif self.tracker.bearish and current.cbe: elif True or self.tracker.bearish and current.cbe:
sl = find_bearish_fractal(candles).high sl = find_bearish_fractal(candles).high
self.tracker.update(snooze=self.ttf.seconds, order_type=OrderType.SELL, sl=sl) self.tracker.update(snooze=self.ttf.seconds, order_type=OrderType.SELL, sl=sl)
else: else:
@@ -84,11 +94,10 @@ class FingerTrap(Strategy):
async def watch_market(self): async def watch_market(self):
await self.check_trend() await self.check_trend()
if not self.tracker.ranging: if self.tracker.ranging is False:
await self.confirm_trend() await self.confirm_trend()
async def trade(self): async def trade(self):
logger.info(f"Trading {self.symbol}")
try: try:
await self.watch_market() await self.watch_market()
if self.tracker.new is False: if self.tracker.new is False:
@@ -97,7 +106,8 @@ class FingerTrap(Strategy):
if self.tracker.order_type is None: if self.tracker.order_type is None:
await self.sleep(secs=self.tracker.snooze) await self.sleep(secs=self.tracker.snooze)
return return
await self.trader.place_trade(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) await self.sleep(secs=self.tracker.snooze)
except Exception as err: except Exception as err:
logger.error(f"{err} For {self.symbol} in {self.__class__.__name__}.trade") logger.error(f"{err} For {self.symbol} in {self.__class__.__name__}.trade")
+3 -3
View File
@@ -17,7 +17,7 @@ class ForexSymbol(Symbol):
def compute_points(self, *, amount: float, volume: float) -> float: 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. """Compute the number of points required for a trade. Given the amount and the volume of the trade.
Args: Args:
amount (float): Amount to trade amount (float): Amount to trade
volume (float): Volume 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: 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. """Compute the volume required for a trade. Given the amount, the price and the stop loss.
Args: Args:
amount (float): Amount to trade amount (float): Amount to trade
price (float): The price of the trade price (float): The price of the trade
sl (float): The stop loss 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 round_down (bool): round down the computed volume to the nearest step default to False
Returns: Returns:
float: The volume required for the trade float: The volume required for the trade
""" """
+1
View File
@@ -1 +1,2 @@
from .simple_trader import SimpleTrader from .simple_trader import SimpleTrader
from .scalp_trader import ScalpTrader
+2 -2
View File
@@ -8,8 +8,8 @@ logger = getLogger(__name__)
class SimpleTrader(Trader): class SimpleTrader(Trader):
async def place_trade(self, *, order_type: OrderType, sl: float, parameters: dict = None): async def place_trade(self, *, order_type: OrderType, sl: float, parameters: dict = None):
"""Places a trade based on the order_type and a given stop_loss. The volume is based on the amount to risk which is """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. calculated using the Risk Assessment Management instance.
Args: Args:
order_type (OrderType): The order_type order_type (OrderType): The order_type
@@ -7,6 +7,7 @@ from ..constants import AccountTradeMode, AccountMarginMode, AccountStopOutMode
@dataclass @dataclass
class BackTestAccount: class BackTestAccount:
"""Account data for backtesting""" """Account data for backtesting"""
login: int = 0 login: int = 0
trade_mode: AccountTradeMode = AccountTradeMode.DEMO trade_mode: AccountTradeMode = AccountTradeMode.DEMO
leverage: float = 1 leverage: float = 1
@@ -40,7 +41,7 @@ class BackTestAccount:
def get_dict(self, exclude: set = None, include: set = None): 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 """Returns a dictionary of the account data. Using the exclude and include arguments, you can filter the data
Args: Args:
exclude (set): A set of keys to exclude exclude (set): A set of keys to exclude
include (set): A set of keys to include include (set): A set of keys to include
@@ -23,12 +23,12 @@ class BackTestController:
tasks (list[Task]): The tasks that are being run tasks (list[Task]): The tasks that are being run
barrier (Barrier): The barrier for synchronizing the tasks barrier (Barrier): The barrier for synchronizing the tasks
""" """
_instance: Self _instance: Self
config: Config config: Config
tasks: list[Task] tasks: list[Task]
barrier: Barrier barrier: Barrier
def __new__(cls, *args, **kwargs): def __new__(cls, *args, **kwargs):
if not hasattr(cls, "_instance"): if not hasattr(cls, "_instance"):
cls._instance = super().__new__(cls) cls._instance = super().__new__(cls)
@@ -76,15 +76,20 @@ class BackTestController:
while True: while True:
pending = self.wait() pending = self.wait()
# all main tasks have been completed in the current cycle # all main tasks have been completed in the current cycle
if pending == 0: if pending == 0:
await self.backtest_engine.tracker() await self.backtest_engine.tracker()
self.backtest_engine.next() self.backtest_engine.next()
# gives an output every 6 hours # gives an output every 6 hours
if self.backtest_engine.cursor.time % (3600 * 6) == 0: 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: if self.backtest_engine.stop_testing:
logger.info( 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 break
await self.backtest_engine.wrap_up() await self.backtest_engine.wrap_up()
+156 -49
View File
@@ -25,7 +25,18 @@ from MetaTrader5 import (
) )
from ..meta_trader import MetaTrader from ..meta_trader import MetaTrader
from ..constants import TimeFrame, OrderType, TradeAction, AccountStopOutMode, PositionReason, DealType, DealReason, DealEntry, OrderReason, CopyTicks from ..constants import (
TimeFrame,
OrderType,
TradeAction,
AccountStopOutMode,
PositionReason,
DealType,
DealReason,
DealEntry,
OrderReason,
CopyTicks,
)
from ..._utils import round_down, round_up, error_handler, error_handler_sync, async_cache from ..._utils import round_down, round_up, error_handler, error_handler_sync, async_cache
@@ -105,7 +116,7 @@ class BackTestEngine:
name (str, optional): The name of the backtest. Defaults to "". If not provided, name (str, optional): The name of the backtest. Defaults to "". If not provided,
it is generated from the start and end times. it is generated from the start and end times.
stop_time (float | datetime, optional): The time to stop the backtest. Defaults to None. 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. 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.config.backtest_engine = self
self.setup_test_range(start=start, end=end, speed=speed, restart=restart) self.setup_test_range(start=start, end=end, speed=speed, restart=restart)
self.setup_data(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) 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.name = name or self._data.name or f"backtest_data_{start:%d_%m_%y}_{end:%d_%m_%y}"
self.stop_testing = False self.stop_testing = False
self.use_terminal = self.config.use_terminal_for_backtesting if use_terminal is None else use_terminal self.use_terminal = self.config.use_terminal_for_backtesting if use_terminal is None else use_terminal
self.close_open_positions_on_exit = close_open_positions_on_exit self.close_open_positions_on_exit = close_open_positions_on_exit
if stop_time is not None: 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()) stop_time = int(val.timestamp())
self.stop_time = stop_time self.stop_time = stop_time
self.preload = preload self.preload = preload
@@ -183,20 +200,21 @@ class BackTestEngine:
def __repr__(self): def __repr__(self):
return f"{self.__class__.__name__}()" return f"{self.__class__.__name__}()"
def setup_test_range(self, *, start: float | datetime = None, end: float | datetime = None, speed: def setup_test_range(
int = 60, restart: bool = True): 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 """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. at which it runs.
Args: Args:
start (float | datetime, optional): The start time of the backtest. Defaults to None. If a float is passed, 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. 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, 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. it is assumed to be a timestamp.
speed (int, optional): The speed of the backtest. Defaults to 60. speed (int, optional): The speed of the backtest. Defaults to 60.
restart (bool, optional): Whether to restart the backtest. Defaults to True. restart (bool, optional): Whether to restart the backtest. Defaults to True.
This is useful when resuming a backtest using a saved BackTestData. This is useful when resuming a backtest using a saved BackTestData.
""" """
@@ -229,7 +247,7 @@ class BackTestEngine:
Args: Args:
restart (bool, optional): Whether to restart the data. Defaults to True. restart (bool, optional): Whether to restart the data. Defaults to True.
""" """
if restart is True: if restart is True:
self.orders = OrdersManager() self.orders = OrdersManager()
self.positions = PositionsManager() self.positions = PositionsManager()
@@ -245,7 +263,9 @@ class BackTestEngine:
positions = {} positions = {}
for ticket, position in self._data.positions.items(): for ticket, position in self._data.positions.items():
positions[ticket] = TradePosition((position.get(k) for k in TradePosition.__match_args__)) 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 = {} deals = {}
for ticket, deal in self._data.deals.items(): for ticket, deal in self._data.deals.items():
@@ -261,7 +281,7 @@ class BackTestEngine:
@property @property
def data(self): def data(self):
"""The BackTestData instance used for the backtest. If not provided, a new instance is created, """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 return self._data
def reset(self, clear_data: bool = False): def reset(self, clear_data: bool = False):
@@ -346,7 +366,7 @@ class BackTestEngine:
@error_handler @error_handler
async def wrap_up(self): 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 """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: if self.close_open_positions_on_exit:
await self.close_all_open() await self.close_all_open()
self.save_result_to_json() self.save_result_to_json()
@@ -484,10 +504,18 @@ class BackTestEngine:
ticket (int): Position ticket ticket (int): Position ticket
""" """
pos = self.positions[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) tick = await self.get_price_tick(symbol=symbol, time=self.cursor.time)
price_current = tick.bid if order_type == OrderType.BUY else tick.ask 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 = dict(price_current=price_current, time_update=self.cursor.time)
kwargs.update(profit=profit) if profit is not None else ... kwargs.update(profit=profit) if profit is not None else ...
self.positions.update(ticket=pos.ticket, **kwargs) self.positions.update(ticket=pos.ticket, **kwargs)
@@ -582,7 +610,9 @@ class BackTestEngine:
Returns: Returns:
bool: True if the stops are modified successfully, False otherwise 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 return True
def update_account(self, *, profit: float = None, margin: float = 0, gain: float = 0): def update_account(self, *, profit: float = None, margin: float = 0, gain: float = 0):
@@ -597,10 +627,14 @@ class BackTestEngine:
self.account_lock.acquire() self.account_lock.acquire()
try: try:
self._account.balance += round(gain, self._account.currency_digits) 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.equity = self._account.balance + self._account.profit
self._account.margin += round(margin, self._account.currency_digits) 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.balance = round(self._account.balance, self._account.currency_digits)
self._account.equity = round(self._account.equity, self._account.currency_digits) self._account.equity = round(self._account.equity, self._account.currency_digits)
self._account.margin = round(self._account.margin, self._account.currency_digits) self._account.margin = round(self._account.margin, self._account.currency_digits)
@@ -766,7 +800,11 @@ class BackTestEngine:
osr["retcode"] = 10018 osr["retcode"] = 10018
return OrderSendResult((osr.get(k, 0) for k in OrderSendResult.__match_args__)) return OrderSendResult((osr.get(k, 0) for k in OrderSendResult.__match_args__))
trade_order = {"external_id": "", "comment": "", **{k: v for k, v in request.items() if k in TradeOrder.__match_args__}} trade_order = {
"external_id": "",
"comment": "",
**{k: v for k, v in request.items() if k in TradeOrder.__match_args__},
}
order_type, symbol = request.get("type"), request.get("symbol", "") order_type, symbol = request.get("type"), request.get("symbol", "")
action, position_id = request.get("action"), request.get("position") action, position_id = request.get("action"), request.get("position")
sl, tp, volume, symbol = (request.get("sl"), request.get("tp"), request.get("volume"), request.get("symbol")) sl, tp, volume, symbol = (request.get("sl"), request.get("tp"), request.get("volume"), request.get("symbol"))
@@ -820,7 +858,9 @@ class BackTestEngine:
self.orders[order.ticket] = order self.orders[order.ticket] = order
deal = TradeDeal((deal.get(k, 0) for k in TradeDeal.__match_args__)) deal = TradeDeal((deal.get(k, 0) for k in TradeDeal.__match_args__))
self.deals[deal.ticket] = deal 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__)) return OrderSendResult((osr.get(k, 0) for k in OrderSendResult.__match_args__))
if action == TradeAction.SLTP and current_position: 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__)) return OrderSendResult((osr.get(k, 0) for k in OrderSendResult.__match_args__))
res = self.modify_stops(ticket=position_id, sl=sl, tp=tp) res = self.modify_stops(ticket=position_id, sl=sl, tp=tp)
if res: 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__)) return OrderSendResult((osr.get(k, 0) for k in OrderSendResult.__match_args__))
if action == TradeAction.DEAL and order_type in (OrderType.BUY, OrderType.SELL): if action == TradeAction.DEAL and order_type in (OrderType.BUY, OrderType.SELL):
@@ -914,7 +956,9 @@ class BackTestEngine:
"deal": deal_ticket, "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.positions.set_margin(ticket=order_ticket, margin=margin)
self.update_account(margin=margin) self.update_account(margin=margin)
return OrderSendResult((osr.get(k, 0) for k in OrderSendResult.__match_args__)) return OrderSendResult((osr.get(k, 0) for k in OrderSendResult.__match_args__))
@@ -949,7 +993,9 @@ class BackTestEngine:
# check margin and confirm order can go through for a deal action and buy or sell order type # 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: 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: if margin is None:
return OrderCheckResult((ocr.get(k, 0) for k in OrderCheckResult.__match_args__)) return OrderCheckResult((ocr.get(k, 0) for k in OrderCheckResult.__match_args__))
@@ -1002,7 +1048,13 @@ class BackTestEngine:
return OrderCheckResult((ocr.get(k, 0) for k in OrderCheckResult.__match_args__)) return OrderCheckResult((ocr.get(k, 0) for k in OrderCheckResult.__match_args__))
ocr.update( ocr.update(
{"balance": self._account.balance, "profit": self._account.profit, "equity": self._account.equity, "comment": "Done", "retcode": 0} {
"balance": self._account.balance,
"profit": self._account.profit,
"equity": self._account.equity,
"comment": "Done",
"retcode": 0,
}
) )
return OrderCheckResult((ocr.get(k, 0) for k in OrderCheckResult.__match_args__)) return OrderCheckResult((ocr.get(k, 0) for k in OrderCheckResult.__match_args__))
@@ -1117,7 +1169,9 @@ class BackTestEngine:
return SymbolInfo((info.get(key) for key in SymbolInfo.__match_args__)) return SymbolInfo((info.get(key) for key in SymbolInfo.__match_args__))
@error_handler @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 """Get rates from a specific date to the current date. Used by the backtester to get rates for a symbol
Args: Args:
@@ -1129,7 +1183,11 @@ class BackTestEngine:
Returns: Returns:
np.ndarray: An array of rates 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: if self.use_terminal:
rates = await self.mt5.copy_rates_from(symbol, timeframe, date_from, count) rates = await self.mt5.copy_rates_from(symbol, timeframe, date_from, count)
return rates return rates
@@ -1171,8 +1229,9 @@ class BackTestEngine:
return np.fromiter((tuple(i) for i in rates.iloc), dtype=self.get_dtype(df=rates)) return np.fromiter((tuple(i) for i in rates.iloc), dtype=self.get_dtype(df=rates))
@error_handler @error_handler
async def get_rates_range(self, *, symbol: str, timeframe: TimeFrame, date_from: datetime | float, async def get_rates_range(
date_to: datetime | float) -> np.ndarray: 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 """Get rates within a specific date range. Used by the backtester to get rates for a symbol
Args: Args:
@@ -1184,8 +1243,14 @@ class BackTestEngine:
Returns: Returns:
np.ndarray: An array of rates 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_to = date_to.astimezone(tz=UTC) if isinstance(date_to, datetime) else datetime.fromtimestamp(date_to, tz=UTC) 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: if self.use_terminal:
rates = await self.mt5.copy_rates_range(symbol, timeframe, date_from, date_to) rates = await self.mt5.copy_rates_range(symbol, timeframe, date_from, date_to)
return rates return rates
@@ -1197,8 +1262,9 @@ class BackTestEngine:
return np.fromiter((tuple(i) for i in rates.iloc), dtype=self.get_dtype(df=rates)) return np.fromiter((tuple(i) for i in rates.iloc), dtype=self.get_dtype(df=rates))
@error_handler @error_handler
async def get_ticks_from(self, *, symbol: str, date_from: datetime | float, count: int, async def get_ticks_from(
flags: CopyTicks = CopyTicks.ALL) -> np.ndarray: 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. """Get a specified number of ticks counting from a specific date.
Args: Args:
symbol (str): The symbol to get ticks for symbol (str): The symbol to get ticks for
@@ -1209,7 +1275,11 @@ class BackTestEngine:
Returns: Returns:
np.ndarray: An array of ticks 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: if self.use_terminal:
ticks = await self.mt5.copy_ticks_from(symbol, date_from, count, flags) ticks = await self.mt5.copy_ticks_from(symbol, date_from, count, flags)
return ticks return ticks
@@ -1234,8 +1304,14 @@ class BackTestEngine:
Returns: Returns:
np.ndarray: An array of ticks 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_to = date_to.astimezone(tz=UTC) if isinstance(date_to, datetime) else datetime.fromtimestamp(date_to, tz=UTC) 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: if self.use_terminal:
ticks = await self.mt5.copy_ticks_range(symbol, date_from, date_to, flags) ticks = await self.mt5.copy_ticks_range(symbol, date_from, date_to, flags)
return ticks return ticks
@@ -1248,8 +1324,14 @@ class BackTestEngine:
@error_handler @error_handler
async def order_calc_margin( async def order_calc_margin(
self, *, action: Literal[OrderType.BUY, OrderType.SELL], symbol: str, volume: float, self,
price: float, use_terminal: bool = None): *,
action: Literal[OrderType.BUY, OrderType.SELL],
symbol: str,
volume: float,
price: float,
use_terminal: bool = None,
):
"""Calculate the margin required for a trade. """Calculate the margin required for a trade.
Args: Args:
@@ -1275,8 +1357,15 @@ class BackTestEngine:
@error_handler @error_handler
async def order_calc_profit( async def order_calc_profit(
self, *, action: Literal[OrderType.BUY, OrderType.SELL], symbol: str, volume: float, self,
price_open: float, price_close: float, use_terminal=None): *,
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. Calculate the profit for a trade.
@@ -1300,7 +1389,11 @@ class BackTestEngine:
sym = self.symbols.get(symbol) sym = self.symbols.get(symbol)
if sym is None and self.use_terminal: if sym is None and self.use_terminal:
sym = await self._symbol_info(symbol=symbol) 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) return round(profit, self._account.currency_digits)
@error_handler_sync @error_handler_sync
@@ -1368,8 +1461,14 @@ class BackTestEngine:
@error_handler_sync @error_handler_sync
def get_history_orders( def get_history_orders(
self, *, date_from: datetime | float = None, date_to: datetime | float = None, group: str = "", self,
ticket: int = None, position: int = None) -> tuple[TradeOrder, ...]: *,
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. """Get orders from the terminal history.
Args: Args:
@@ -1382,8 +1481,9 @@ class BackTestEngine:
Returns: Returns:
tuple[TradeOrder, ...]: Orders in the history tuple[TradeOrder, ...]: Orders in the history
""" """
return self.orders.history_orders_get(date_from=date_from, date_to=date_to, group=group, return self.orders.history_orders_get(
ticket=ticket, position=position) date_from=date_from, date_to=date_to, group=group, ticket=ticket, position=position
)
@error_handler_sync @error_handler_sync
def get_history_deals_total(self, *, date_from: datetime | float, date_to: datetime | float) -> int: def get_history_deals_total(self, *, date_from: datetime | float, date_to: datetime | float) -> int:
@@ -1400,8 +1500,14 @@ class BackTestEngine:
@error_handler_sync @error_handler_sync
def get_history_deals( def get_history_deals(
self, *, date_from: datetime | float = None, date_to: datetime | float = None, group: str = None, self,
position: int = None, ticket: int = None) -> tuple[TradeDeal, ...]: *,
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. """Get deals from the terminal history.
Args: Args:
@@ -1414,5 +1520,6 @@ class BackTestEngine:
Returns: Returns:
tuple[TradeDeal, ...]: Deals in the history tuple[TradeDeal, ...]: Deals in the history
""" """
return self.deals.history_deals_get(date_from=date_from, date_to=date_to, group=group, return self.deals.history_deals_get(
position=position, ticket=ticket) 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): class Cursor(NamedTuple):
"""A cursor to iterate over the data. Marks the current position.""" """A cursor to iterate over the data. Marks the current position."""
index: int index: int
time: int time: int
@@ -45,6 +46,7 @@ class BackTestData:
margins (dict): The margins data. margins (dict): The margins data.
fully_loaded (bool): A flag to indicate if the data is fully loaded fully_loaded (bool): A flag to indicate if the data is fully loaded
""" """
name: str = "" name: str = ""
terminal: dict[str, [str | int | bool | float]] = field(default_factory=dict) terminal: dict[str, [str | int | bool | float]] = field(default_factory=dict)
version: tuple[int, int, str] = (0, 0, "") version: tuple[int, int, str] = (0, 0, "")
@@ -93,10 +95,12 @@ class GetData:
mt5 (MetaTrader): The MetaTrader5 instance. mt5 (MetaTrader): The MetaTrader5 instance.
task_queue (TaskQueue): The task queue to handle the requests. task_queue (TaskQueue): The task queue to handle the requests.
""" """
data: BackTestData data: BackTestData
def __init__(self, *, start: datetime, end: datetime, symbols: Iterable[str], def __init__(
timeframes: Iterable[TimeFrame], name: str = ""): self, *, start: datetime, end: datetime, symbols: Iterable[str], timeframes: Iterable[TimeFrame], name: str = ""
):
""" """
Get the backtesting data from the MetaTrader5 terminal. Get the backtesting data from the MetaTrader5 terminal.
@@ -170,7 +174,11 @@ class GetData:
if workers: if workers:
self.task_queue.workers = workers self.task_queue.workers = workers
q_items = [QueueItem(self.get_symbols_rates), QueueItem(self.get_symbols_ticks), QueueItem(self.get_symbols_info)] q_items = [
QueueItem(self.get_symbols_rates),
QueueItem(self.get_symbols_ticks),
QueueItem(self.get_symbols_info),
]
[self.task_queue.add(item=item, priority=0, must_complete=True) for item in q_items] [self.task_queue.add(item=item, priority=0, must_complete=True) for item in q_items]
@@ -214,12 +222,18 @@ class GetData:
self.data.set_attrs(account=res) self.data.set_attrs(account=res)
async def get_symbols_info(self): async def get_symbols_info(self):
[self.task_queue.add(item=QueueItem(self.get_symbol_info, symbol=symbol)) for symbol in self.symbols [
if self.data.symbols.get(symbol) is None] self.task_queue.add(item=QueueItem(self.get_symbol_info, symbol=symbol))
for symbol in self.symbols
if self.data.symbols.get(symbol) is None
]
async def get_symbols_ticks(self): async def get_symbols_ticks(self):
[self.task_queue.add(item=QueueItem(self.get_symbol_ticks, symbol=symbol)) for symbol in self.symbols if [
self.data.ticks.get(symbol) is None] self.task_queue.add(item=QueueItem(self.get_symbol_ticks, symbol=symbol))
for symbol in self.symbols
if self.data.ticks.get(symbol) is None
]
async def get_symbols_rates(self): async def get_symbols_rates(self):
[ [
+23 -6
View File
@@ -42,6 +42,7 @@ class TradeManager(Generic[TradeData]):
>>> pos in manager >>> pos in manager
False False
""" """
_data: dict[int, TradeData] _data: dict[int, TradeData]
def __init__(self, *, data: dict = None): def __init__(self, *, data: dict = None):
@@ -112,6 +113,7 @@ class PositionsManager(TradeManager):
_open_positions (set[int]): The open positions. _open_positions (set[int]): The open positions.
margins (dict[int, float]): The margins of the open positions. margins (dict[int, float]): The margins of the open positions.
""" """
_data: dict[int, TradePosition] _data: dict[int, TradePosition]
_open_positions: set[int] _open_positions: set[int]
margins: dict[int, float] margins: dict[int, float]
@@ -240,8 +242,9 @@ class PositionsManager(TradeManager):
class OrdersManager(TradeManager): class OrdersManager(TradeManager):
"""Managers orders data during a backtest. It is a subclass of TradeManager. It manages access to the historical """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] _data = dict[int, TradeOrder]
def get_orders_range(self, *, date_from: float, date_to: float) -> tuple[TradeData, ...]: def get_orders_range(self, *, date_from: float, date_to: float) -> tuple[TradeData, ...]:
@@ -258,8 +261,15 @@ class OrdersManager(TradeManager):
end = date_to.timestamp() if isinstance(date_to, datetime) else date_to end = date_to.timestamp() if isinstance(date_to, datetime) else date_to
return tuple(order for order in self.values() if start <= order.time_setup <= end) return tuple(order for order in self.values() if start <= order.time_setup <= end)
def history_orders_get(self, *, date_from: float | datetime = None, date_to: float | datetime = None, def history_orders_get(
group: str = "", ticket: int = None, position: int = None) -> tuple[TradeOrder, ...]: 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 """Get historical orders. Given the start and end date of the range, the group, ticket, or position of the
orders. orders.
@@ -272,7 +282,7 @@ class OrdersManager(TradeManager):
Returns: Returns:
tuple[TradeOrder, ...]: The historical orders. tuple[TradeOrder, ...]: The historical orders.
""" """
if date_from and date_to: if date_from and date_to:
orders = self.get_orders_range(date_from=date_from, date_to=date_to) orders = self.get_orders_range(date_from=date_from, date_to=date_to)
if group: if group:
@@ -314,8 +324,15 @@ class DealsManager(TradeManager):
end = date_to.timestamp() if isinstance(date_to, datetime) else date_to end = date_to.timestamp() if isinstance(date_to, datetime) else date_to
return tuple(deal for deal in self.values() if start <= deal.time <= end) return tuple(deal for deal in self.values() if start <= deal.time <= end)
def history_deals_get(self, *, date_from: float | datetime = None, date_to: float | datetime = None, def history_deals_get(
group: str = "", ticket: int = None, position: int = None) -> tuple[TradeDeal, ...]: 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. """History deals get. Given the start and end date of the range, the group, ticket, or position of the deals.
Args: Args:
+11 -2
View File
@@ -35,7 +35,11 @@ class Base:
self.set_attributes(**kwargs) self.set_attributes(**kwargs)
def __repr__(self): 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 = ", ".join("%s=%s" % (i, j) for i, j in kv[:3])
args = args if len(kv) <= 3 else args + " ... " + ", ".join("%s=%s" % (i, j) for i, j in kv[-1:]) args = args if len(kv) <= 3 else args + " ... " + ", ".join("%s=%s" % (i, j) for i, j in kv[-1:])
return "%(class)s(%(args)s)" % {"class": self.__class__.__name__, "args": args} return "%(class)s(%(args)s)" % {"class": self.__class__.__name__, "args": args}
@@ -120,7 +124,11 @@ class Base:
""" """
try: try:
_filter = self.exclude.difference(self.include) _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: except Exception as err:
logger.warning(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 """Base class that provides access to the MetaTrader and Config classes as well as the MetaBackTester class for
backtesting mode. backtesting mode.
""" """
def __init__(self, **kwargs): def __init__(self, **kwargs):
self.config = Config() self.config = Config()
self.mt5 = MetaTrader() if self.config.mode != "backtest" else MetaBackTester() self.mt5 = MetaTrader() if self.config.mode != "backtest" else MetaBackTester()
+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. 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. The root directory is used to locate the config file and to set the records_dir and backtest_dir attributes.
""" """
login: int login: int
trade_record_mode: Literal["csv", "json"] trade_record_mode: Literal["csv", "json"]
password: str password: str
@@ -190,7 +191,9 @@ class Config:
if self.path: if self.path:
self.path = self.root / self.path if not Path(self.path).resolve().exists() else self.path self.path = self.root / self.path if not Path(self.path).resolve().exists() else self.path
if self.record_trades and (hasattr(self, "records_dir") is False or self.records_dir is None or root is not None): if self.record_trades and (
hasattr(self, "records_dir") is False or self.records_dir is None or root is not None
):
self.records_dir = self.root / self.records_dir_name self.records_dir = self.root / self.records_dir_name
self.records_dir.mkdir(parents=True, exist_ok=True) 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 typing import Literal, TypeVar
from numpy import ndarray 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 .meta_trader import MetaTrader
from .constants import TimeFrame, CopyTicks, OrderType from .constants import TimeFrame, CopyTicks, OrderType
@@ -43,7 +53,14 @@ class MetaBackTester(MetaTrader):
return await super().last_error() return await super().last_error()
async def initialize( 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: ) -> bool:
if self.config.use_terminal_for_backtesting: if self.config.use_terminal_for_backtesting:
return await super().initialize(path=path, login=login, password=password, server=server, timeout=timeout) return await super().initialize(path=path, login=login, password=password, server=server, timeout=timeout)
@@ -51,7 +68,14 @@ class MetaBackTester(MetaTrader):
return True return True
def initialize_sync( 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: ) -> bool:
if self.config.use_terminal_for_backtesting: if self.config.use_terminal_for_backtesting:
return super().initialize_sync(path=path, login=login, password=password, server=server, timeout=timeout) return super().initialize_sync(path=path, login=login, password=password, server=server, timeout=timeout)
@@ -103,28 +127,46 @@ class MetaBackTester(MetaTrader):
return tick return tick
@error_handler(msg="test data not available", exe=AttributeError) @error_handler(msg="test data not available", exe=AttributeError)
async def copy_rates_from(self, symbol: str, timeframe: TimeFrame, date_from: datetime | float, count: int) -> ndarray | None: async def copy_rates_from(
rates = await self.backtest_engine.get_rates_from(symbol=symbol, timeframe=timeframe, date_from=date_from, count=count) 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 return rates
@error_handler(msg="test data not available", exe=AttributeError) @error_handler(msg="test data not available", exe=AttributeError)
async def copy_rates_from_pos(self, symbol: str, timeframe: TimeFrame, start_pos: int, count: int) -> ndarray | None: async def copy_rates_from_pos(
rates = await self.backtest_engine.get_rates_from_pos(symbol=symbol, timeframe=timeframe, start_pos=start_pos, count=count) 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 return rates
@error_handler(msg="test data not available", exe=AttributeError) @error_handler(msg="test data not available", exe=AttributeError)
async def copy_rates_range(self, symbol: str, timeframe: TimeFrame, date_from: datetime | float, date_to: datetime | float) -> ndarray | None: async def copy_rates_range(
rates = await self.backtest_engine.get_rates_range(symbol=symbol, timeframe=timeframe, date_from=date_from, date_to=date_to) 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 return rates
@error_handler(msg="test data not available", exe=AttributeError) @error_handler(msg="test data not available", exe=AttributeError)
async def copy_ticks_from(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) ticks = await self.backtest_engine.get_ticks_from(symbol=symbol, date_from=date_from, count=count, flags=flags)
return ticks return ticks
@error_handler(msg="test data not available", exe=AttributeError) @error_handler(msg="test data not available", exe=AttributeError)
async def copy_ticks_range(self, symbol: str, date_from: datetime | float, date_to: datetime | float, flags: CopyTicks) -> ndarray | None: async def copy_ticks_range(
ticks = await self.backtest_engine.get_ticks_range(symbol=symbol, date_from=date_from, date_to=date_to, flags=flags) 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 return ticks
@error_handler(msg="test data not available", exe=AttributeError) @error_handler(msg="test data not available", exe=AttributeError)
@@ -142,7 +184,9 @@ class MetaBackTester(MetaTrader):
return res return res
@error_handler(msg="test data not available", exe=AttributeError) @error_handler(msg="test data not available", exe=AttributeError)
async def order_calc_profit(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( profit = await self.backtest_engine.order_calc_profit(
action=action, symbol=symbol, volume=volume, price_open=price_open, price_close=price_close action=action, symbol=symbol, volume=volume, price_open=price_open, price_close=price_close
) )
@@ -162,7 +206,9 @@ class MetaBackTester(MetaTrader):
return self.backtest_engine.get_positions_total() return self.backtest_engine.get_positions_total()
@error_handler(msg="test data not available", exe=AttributeError) @error_handler(msg="test data not available", exe=AttributeError)
async def positions_get(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} kwargs = {key: value for key, value in (("group", group), ("ticket", ticket), ("symbol", symbol)) if value}
return self.backtest_engine.get_positions(**kwargs) return self.backtest_engine.get_positions(**kwargs)
@@ -172,9 +218,20 @@ class MetaBackTester(MetaTrader):
@error_handler(msg="test data not available", exe=AttributeError) @error_handler(msg="test data not available", exe=AttributeError)
async def history_orders_get( async def history_orders_get(
self, 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: ) -> 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} kwargs = {key: value for key, value in args if value}
return self.backtest_engine.get_history_orders(**kwargs) return self.backtest_engine.get_history_orders(**kwargs)
@@ -184,8 +241,19 @@ class MetaBackTester(MetaTrader):
@error_handler(msg="test data not available", exe=AttributeError) @error_handler(msg="test data not available", exe=AttributeError)
async def history_deals_get( async def history_deals_get(
self, 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: ) -> 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} kwargs = {key: value for key, value in args if value}
return self.backtest_engine.get_history_deals(**kwargs) return self.backtest_engine.get_history_deals(**kwargs)
+126 -25
View File
@@ -5,8 +5,18 @@ from typing import Literal, Self
from pathlib import Path from pathlib import Path
import numpy as np import numpy as np
from MetaTrader5 import (BookInfo, SymbolInfo, AccountInfo, Tick, TerminalInfo, TradeOrder, TradeDeal, TradePosition, from MetaTrader5 import (
OrderSendResult, OrderCheckResult) BookInfo,
SymbolInfo,
AccountInfo,
Tick,
TerminalInfo,
TradeOrder,
TradeDeal,
TradePosition,
OrderSendResult,
OrderCheckResult,
)
import MetaTrader5 as mt5 import MetaTrader5 as mt5
from .constants import OrderType, CopyTicks from .constants import OrderType, CopyTicks
@@ -105,7 +115,13 @@ class MetaTrader(MetaCore):
return res return res
async def initialize( 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: ) -> bool:
""" """
Initializes the connection to the MetaTrader terminal. All parameters are optional. Initializes the connection to the MetaTrader terminal. All parameters are optional.
@@ -147,7 +163,13 @@ class MetaTrader(MetaCore):
return res return res
def initialize_sync( 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: ) -> bool:
""" """
Initializes the connection to the MetaTrader terminal. All parameters are optional. Initializes the connection to the MetaTrader terminal. All parameters are optional.
@@ -227,7 +249,11 @@ class MetaTrader(MetaCore):
return res return res
async def symbol_info(self, symbol: str) -> SymbolInfo | None: 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) res = await self._handler(api)
return res return res
@@ -242,22 +268,40 @@ class MetaTrader(MetaCore):
return res return res
async def market_book_add(self, symbol: str) -> bool: 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) res = await self._handler(api)
return res return res
async def market_book_get(self, symbol: str) -> tuple[BookInfo] | None: async def market_book_get(self, symbol: str) -> tuple[BookInfo] | None:
api = {"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) res = await self._handler(api)
return res return res
async def market_book_release(self, symbol: str) -> bool: async def market_book_release(self, symbol: str) -> bool:
api = {"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) res = await self._handler(api)
return res return res
async def copy_rates_from(self, symbol: str, timeframe: int, date_from: datetime | float, count: int) -> np.ndarray | None: async def copy_rates_from(
api = {"func": self._copy_rates_from, "args": (symbol, timeframe, date_from, count), "error_msg": f"Error in obtaining rates for {symbol}"} 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) res = await self._handler(api)
return res return res
@@ -270,18 +314,36 @@ class MetaTrader(MetaCore):
res = await self._handler(api) res = await self._handler(api)
return res return res
async def copy_rates_range(self, symbol: str, timeframe: int, date_from: datetime | float, date_to: datetime | float) -> np.ndarray | None: async def copy_rates_range(
api = {"func": self._copy_rates_range, "args": (symbol, timeframe, date_from, date_to), "error_msg": f"Error in obtaining rates for {symbol}"} 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) res = await self._handler(api)
return res return res
async def copy_ticks_from(self, symbol: str, date_from: datetime | float, count: int, flags: CopyTicks) -> np.ndarray | None: async def copy_ticks_from(
api = {"func": self._copy_ticks_from, "args": (symbol, date_from, count, flags), "error_msg": f"Error in obtaining ticks for {symbol}"} 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) res = await self._handler(api)
return res return res
async def copy_ticks_range(self, symbol: str, date_from: datetime | float, date_to: datetime | float, flags: CopyTicks) -> np.ndarray | None: async def copy_ticks_range(
api = {"func": self._copy_ticks_range, "args": (symbol, date_from, date_to, flags), "error_msg": f"Error in obtaining ticks for {symbol}"} 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) res = await self._handler(api)
return res return res
@@ -296,13 +358,24 @@ class MetaTrader(MetaCore):
res = await self._handler(api) res = await self._handler(api)
return res return res
async def order_calc_margin(self, action: Literal[OrderType.BUY, OrderType.SELL], symbol: str, volume: float, price: float) -> float | None: async def order_calc_margin(
api = {"func": self._order_calc_margin, "args": (action, symbol, volume, price), "error_msg": "Error in calculating 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) res = await self._handler(api)
return res return res
async def order_calc_profit( 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: ) -> float | None:
api = { api = {
"func": self._order_calc_profit, "func": self._order_calc_profit,
@@ -334,29 +407,57 @@ class MetaTrader(MetaCore):
return res return res
async def history_orders_total(self, date_from: datetime | float, date_to: datetime | float) -> int: 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) res = await self._handler(api)
return res return res
async def history_orders_get( 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: ) -> tuple[TradeOrder] | None:
kwargs = {key: value for key, value in (("group", group), ("ticket", ticket), ("position", position)) if value} kwargs = {key: value for key, value in (("group", group), ("ticket", ticket), ("position", position)) if value}
args = tuple(arg for arg in (date_from, date_to) if arg) 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) res = await self._handler(api)
return res return res
async def history_deals_total(self, date_from: datetime | float, date_to: datetime | float) -> int: 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) res = await self._handler(api)
return res return res
async def history_deals_get( 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: ) -> tuple[TradeDeal] | None:
kwargs = {key: value for key, value in (("group", group), ("ticket", ticket), ("position", position)) if value} kwargs = {key: value for key, value in (("group", group), ("ticket", ticket), ("position", position)) if value}
args = tuple(arg for arg in (date_from, date_to) if arg) 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) res = await self._handler(api)
return res return res
+16 -6
View File
@@ -27,8 +27,9 @@ class QueueItem:
if asyncio.iscoroutinefunction(self.task_item): if asyncio.iscoroutinefunction(self.task_item):
await self.task_item(*self.args, **self.kwargs) await self.task_item(*self.args, **self.kwargs)
except Exception as err: except Exception as err:
logger.error(f"Error {err} occurred in {self.task_item.__name__} with args {self.args} and kwargs" logger.error(
f" {self.kwargs}") f"Error {err} occurred in {self.task_item.__name__} with args {self.args} and kwargs" f" {self.kwargs}"
)
class TaskQueue: class TaskQueue:
@@ -57,9 +58,16 @@ class TaskQueue:
- `priority_tasks` (set): A set to store the QueueItems that must complete before the queue stops. - `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, def __init__(
on_exit: Literal["cancel", "complete_priority"] = "complete_priority", self,
mode: Literal["finite", "infinite"] = "infinite", worker_timeout: int = 60): 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.queue = queue or asyncio.PriorityQueue(maxsize=size)
self.workers = workers self.workers = workers
self.tasks = [] self.tasks = []
@@ -140,7 +148,9 @@ class TaskQueue:
await main_task await main_task
except TimeoutError: 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 self.stop = True
except asyncio.CancelledError as _: except asyncio.CancelledError as _:
+3 -1
View File
@@ -142,7 +142,9 @@ class BackTester:
""" """
[self.add_strategy(strategy=strategy) for strategy in strategies] [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. """Use this to run a single strategy on multiple symbols with the same parameters and keyword arguments.
Keyword Args: Keyword Args:
+9 -3
View File
@@ -1,4 +1,5 @@
import asyncio import asyncio
import time
from concurrent.futures import ProcessPoolExecutor from concurrent.futures import ProcessPoolExecutor
from typing import Type, Iterable, Callable, Coroutine from typing import Type, Iterable, Callable, Coroutine
import logging import logging
@@ -66,7 +67,9 @@ class Bot:
self.add_coroutine(coroutine=self.executor.exit) self.add_coroutine(coroutine=self.executor.exit)
if len(self.executor.strategy_runners) == 0: if len(self.executor.strategy_runners) == 0:
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: except Exception as err:
logger.error("%s: Bot initialization failed", err) logger.error("%s: Bot initialization failed", err)
raise SystemExit raise SystemExit
@@ -90,7 +93,8 @@ class Bot:
self.add_coroutine(coroutine=self.executor.exit) self.add_coroutine(coroutine=self.executor.exit)
if len(self.executor.strategy_runners) == 0: if len(self.executor.strategy_runners) == 0:
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: except Exception as err:
logger.error("%s: Bot initialization failed", err) logger.error("%s: Bot initialization failed", err)
raise SystemExit raise SystemExit
@@ -146,7 +150,9 @@ class Bot:
""" """
[self.add_strategy(strategy=strategy) for strategy in strategies] [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. """Use this to run a single strategy on multiple symbols with the same parameters and keyword arguments.
Keyword Args: Keyword Args:
+12 -9
View File
@@ -55,15 +55,18 @@ class Candle:
self.set_attributes(**kwargs) self.set_attributes(**kwargs)
def __repr__(self): def __repr__(self):
return "%(class)s(Index=%(Index)s, time=%(time)s, open=%(open)s, high=%(high)s, low=%(low)s, close=%(close)s)" % { return (
"class": self.__class__.__name__, "%(class)s(Index=%(Index)s, time=%(time)s, open=%(open)s, high=%(high)s, low=%(low)s, close=%(close)s)"
"open": self.open, % {
"high": self.high, "class": self.__class__.__name__,
"low": self.low, "open": self.open,
"close": self.close, "high": self.high,
"time": self.time, "low": self.low,
"Index": self.Index, "close": self.close,
} "time": self.time,
"Index": self.Index,
}
)
def __eq__(self, other: Self): def __eq__(self, other: Self):
return self.time == other.time return self.time == other.time
+6 -2
View File
@@ -34,7 +34,9 @@ class History:
total_orders: int total_orders: int
group: str 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: Args:
date_from (datetime, float): Date the orders are requested from. Set by the 'datetime' object or as a date_from (datetime, float): Date the orders are requested from. Set by the 'datetime' object or as a
@@ -122,4 +124,6 @@ class History:
def get_orders_by_position(self, *, position: int) -> tuple[TradeOrder, ...]: def get_orders_by_position(self, *, position: int) -> tuple[TradeOrder, ...]:
"""filter orders by position""" """filter orders by position"""
return tuple(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. volume (float): Volume to close.
order_type (OrderType): Order type. 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() return await order.send()
async def close_position_by_ticket(self, *, ticket: int) -> OrderSendResult | None: async def close_position_by_ticket(self, *, ticket: int) -> OrderSendResult | None:
@@ -119,5 +126,7 @@ class Positions:
int: Return number of positions closed. int: Return number of positions closed.
""" """
positions = self.positions or await self.get_positions() positions = self.positions or await self.get_positions()
results = await asyncio.gather(*(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)]) 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) return Duration(hours=hours, minutes=minutes, seconds=seconds)
async def close_positions(self, *, positions: tuple[TradePosition, ...]): async def close_positions(self, *, positions: tuple[TradePosition, ...]):
results = asyncio.gather(*(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 closed = pending = 0
for result in results: for result in results:
if isinstance(result, OrderSendResult) and result.retcode == 10009: if isinstance(result, OrderSendResult) and result.retcode == 10009:
+23 -4
View File
@@ -117,11 +117,15 @@ class Strategy(ABC):
else: else:
await self.live_sleep(secs=secs) 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: 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: if self.backtest_controller.parties == 2:
steps = int(secs) // self.config.backtest_engine.speed steps = int(secs) // self.config.backtest_engine.speed
steps = max(steps, 1) steps = max(steps, 1)
@@ -134,6 +138,21 @@ class Strategy(ABC):
self.backtest_controller.wait() self.backtest_controller.wait()
else: else:
self.backtest_controller.wait() 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: except Exception as err:
logger.error("Error: %s in backtest_sleep", 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: async def amount_in_quote_currency(self, *, amount: float) -> float:
"""Convert the amount to the quote currency of the symbol.""" """Convert the amount to the quote currency of the symbol."""
if self.currency_profit != self.account.currency: if self.currency_profit != self.account.currency:
amount = await self.convert_currency(amount=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 return amount
async def compute_volume(self) -> float: async def compute_volume(self) -> float:
@@ -257,7 +259,9 @@ class Symbol(_Base, SymbolInfo):
raise ValueError(f"Could not get rates for {self.name}.") raise ValueError(f"Could not get rates for {self.name}.")
@backoff_decorator @backoff_decorator
async def copy_rates_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. """Get bars in the specified date range from the MetaTrader 5 terminal.
Args: Args:
@@ -277,13 +281,17 @@ class Symbol(_Base, SymbolInfo):
Raises: Raises:
ValueError: If request was unsuccessful and None was returned ValueError: If request was unsuccessful and None was returned
""" """
rates = await self.mt5.copy_rates_range(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: if rates is not None:
return Candles(data=rates) return Candles(data=rates)
raise ValueError(f"Could not get rates for {self.name}.") raise ValueError(f"Could not get rates for {self.name}.")
@backoff_decorator @backoff_decorator
async def copy_ticks_from(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. 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}.") raise ValueError(f"Could not get ticks for {self.name}.")
@backoff_decorator @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. """Get ticks for the specified date range from the MetaTrader 5 terminal.
Args: Args:
+12 -9
View File
@@ -45,15 +45,18 @@ class Tick:
self.set_attributes(**kwargs) self.set_attributes(**kwargs)
def __repr__(self): def __repr__(self):
return "%(class)s(Index=%(Index)s, time=%(time)s, bid=%(bid)s, ask=%(ask)s, last=%(last)s, volume=%(volume)s)" % { return (
"class": self.__class__.__name__, "%(class)s(Index=%(Index)s, time=%(time)s, bid=%(bid)s, ask=%(ask)s, last=%(last)s, volume=%(volume)s)"
"time": self.time, % {
"bid": self.bid, "class": self.__class__.__name__,
"ask": self.ask, "time": self.time,
"last": self.last, "bid": self.bid,
"volume": self.volume, "ask": self.ask,
"Index": self.Index, "last": self.last,
} "volume": self.volume,
"Index": self.Index,
}
)
def __eq__(self, other: Self): def __eq__(self, other: Self):
return self.time == other.time return self.time == other.time
+6 -1
View File
@@ -114,7 +114,12 @@ class TradeRecords:
deals = [ deals = [
deal deal
for deal in deals 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) deals.sort(key=lambda deal: deal.time_msc)
deal = deals[-1] deal = deals[-1]
+17 -7
View File
@@ -82,7 +82,9 @@ class Trader(ABC):
elif self.order.type == OrderType.SELL: elif self.order.type == OrderType.SELL:
self.order.sl, self.order.tp = round(price + sl, self.symbol.digits), round(price - tp, digits) self.order.sl, self.order.tp = round(price + sl, self.symbol.digits), round(price - tp, digits)
async def create_order_with_stops(self, *, order_type: OrderType, sl: float, tp: float, amount_to_risk: float = None): async def create_order_with_stops(
self, *, order_type: OrderType, sl: float, tp: float, amount_to_risk: float = None
):
"""Create an order with stop loss and take profit levels. Use the amount to risk per trade to """Create an order with stop loss and take profit levels. Use the amount to risk per trade to
calculate the volume. calculate the volume.
@@ -100,7 +102,9 @@ class Trader(ABC):
volume = await self.symbol.compute_volume_sl(amount=amount, price=price, sl=sl) volume = await self.symbol.compute_volume_sl(amount=amount, price=price, sl=sl)
self.order.set_attributes(sl=sl, tp=tp, volume=volume, price=price, type=order_type) self.order.set_attributes(sl=sl, tp=tp, volume=volume, price=price, type=order_type)
async def create_order_with_sl(self, *, order_type: OrderType, sl: float, amount_to_risk: float = None, risk_to_reward: float = None): async def create_order_with_sl(
self, *, order_type: OrderType, sl: float, amount_to_risk: float = None, risk_to_reward: float = None
):
""" """
Create an order with a given stop_loss level. Use the amount to risk per trade to calculate the volume. 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) volume = await self.symbol.compute_volume_sl(amount=amount, price=price, sl=sl)
self.order.set_attributes(sl=sl, tp=tp, volume=volume, price=price, type=order_type) self.order.set_attributes(sl=sl, tp=tp, volume=volume, price=price, type=order_type)
async def create_order_with_points(self, *, order_type: OrderType, points: float, amount_to_risk: float = None, risk_to_reward: float = None): async def create_order_with_points(
self, *, order_type: OrderType, points: float, amount_to_risk: float = None, risk_to_reward: float = None
):
"""Create an order with specific points to risk. Use the amount to risk per trade to calculate the volume. """Create an order with specific points to risk. Use the amount to risk per trade to calculate the volume.
Args: Args:
@@ -167,18 +173,18 @@ class Trader(ABC):
return check return check
if check.retcode != 0: 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 return check
async def send_order(self) -> OrderSendResult | None: async def send_order(self) -> OrderSendResult | None:
"""Send the order to the broker.""" """Send the order to the broker."""
result = await self.order.send() result = await self.order.send()
if result is None: 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 return result
if result.retcode != 10009: 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
return result return result
@@ -195,7 +201,11 @@ class Trader(ABC):
params = {**parameters} or {} params = {**parameters} or {}
profit = await self.order.calc_profit() profit = await self.order.calc_profit()
params["expected_profit"] = profit params["expected_profit"] = profit
date = 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") params["date"] = date.strftime("%Y-%m-%d %H:%M:%S.%f")
res = Result(result=result, parameters=params, name=name) res = Result(result=result, parameters=params, name=name)
self.config.task_queue.add(item=QueueItem(res.save), must_complete=True) self.config.task_queue.add(item=QueueItem(res.save), must_complete=True)
+3 -1
View File
@@ -50,7 +50,9 @@ async def close_all_positions():
@pytest.fixture(scope="package", autouse=True) @pytest.fixture(scope="package", autouse=True)
async def config(request): async def config(request):
Path("tests/backtest/configs").mkdir(exist_ok=True) Path("tests/backtest/configs").mkdir(exist_ok=True)
with open("aiomql.json", "r") as fh, open("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 = json.load(fh)
data["mode"] = "backtest" data["mode"] = "backtest"
json.dump(data, fh1, indent=2) 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) deal = backtest_engine.deals.history_deals_get(position=bo.order)
bo_profit = deal[-1].profit bo_profit = deal[-1].profit
assert len(all_pos) == 1 assert len(all_pos) == 1
assert 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]) profit = sum([pos.profit for pos in all_pos])
n_balance = backtest_engine._account.balance n_balance = backtest_engine._account.balance
n_equity = backtest_engine._account.equity n_equity = backtest_engine._account.equity
+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) deals = backtest_engine.deals.history_deals_get(position=bo.order)
assert len(deals) <= 2 assert len(deals) <= 2
orders = backtest_engine.deals.get_deals_range(date_from=start, date_to=end) orders = backtest_engine.deals.get_deals_range(date_from=start, date_to=end)
assert 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) orders = backtest_engine.orders.history_orders_get(position=bo.order)
assert len(orders) <= 2 assert len(orders) <= 2
orders = backtest_engine.orders.get_orders_range(date_from=start, date_to=end) orders = backtest_engine.orders.get_orders_range(date_from=start, date_to=end)
assert 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) @pytest.fixture(scope="package", autouse=True)
async def config(request): async def config(request):
Path("tests/live/configs").mkdir(exist_ok=True) Path("tests/live/configs").mkdir(exist_ok=True)
with open("aiomql.json", "r") as fh, open("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) data = json.load(fh)
json.dump(data, fh1, indent=2) json.dump(data, fh1, indent=2)
json.dump(data, fh2, indent=2) json.dump(data, fh2, indent=2)
+13 -2
View File
@@ -36,7 +36,13 @@ class TestRecordsAndResults:
async def sell(self, mt): async def sell(self, mt):
sym = "BTCUSD" sym = "BTCUSD"
sym_info = await mt.symbol_info(sym) sym_info = await mt.symbol_info(sym)
return {"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) @pytest.fixture(scope="class", autouse=True)
async def setup(self, sell, buy, mt): 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 = Result(result=OrderSendResult(**sell_res._asdict()), name="test_result")
sell_res_2 = Result(result=OrderSendResult(**sell_res_2._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") 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() await Positions().close_all()
def test_records_dir(self): def test_records_dir(self):
+22 -5
View File
@@ -14,7 +14,13 @@ class TestBackTestEngine:
def setup_class(cls): def setup_class(cls):
cls.start = datetime(2024, 2, 1) cls.start = datetime(2024, 2, 1)
cls.end = datetime(2024, 2, 7) cls.end = datetime(2024, 2, 7)
cls.g_data = GetData(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) cls.bte = BackTestEngine(start=cls.start, end=cls.end, assign_to_config=True, preload=False)
@pytest.fixture(scope="class") @pytest.fixture(scope="class")
@@ -27,7 +33,13 @@ class TestBackTestEngine:
@pytest.fixture(scope="class") @pytest.fixture(scope="class")
async def sell_order(self): async def sell_order(self):
sym = await self.bte.get_symbol_info(symbol="BTCUSD") sym = await self.bte.get_symbol_info(symbol="BTCUSD")
request = {"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 return request
@pytest.fixture(scope="class") @pytest.fixture(scope="class")
@@ -47,7 +59,8 @@ class TestBackTestEngine:
} }
return request return request
def modify_stops(self, order): ... def modify_stops(self, order):
...
def test_span_and_range(self): def test_span_and_range(self):
assert self.bte.range == range(0, int((self.end - self.start).total_seconds()), self.bte.speed) 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) bte2.go_to(time=moment)
sym = "BTCUSD" sym = "BTCUSD"
sym_info = await self.bte.get_symbol_info(symbol=sym) sym_info = await self.bte.get_symbol_info(symbol=sym)
margin = await self.bte.order_calc_margin(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 assert margin > 0
sym_info2 = await self.bte.get_symbol_info(symbol=sym) sym_info2 = await self.bte.get_symbol_info(symbol=sym)
margin2 = await bte2.order_calc_margin(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 assert margin2 > 0
async def test_order_check(self, buy_order, sell_order): 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.end = datetime(2024, 2, 2, tzinfo=UTC)
cls.symbols = ["BTCUSD", "ETHUSD"] cls.symbols = ["BTCUSD", "ETHUSD"]
cls.timeframes = [TimeFrame.H1, TimeFrame.H2] cls.timeframes = [TimeFrame.H1, TimeFrame.H2]
cls.g_data = GetData(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) @pytest.fixture(scope="class", autouse=True)
async def get_data(self): async def get_data(self):