mirror of
https://github.com/Ichinga-Samuel/aiomql.git
synced 2026-08-08 17:57:46 +00:00
v4
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||

|
||||

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