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

|

|
||||||

|

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