mirror of
https://github.com/Ichinga-Samuel/aiomql.git
synced 2026-07-28 04:37:43 +00:00
v4
This commit is contained in:
@@ -43,6 +43,7 @@ htmlcov/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
.ruff_cache
|
||||
.pytest_cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import asyncio
|
||||
|
||||
|
||||
def run():
|
||||
for i in range(10):
|
||||
print('running')
|
||||
|
||||
|
||||
async def run_async():
|
||||
for i in range(10):
|
||||
await asyncio.sleep(1)
|
||||
print('running async')
|
||||
|
||||
|
||||
async def main():
|
||||
await run_async()
|
||||
run()
|
||||
|
||||
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,71 @@
|
||||
# Table of Contents
|
||||
|
||||
* [aiomql.contrib.symbols.forex\_symbol](#aiomql.contrib.symbols.forex_symbol)
|
||||
* [ForexSymbol](#aiomql.contrib.symbols.forex_symbol.ForexSymbol)
|
||||
* [pip](#aiomql.contrib.symbols.forex_symbol.ForexSymbol.pip)
|
||||
* [compute\_points](#aiomql.contrib.symbols.forex_symbol.ForexSymbol.compute_points)
|
||||
* [compute\_volume\_points](#aiomql.contrib.symbols.forex_symbol.ForexSymbol.compute_volume_points)
|
||||
|
||||
<a id="aiomql.contrib.symbols.forex_symbol"></a>
|
||||
|
||||
# aiomql.contrib.symbols.forex\_symbol
|
||||
|
||||
<a id="aiomql.contrib.symbols.forex_symbol.ForexSymbol"></a>
|
||||
|
||||
## ForexSymbol Objects
|
||||
|
||||
```python
|
||||
class ForexSymbol(Symbol)
|
||||
```
|
||||
|
||||
Subclass of Symbol for Forex Symbols. Handles the conversion of currency and the computation of stop loss,
|
||||
take profit and volume.
|
||||
|
||||
<a id="aiomql.contrib.symbols.forex_symbol.ForexSymbol.pip"></a>
|
||||
|
||||
#### pip
|
||||
|
||||
```python
|
||||
@property
|
||||
def pip()
|
||||
```
|
||||
|
||||
Returns the pip value of the symbol. This is ten times the point value for forex symbols.
|
||||
|
||||
**Returns**:
|
||||
|
||||
- `float` - The pip value of the symbol.
|
||||
|
||||
<a id="aiomql.contrib.symbols.forex_symbol.ForexSymbol.compute_points"></a>
|
||||
|
||||
#### compute\_points
|
||||
|
||||
```python
|
||||
def compute_points(*, amount: float, volume: float) -> float
|
||||
```
|
||||
|
||||
Compute the number of points required for a trade. Given the amount and the volume of the trade.
|
||||
|
||||
**Arguments**:
|
||||
|
||||
- `amount` _float_ - Amount to trade
|
||||
- `volume` _float_ - Volume to trade
|
||||
|
||||
<a id="aiomql.contrib.symbols.forex_symbol.ForexSymbol.compute_volume_points"></a>
|
||||
|
||||
#### compute\_volume\_points
|
||||
|
||||
```python
|
||||
async def compute_volume_points(*,
|
||||
amount: float,
|
||||
points: float,
|
||||
round_down: bool = False) -> float
|
||||
```
|
||||
|
||||
Compute the volume required for a trade. Given the amount and the number of points.
|
||||
|
||||
**Arguments**:
|
||||
|
||||
- `amount` _float_ - Amount to trade
|
||||
- `points` _float_ - Number of points
|
||||
- `round_down` - round down the computed volume to the nearest step default True
|
||||
@@ -38,14 +38,15 @@ The MetaTrader Class provides an asynchronous wrapper around the MetaTrader5 API
|
||||
- [history\_deals\_total](#history_deals_total)
|
||||
- [history\_deals\_get](#history_deals_get)
|
||||
|
||||
<a id="MetaTrader"></a>
|
||||
<a id="meta_trader.meta_trader"></a>
|
||||
### MetaTrader
|
||||
```python
|
||||
class MetaTrader(metaclass=BaseMeta)
|
||||
class MetaTrader(MetaCore)
|
||||
```
|
||||
The MetaTrader class is a wrapper around the MetaTrader terminal.
|
||||
It provides methods for connecting to the MetaTrader terminal and retrieving data from it.
|
||||
#### Attributes
|
||||
|
||||
#### Attributes:
|
||||
| Name | Type | Description | Default |
|
||||
|-------|-------|--------------------------------------------------------|------------------------|
|
||||
| error | Error | The last error encountered by the MetaTrader terminal. | Error(1, 'Successful') |
|
||||
@@ -62,7 +63,7 @@ async def __aenter__() -> 'MetaTrader'
|
||||
Async context manager entry point.
|
||||
Initializes the connection to the MetaTrader terminal.
|
||||
|
||||
#### Returns
|
||||
#### Returns:
|
||||
| Type | Description |
|
||||
|--------------|-------------------------------------|
|
||||
| `MetaTrader` | An instance of the MetaTrader class |
|
||||
@@ -621,4 +622,4 @@ Call without parameters. Return closed deals on all symbols
|
||||
#### Returns
|
||||
| Type | Description |
|
||||
|--------------------|----------------------------------------------------|
|
||||
| `tuple[TradeDeal]` | A tuple of closed trade deals as TradeDeal objects |
|
||||
| `tuple[TradeDeal]` | A tuple of closed trade deals as TradeDeal objects |
|
||||
|
||||
@@ -1,4 +1,2 @@
|
||||
line-length = 150
|
||||
target-version = "py311"
|
||||
[format]
|
||||
skip-magic-trailing-comma=true
|
||||
|
||||
+3
-11
@@ -12,16 +12,8 @@ from aiomql.core.backtesting import BackTestEngine
|
||||
async def back_tester():
|
||||
config = 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",
|
||||
]
|
||||
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s")
|
||||
syms = ["Volatility 75 Index", "Volatility 100 Index", "Volatility 25 Index", "Volatility 10 Index"]
|
||||
symbols = [ForexSymbol(name=sym) for sym in syms]
|
||||
strategies = [Chaos(symbol=symbol) for symbol in symbols]
|
||||
start = datetime(2024, 5, 1, tzinfo=UTC)
|
||||
@@ -35,7 +27,7 @@ async def back_tester():
|
||||
close_open_positions_on_exit=True,
|
||||
assign_to_config=True,
|
||||
preload=True,
|
||||
account_info={'balance': 350}
|
||||
account_info={"balance": 350},
|
||||
)
|
||||
backtester = BackTester(backtest_engine=back_test_engine)
|
||||
backtester.add_strategies(strategies=strategies)
|
||||
|
||||
+1
-4
@@ -6,10 +6,7 @@ from aiomql.contrib.symbols import ForexSymbol
|
||||
|
||||
|
||||
def chaos_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"]
|
||||
symbols = [ForexSymbol(name=sym) for sym in syms]
|
||||
strategies = [Chaos(symbol=symbol) for symbol in symbols]
|
||||
|
||||
+7
-26
@@ -1,4 +1,5 @@
|
||||
"""Utility functions for aiomql."""
|
||||
|
||||
import decimal
|
||||
import random
|
||||
from functools import wraps, partial
|
||||
@@ -26,13 +27,9 @@ def dict_to_string(data: dict, multi=False) -> str:
|
||||
return f"{sep}".join(f"{key}: {value}" for key, value in data.items())
|
||||
|
||||
|
||||
def backoff_decorator(
|
||||
func=None, *, max_retries: int = 2, retries: int = 0, error=""
|
||||
) -> callable:
|
||||
def backoff_decorator(func=None, *, max_retries: int = 2, retries: int = 0, error="") -> callable:
|
||||
if func is None:
|
||||
return partial(
|
||||
backoff_decorator, max_retries=max_retries, retries=retries, error=error
|
||||
)
|
||||
return partial(backoff_decorator, max_retries=max_retries, retries=retries, error=error)
|
||||
|
||||
@wraps(func)
|
||||
async def wrapper(*args, **kwargs):
|
||||
@@ -57,17 +54,9 @@ def backoff_decorator(
|
||||
return wrapper
|
||||
|
||||
|
||||
def error_handler(
|
||||
func=None, *, msg="", exe=Exception, response=None, log_error_msg=True
|
||||
):
|
||||
def error_handler(func=None, *, msg="", exe=Exception, response=None, log_error_msg=True):
|
||||
if func is None:
|
||||
return partial(
|
||||
error_handler,
|
||||
msg=msg,
|
||||
exe=exe,
|
||||
response=response,
|
||||
log_error_msg=log_error_msg,
|
||||
)
|
||||
return partial(error_handler, msg=msg, exe=exe, response=response, log_error_msg=log_error_msg)
|
||||
|
||||
@wraps(func)
|
||||
async def wrapper(*args, **kwargs):
|
||||
@@ -82,17 +71,9 @@ def error_handler(
|
||||
return wrapper
|
||||
|
||||
|
||||
def error_handler_sync(
|
||||
func=None, *, msg="", exe=Exception, response=None, log_error_msg=True
|
||||
):
|
||||
def error_handler_sync(func=None, *, msg="", exe=Exception, response=None, log_error_msg=True):
|
||||
if func is None:
|
||||
return partial(
|
||||
error_handler,
|
||||
msg=msg,
|
||||
exe=exe,
|
||||
response=response,
|
||||
log_error_msg=log_error_msg,
|
||||
)
|
||||
return partial(error_handler, msg=msg, exe=exe, response=response, log_error_msg=log_error_msg)
|
||||
|
||||
@wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
|
||||
@@ -3,21 +3,11 @@ from ...lib.candle import Candle, Candles
|
||||
|
||||
def find_bearish_fractal(candles: Candles) -> Candle | None:
|
||||
for i in range(len(candles) - 3, 1, -1):
|
||||
if candles[i].high > max(
|
||||
candles[i - 1].high,
|
||||
candles[i + 1].high,
|
||||
candles[i - 2].high,
|
||||
candles[i + 2].high,
|
||||
):
|
||||
if candles[i].high > max(candles[i - 1].high, candles[i + 1].high, candles[i - 2].high, candles[i + 2].high):
|
||||
return candles[i]
|
||||
|
||||
|
||||
def find_bullish_fractal(candles: Candles) -> Candle | None:
|
||||
for i in range(len(candles) - 3, 1, -1):
|
||||
if candles[i].low < min(
|
||||
candles[i - 1].low,
|
||||
candles[i + 1].low,
|
||||
candles[i - 2].low,
|
||||
candles[i + 2].low,
|
||||
):
|
||||
if candles[i].low < min(candles[i - 1].low, candles[i + 1].low, candles[i - 2].low, candles[i + 2].low):
|
||||
return candles[i]
|
||||
|
||||
@@ -20,70 +20,40 @@ class Chaos(Strategy):
|
||||
fast_ema: int
|
||||
slow_ema: int
|
||||
tracker: Tracker
|
||||
parameters = {
|
||||
"fast_ema": 8,
|
||||
"slow_ema": 20,
|
||||
"ltf": TimeFrame.M1,
|
||||
"htf": TimeFrame.M2,
|
||||
"lcc": 100,
|
||||
"hcc": 100,
|
||||
}
|
||||
parameters = {"fast_ema": 8, "slow_ema": 20, "ltf": TimeFrame.M1, "htf": TimeFrame.M2, "lcc": 100, "hcc": 100}
|
||||
|
||||
def __init__(
|
||||
self, *, symbol: ForexSymbol, params: dict = None, sessions=None, name="Chaos"
|
||||
):
|
||||
def __init__(self, *, symbol: ForexSymbol, params: dict = None, sessions=None, name="Chaos"):
|
||||
super().__init__(symbol=symbol, params=params, sessions=sessions, name=name)
|
||||
self.tracker = Tracker(snooze=self.ltf.seconds)
|
||||
self.trader = ScalpTrader(symbol=self.symbol)
|
||||
|
||||
async def check_trend(self):
|
||||
try:
|
||||
candles = await self.symbol.copy_rates_from_pos(
|
||||
timeframe=self.htf, count=self.hcc
|
||||
)
|
||||
if (
|
||||
(current := candles[-1])
|
||||
and current.time < self.tracker.trend_time
|
||||
and current.close == self.tracker.last_trend_price
|
||||
):
|
||||
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:
|
||||
self.tracker.update(new=False, order_type=None, snooze=5)
|
||||
return
|
||||
self.tracker.update(
|
||||
new=True, trend_time=current.time, last_trend_price=current.close
|
||||
)
|
||||
self.tracker.update(new=True, trend_time=current.time, last_trend_price=current.close)
|
||||
candles.ta.ema(length=self.slow_ema, append=True, fillna=0)
|
||||
candles.ta.ema(length=self.fast_ema, append=True, fillna=0)
|
||||
candles.rename(
|
||||
inplace=True,
|
||||
**{f"EMA_{self.fast_ema}": "fast", f"EMA_{self.slow_ema}": "slow"},
|
||||
)
|
||||
candles.rename(inplace=True, **{f"EMA_{self.fast_ema}": "fast", f"EMA_{self.slow_ema}": "slow"})
|
||||
order_type = random.choice([OrderType.BUY, OrderType.SELL])
|
||||
if order_type == OrderType.BUY:
|
||||
self.tracker.update(
|
||||
trend="bullish", snooze=self.htf.seconds, order_type=OrderType.BUY
|
||||
)
|
||||
self.tracker.update(trend="bullish", snooze=self.htf.seconds, order_type=OrderType.BUY)
|
||||
else:
|
||||
self.tracker.update(
|
||||
trend="bearish", snooze=self.htf.seconds, order_type=OrderType.SELL
|
||||
)
|
||||
self.tracker.update(trend="bearish", snooze=self.htf.seconds, order_type=OrderType.SELL)
|
||||
except Exception as err:
|
||||
logger.error(f"{err}. Failed to check trend")
|
||||
self.tracker.update(
|
||||
trend="ranging", snooze=self.ltf.seconds, order_type=None
|
||||
)
|
||||
self.tracker.update(trend="ranging", snooze=self.ltf.seconds, order_type=None)
|
||||
|
||||
async def trade(self):
|
||||
try:
|
||||
await self.check_trend()
|
||||
if self.tracker.order_type is not None:
|
||||
await self.trader.place_trade(
|
||||
order_type=self.tracker.order_type, parameters=self.parameters
|
||||
)
|
||||
await self.trader.place_trade(order_type=self.tracker.order_type, parameters=self.parameters)
|
||||
self.tracker.update(order_type=None)
|
||||
await self.sleep(secs=self.tracker.snooze)
|
||||
else:
|
||||
await self.sleep(secs=self.tracker.snooze)
|
||||
except Exception as err:
|
||||
logger.error(
|
||||
f"{err}. Failed to trade {self.symbol.name} with {self.__class__.__name__}"
|
||||
)
|
||||
logger.error(f"{err}. Failed to trade {self.symbol.name} with {self.__class__.__name__}")
|
||||
|
||||
@@ -24,46 +24,23 @@ class FingerTrap(Strategy):
|
||||
tcc: int
|
||||
trader: Trader
|
||||
tracker: Tracker
|
||||
parameters = {
|
||||
"fast_ema": 8,
|
||||
"slow_ema": 20,
|
||||
"etf": TimeFrame.M5,
|
||||
"ttf": TimeFrame.H1,
|
||||
"entry_ema": 5,
|
||||
"tcc": 672,
|
||||
"ecc": 3360,
|
||||
}
|
||||
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",
|
||||
):
|
||||
def __init__(self, *, symbol: Symbol, params: dict | None = None, trader: Trader = None, sessions: Sessions = None, name: str = "FingerTrap"):
|
||||
super().__init__(symbol=symbol, params=params, sessions=sessions, name=name)
|
||||
self.trader = trader or SimpleTrader(symbol=self.symbol)
|
||||
self.tracker: Tracker = Tracker(snooze=self.ttf.seconds)
|
||||
|
||||
async def check_trend(self):
|
||||
try:
|
||||
candles: Candles = await self.symbol.copy_rates_from_pos(
|
||||
timeframe=self.ttf, count=self.tcc
|
||||
)
|
||||
candles: Candles = await self.symbol.copy_rates_from_pos(timeframe=self.ttf, count=self.tcc)
|
||||
if (current := candles[-1]) and current.time < self.tracker.trend_time:
|
||||
self.tracker.update(new=False, order_type=None)
|
||||
return
|
||||
self.tracker.update(
|
||||
new=True, trend_time=current.time, last_trend_price=current.close
|
||||
)
|
||||
self.tracker.update(new=True, trend_time=current.time, last_trend_price=current.close)
|
||||
candles.ta.ema(length=self.slow_ema, append=True, fillna=0)
|
||||
candles.ta.ema(length=self.fast_ema, append=True, fillna=0)
|
||||
candles.rename(
|
||||
inplace=True,
|
||||
**{f"EMA_{self.fast_ema}": "fast", f"EMA_{self.slow_ema}": "slow"},
|
||||
)
|
||||
candles.rename(inplace=True, **{f"EMA_{self.fast_ema}": "fast", f"EMA_{self.slow_ema}": "slow"})
|
||||
|
||||
fas = candles.ta_lib.above(candles.fast, candles.slow)
|
||||
fbs = candles.ta_lib.below(candles.fast, candles.slow)
|
||||
@@ -75,52 +52,34 @@ class FingerTrap(Strategy):
|
||||
elif fbs.iloc[-1] and cbf.iloc[-1] and current.is_bearish():
|
||||
self.tracker.update(trend="bearish")
|
||||
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
|
||||
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)
|
||||
|
||||
async def confirm_trend(self):
|
||||
try:
|
||||
candles = await self.symbol.copy_rates_from_pos(
|
||||
timeframe=self.etf, count=self.ecc
|
||||
)
|
||||
candles = await self.symbol.copy_rates_from_pos(timeframe=self.etf, count=self.ecc)
|
||||
if (current := candles[-1]) and current.time < self.tracker.entry_time:
|
||||
self.tracker.update(new=False, order_type=None)
|
||||
return
|
||||
self.tracker.update(
|
||||
new=True, trend_time=current.time, last_entry_price=current.close
|
||||
)
|
||||
self.tracker.update(new=True, trend_time=current.time, last_entry_price=current.close)
|
||||
candles.ta.ema(length=self.entry_ema, append=True)
|
||||
candles.rename(**{f"EMA_{self.entry_ema}": "ema"})
|
||||
candles["cae"] = candles.ta_lib.cross(candles.close, candles.ema)
|
||||
candles["cbe"] = candles.ta_lib.cross(
|
||||
candles.close, candles.ema, above=False
|
||||
)
|
||||
candles["cbe"] = candles.ta_lib.cross(candles.close, candles.ema, above=False)
|
||||
current = candles[-1]
|
||||
if (
|
||||
self.tracker.bullish and True or current.cae
|
||||
): # change True to current.cae
|
||||
if self.tracker.bullish and True or current.cae: # change True to current.cae
|
||||
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:
|
||||
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:
|
||||
self.tracker.update(snooze=self.etf.seconds, order_type=None)
|
||||
except Exception as err:
|
||||
logger.error(
|
||||
f"{err} for {self.symbol} in {self.__class__.__name__}.confirm_trend"
|
||||
)
|
||||
logger.error(f"{err} for {self.symbol} in {self.__class__.__name__}.confirm_trend")
|
||||
self.tracker.update(snooze=self.etf.seconds, order_type=None)
|
||||
|
||||
async def watch_market(self):
|
||||
@@ -138,11 +97,7 @@ class FingerTrap(Strategy):
|
||||
if self.tracker.order_type is None:
|
||||
await self.sleep(secs=self.tracker.snooze)
|
||||
return
|
||||
await self.trader.place_trade(
|
||||
order_type=self.tracker.order_type,
|
||||
parameters=self.parameters,
|
||||
sl=self.tracker.sl,
|
||||
)
|
||||
await self.trader.place_trade(order_type=self.tracker.order_type, parameters=self.parameters, sl=self.tracker.sl)
|
||||
await self.sleep(secs=self.tracker.snooze)
|
||||
except Exception as err:
|
||||
logger.error(f"{err} For {self.symbol} in {self.__class__.__name__}.trade")
|
||||
|
||||
@@ -24,9 +24,7 @@ class ForexSymbol(Symbol):
|
||||
points = amount / (volume * self.point * self.trade_contract_size)
|
||||
return points
|
||||
|
||||
async def compute_volume_points(
|
||||
self, *, amount: float, points: float, round_down: bool = False
|
||||
) -> float:
|
||||
async def compute_volume_points(self, *, amount: float, points: float, round_down: bool = False) -> float:
|
||||
"""Compute the volume required for a trade. Given the amount and the number of points.
|
||||
|
||||
Args:
|
||||
@@ -37,8 +35,6 @@ class ForexSymbol(Symbol):
|
||||
volume = amount / (self.point * points * self.trade_contract_size)
|
||||
return self.round_off_volume(volume=volume, round_down=round_down)
|
||||
|
||||
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:
|
||||
volume = amount / (abs(price - sl) * self.trade_contract_size)
|
||||
return self.round_off_volume(volume=volume, round_down=round_down)
|
||||
|
||||
@@ -7,9 +7,7 @@ logger = getLogger(__name__)
|
||||
|
||||
|
||||
class ScalpTrader(Trader):
|
||||
async def place_trade(
|
||||
self, *, order_type: OrderType, volume: float = None, parameters: dict = None
|
||||
):
|
||||
async def place_trade(self, *, order_type: OrderType, volume: float = None, parameters: dict = None):
|
||||
"""Places a trade based on the order_type and a given stop_loss
|
||||
|
||||
Args:
|
||||
@@ -28,6 +26,4 @@ class ScalpTrader(Trader):
|
||||
if res is not None:
|
||||
await self.record_trade(result=res, parameters=self.parameters)
|
||||
except Exception as err:
|
||||
logger.error(
|
||||
f"{err} in {self.__class__.__name__}.place_trade for {self.symbol.name}"
|
||||
)
|
||||
logger.error(f"{err} in {self.__class__.__name__}.place_trade for {self.symbol.name}")
|
||||
|
||||
@@ -7,9 +7,7 @@ logger = getLogger(__name__)
|
||||
|
||||
|
||||
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
|
||||
|
||||
Args:
|
||||
@@ -25,6 +23,4 @@ class SimpleTrader(Trader):
|
||||
self.order.comment = self.parameters.get("name", self.__class__.__name__)
|
||||
await self.send_order()
|
||||
except Exception as err:
|
||||
logger.error(
|
||||
f"{err} in {self.__class__.__name__}.place_trade for {self.symbol.name}"
|
||||
)
|
||||
logger.error(f"{err} in {self.__class__.__name__}.place_trade for {self.symbol.name}")
|
||||
|
||||
@@ -294,21 +294,9 @@ types = (
|
||||
class BaseMeta(type):
|
||||
def __new__(mcs, cls_name, bases, cls_dict):
|
||||
defaults: dict = getattr(MetaTrader5, "__dict__", {})
|
||||
callables = {
|
||||
f"_{key}": value
|
||||
for key in core_mt5_functions
|
||||
if (value := defaults.get(key, None)) is not None
|
||||
}
|
||||
consts = {
|
||||
key: value
|
||||
for key in constants
|
||||
if (value := defaults.get(key, None)) is not None
|
||||
}
|
||||
types_ = {
|
||||
key: value
|
||||
for key in types
|
||||
if (value := defaults.get(key, None)) is not None
|
||||
}
|
||||
callables = {f"_{key}": value for key in core_mt5_functions if (value := defaults.get(key, None)) is not None}
|
||||
consts = {key: value for key in constants if (value := defaults.get(key, None)) is not None}
|
||||
types_ = {key: value for key in types if (value := defaults.get(key, None)) is not None}
|
||||
cls_dict |= callables
|
||||
cls_dict |= consts
|
||||
cls_dict |= types_
|
||||
|
||||
@@ -52,24 +52,14 @@ class BackTestController:
|
||||
self.backtest_engine.next()
|
||||
while True:
|
||||
pending = self.wait()
|
||||
if (
|
||||
pending == 0
|
||||
): # all main tasks have been completed in the current cycle
|
||||
if pending == 0: # all main tasks have been completed in the current cycle
|
||||
await self.backtest_engine.tracker()
|
||||
self.backtest_engine.next()
|
||||
if self.backtest_engine.cursor.time % 3600 == 0:
|
||||
logger.info(
|
||||
datetime.strftime(
|
||||
datetime.fromtimestamp(
|
||||
self.backtest_engine.cursor.time
|
||||
),
|
||||
"%Y-%m-%d %H:%M:%S",
|
||||
)
|
||||
)
|
||||
logger.info(datetime.strftime(datetime.fromtimestamp(self.backtest_engine.cursor.time), "%Y-%m-%d %H:%M:%S"))
|
||||
if self.backtest_engine.stop_testing:
|
||||
logger.info(
|
||||
"Stop trading called in control at %s",
|
||||
datetime.fromtimestamp(self.backtest_engine.cursor.time).strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"Stop trading called in control at %s", datetime.fromtimestamp(self.backtest_engine.cursor.time).strftime("%Y-%m-%d %H:%M:%S")
|
||||
)
|
||||
break
|
||||
await self.backtest_engine.wrap_up()
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -58,15 +58,7 @@ class BackTestData:
|
||||
class GetData:
|
||||
data: BackTestData
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
start: datetime,
|
||||
end: datetime,
|
||||
symbols: Sequence[str],
|
||||
timeframes: Sequence[TimeFrame],
|
||||
name: str = "",
|
||||
):
|
||||
def __init__(self, *, start: datetime, end: datetime, symbols: Sequence[str], timeframes: Sequence[TimeFrame], name: str = ""):
|
||||
""""""
|
||||
self.config = Config()
|
||||
self.start = start.astimezone(tz=UTC)
|
||||
@@ -102,14 +94,8 @@ class GetData:
|
||||
logger.error(f"Error: {err}")
|
||||
|
||||
def save_data(self, *, name: str | Path = ""):
|
||||
name = name or (
|
||||
self.name + ".pkl" if not self.name.endswith(".pkl") else self.name
|
||||
)
|
||||
name = (
|
||||
Path(self.config.backtest_dir) / name
|
||||
if not isinstance(name, Path)
|
||||
else name
|
||||
)
|
||||
name = name or (self.name + ".pkl" if not self.name.endswith(".pkl") else self.name)
|
||||
name = Path(self.config.backtest_dir) / name if not isinstance(name, Path) else name
|
||||
with open(name, "wb") as fo:
|
||||
pickle.dump(self.data, fo, protocol=pickle.HIGHEST_PROTOCOL)
|
||||
|
||||
@@ -118,26 +104,15 @@ class GetData:
|
||||
if workers:
|
||||
self.task_queue.workers = workers
|
||||
|
||||
q_items = [
|
||||
QueueItem(self.get_symbols_rates),
|
||||
QueueItem(self.get_symbols_ticks),
|
||||
QueueItem(self.get_symbols_info),
|
||||
]
|
||||
q_items = [QueueItem(self.get_symbols_rates), QueueItem(self.get_symbols_ticks), QueueItem(self.get_symbols_info)]
|
||||
|
||||
[
|
||||
self.task_queue.add(item=item, priority=0, must_complete=True)
|
||||
for item in q_items
|
||||
]
|
||||
[self.task_queue.add(item=item, priority=0, must_complete=True) for item in q_items]
|
||||
|
||||
if not self.data.account:
|
||||
self.task_queue.add(
|
||||
item=QueueItem(self.get_account_info), must_complete=True
|
||||
)
|
||||
self.task_queue.add(item=QueueItem(self.get_account_info), must_complete=True)
|
||||
|
||||
if not self.data.terminal:
|
||||
self.task_queue.add(
|
||||
item=QueueItem(self.get_terminal_info), must_complete=True
|
||||
)
|
||||
self.task_queue.add(item=QueueItem(self.get_terminal_info), must_complete=True)
|
||||
|
||||
if not self.data.version:
|
||||
self.task_queue.add(item=QueueItem(self.get_version), must_complete=True)
|
||||
@@ -146,9 +121,7 @@ class GetData:
|
||||
|
||||
if self.data.fully_loaded is False:
|
||||
logger.warning("Data not fully loaded")
|
||||
self.data = BackTestData(
|
||||
name=self.name, span=self.span, range=self.range, fully_loaded=False
|
||||
)
|
||||
self.data = BackTestData(name=self.name, span=self.span, range=self.range, fully_loaded=False)
|
||||
|
||||
async def get_terminal_info(self):
|
||||
""""""
|
||||
@@ -179,29 +152,16 @@ class GetData:
|
||||
|
||||
async def get_symbols_info(self):
|
||||
""""""
|
||||
[
|
||||
self.task_queue.add(item=QueueItem(self.get_symbol_info, symbol=symbol))
|
||||
for symbol in self.symbols
|
||||
if self.data.symbols.get(symbol) is None
|
||||
]
|
||||
[self.task_queue.add(item=QueueItem(self.get_symbol_info, symbol=symbol)) for symbol in self.symbols if self.data.symbols.get(symbol) is None]
|
||||
|
||||
async def get_symbols_ticks(self):
|
||||
""""""
|
||||
[
|
||||
self.task_queue.add(item=QueueItem(self.get_symbol_ticks, symbol=symbol))
|
||||
for symbol in self.symbols
|
||||
if self.data.ticks.get(symbol) is None
|
||||
]
|
||||
[self.task_queue.add(item=QueueItem(self.get_symbol_ticks, symbol=symbol)) for symbol in self.symbols if self.data.ticks.get(symbol) is None]
|
||||
|
||||
async def get_symbols_rates(self):
|
||||
""""""
|
||||
[
|
||||
self.task_queue.add(
|
||||
item=QueueItem(
|
||||
self.get_symbol_rates, symbol=symbol, timeframe=timeframe
|
||||
),
|
||||
priority=4,
|
||||
)
|
||||
self.task_queue.add(item=QueueItem(self.get_symbol_rates, symbol=symbol, timeframe=timeframe), priority=4)
|
||||
for symbol in self.symbols
|
||||
for timeframe in self.timeframes
|
||||
if self.data.rates.get(symbol, {}).get(timeframe) is None
|
||||
@@ -219,9 +179,7 @@ class GetData:
|
||||
@backoff_decorator
|
||||
async def get_symbol_ticks(self, *, symbol: str):
|
||||
""""""
|
||||
res = await self.mt5.copy_ticks_range(
|
||||
symbol, self.start, self.end, MetaTrader5.COPY_TICKS_ALL
|
||||
)
|
||||
res = await self.mt5.copy_ticks_range(symbol, self.start, self.end, MetaTrader5.COPY_TICKS_ALL)
|
||||
if res is None:
|
||||
self.data.fully_loaded = False
|
||||
self.task_queue.stop_queue()
|
||||
|
||||
@@ -66,13 +66,9 @@ class PositionsManager(TradeManager):
|
||||
_open_positions: set[int]
|
||||
margins: dict[int, float]
|
||||
|
||||
def __init__(
|
||||
self, *, data: dict = None, open_positions: set = None, margins: dict = None
|
||||
):
|
||||
def __init__(self, *, data: dict = None, open_positions: set = None, margins: dict = None):
|
||||
super().__init__(data=data)
|
||||
self._open_positions = open_positions or {
|
||||
trade.ticket for trade in self._data.values()
|
||||
}
|
||||
self._open_positions = open_positions or {trade.ticket for trade in self._data.values()}
|
||||
self.margins: dict[int, float] = margins or dict()
|
||||
|
||||
def __len__(self):
|
||||
@@ -113,22 +109,12 @@ class PositionsManager(TradeManager):
|
||||
def set_margin(self, *, ticket: int, margin: float):
|
||||
self.margins[ticket] = margin
|
||||
|
||||
def positions_get(
|
||||
self, *, ticket: int = None, symbol: str = None, group: None = None
|
||||
) -> tuple[TradePosition, ...]:
|
||||
def positions_get(self, *, ticket: int = None, symbol: str = None, group: None = None) -> tuple[TradePosition, ...]:
|
||||
if ticket:
|
||||
return tuple(
|
||||
position
|
||||
for position in self.open_positions
|
||||
if position.ticket == ticket
|
||||
)
|
||||
return tuple(position for position in self.open_positions if position.ticket == ticket)
|
||||
|
||||
if symbol:
|
||||
return tuple(
|
||||
position
|
||||
for position in self.open_positions
|
||||
if position.symbol == symbol
|
||||
)
|
||||
return tuple(position for position in self.open_positions if position.symbol == symbol)
|
||||
|
||||
if group:
|
||||
return self.open_positions
|
||||
@@ -143,33 +129,19 @@ class PositionsManager(TradeManager):
|
||||
|
||||
@property
|
||||
def open_positions(self) -> tuple[TradePosition, ...]:
|
||||
return tuple(
|
||||
position
|
||||
for position in self.values()
|
||||
if position.ticket in self._open_positions
|
||||
)
|
||||
return tuple(position for position in self.values() if position.ticket in self._open_positions)
|
||||
|
||||
|
||||
class OrdersManager(TradeManager):
|
||||
_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, ...]:
|
||||
start = date_from.timestamp() if isinstance(date_from, datetime) else date_from
|
||||
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,
|
||||
group: str = "",
|
||||
ticket: int = None,
|
||||
position: int = None,
|
||||
self, *, date_from: float | datetime = None, date_to: float | datetime = None, group: str = "", ticket: int = None, position: int = None
|
||||
) -> tuple[TradeOrder, ...]:
|
||||
if date_from and date_to:
|
||||
orders = self.get_orders_range(date_from=date_from, date_to=date_to)
|
||||
@@ -181,36 +153,24 @@ class OrdersManager(TradeManager):
|
||||
return tuple(order for order in self.values() if order.ticket == ticket)
|
||||
|
||||
if position:
|
||||
return tuple(
|
||||
order for order in self.values() if order.position_id == position
|
||||
)
|
||||
return tuple(order for order in self.values() if order.position_id == position)
|
||||
|
||||
return ()
|
||||
|
||||
def history_orders_total(
|
||||
self, *, date_from: datetime | float, date_to: datetime | float
|
||||
) -> int:
|
||||
def history_orders_total(self, *, date_from: datetime | float, date_to: datetime | float) -> int:
|
||||
return len(self.get_orders_range(date_from=date_from, date_to=date_to))
|
||||
|
||||
|
||||
class DealsManager(TradeManager):
|
||||
_data = dict[int, TradeDeal]
|
||||
|
||||
def get_deals_range(
|
||||
self, *, date_from: float, date_to: float
|
||||
) -> tuple[TradeData, ...]:
|
||||
def get_deals_range(self, *, date_from: float, date_to: float) -> tuple[TradeData, ...]:
|
||||
start = date_from.timestamp() if isinstance(date_from, datetime) else date_from
|
||||
end = date_to.timestamp() if isinstance(date_to, datetime) else date_to
|
||||
return tuple(deal for deal in self.values() if start <= deal.time <= end)
|
||||
|
||||
def history_deals_get(
|
||||
self,
|
||||
*,
|
||||
date_from: float | datetime = None,
|
||||
date_to: float | datetime = None,
|
||||
group: str = "",
|
||||
ticket: int = None,
|
||||
position: int = None,
|
||||
self, *, date_from: float | datetime = None, date_to: float | datetime = None, group: str = "", ticket: int = None, position: int = None
|
||||
) -> tuple[TradeDeal, ...]:
|
||||
if date_from and date_to:
|
||||
deals = self.get_deals_range(date_from=date_from, date_to=date_to)
|
||||
@@ -226,7 +186,5 @@ class DealsManager(TradeManager):
|
||||
|
||||
return ()
|
||||
|
||||
def history_deals_total(
|
||||
self, *, date_from: datetime | float, date_to: datetime | float
|
||||
) -> int:
|
||||
def history_deals_total(self, *, date_from: datetime | float, date_to: datetime | float) -> int:
|
||||
return len(self.get_deals_range(date_from=date_from, date_to=date_to))
|
||||
|
||||
+9
-43
@@ -24,32 +24,14 @@ class Base:
|
||||
Args:
|
||||
**kwargs: Set instance attributes with keyword arguments. Only if they are annotated on the class body.
|
||||
"""
|
||||
self.exclude = {
|
||||
"mt5",
|
||||
"config",
|
||||
"exclude",
|
||||
"include",
|
||||
"annotations",
|
||||
"class_vars",
|
||||
"dict",
|
||||
"_instance",
|
||||
}
|
||||
self.exclude = {"mt5", "config", "exclude", "include", "annotations", "class_vars", "dict", "_instance"}
|
||||
self.include = set()
|
||||
self.set_attributes(**kwargs)
|
||||
|
||||
def __repr__(self):
|
||||
kv = [
|
||||
(k, v)
|
||||
for k, v in self.__dict__.items()
|
||||
if not k.startswith("_")
|
||||
and (type(v) in (int, float, str) or isinstance(v, enum.Enum))
|
||||
]
|
||||
kv = [(k, v) for k, v in self.__dict__.items() if not k.startswith("_") and (type(v) in (int, float, str) or isinstance(v, enum.Enum))]
|
||||
args = ", ".join("%s=%s" % (i, j) for i, j in kv[:3])
|
||||
args = (
|
||||
args
|
||||
if len(kv) <= 3
|
||||
else args + " ... " + ", ".join("%s=%s" % (i, j) for i, j in kv[-1:])
|
||||
)
|
||||
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}
|
||||
|
||||
def set_attributes(self, **kwargs):
|
||||
@@ -68,21 +50,15 @@ class Base:
|
||||
try:
|
||||
setattr(self, i, self.annotations[i](j))
|
||||
except KeyError:
|
||||
logger.debug(
|
||||
f"Attribute {i} does not belong to class {self.__class__.__name__}"
|
||||
)
|
||||
logger.debug(f"Attribute {i} does not belong to class {self.__class__.__name__}")
|
||||
continue
|
||||
|
||||
except (ValueError, TypeError):
|
||||
logger.debug(
|
||||
f"Cannot covert object of type {type(j)} to type {self.annotations[i]}"
|
||||
)
|
||||
logger.debug(f"Cannot covert object of type {type(j)} to type {self.annotations[i]}")
|
||||
setattr(self, i, j)
|
||||
|
||||
except Exception as exe:
|
||||
logger.debug(
|
||||
f"Did not set attribute {i} on class {self.__class__.__name__} due to {exe}"
|
||||
)
|
||||
logger.debug(f"Did not set attribute {i} on class {self.__class__.__name__} due to {exe}")
|
||||
continue
|
||||
|
||||
@property
|
||||
@@ -113,11 +89,7 @@ class Base:
|
||||
"""
|
||||
exclude, include = exclude or set(), include or set()
|
||||
filter_ = include or set(self.dict.keys()).difference(exclude)
|
||||
return {
|
||||
key: value
|
||||
for key, value in self.dict.items()
|
||||
if key in filter_ and value is not None
|
||||
}
|
||||
return {key: value for key, value in self.dict.items() if key in filter_ and value is not None}
|
||||
|
||||
@property
|
||||
@cache
|
||||
@@ -131,9 +103,7 @@ class Base:
|
||||
cls_dict = {}
|
||||
for cls in clss:
|
||||
cls_dict |= cls.__dict__
|
||||
return {
|
||||
key: value for key, value in cls_dict.items() if key in self.annotations
|
||||
}
|
||||
return {key: value for key, value in cls_dict.items() if key in self.annotations}
|
||||
|
||||
@property
|
||||
def dict(self) -> dict:
|
||||
@@ -144,11 +114,7 @@ class Base:
|
||||
"""
|
||||
try:
|
||||
_filter = self.exclude.difference(self.include)
|
||||
return {
|
||||
key: value
|
||||
for key, value in (self.class_vars | self.__dict__).items()
|
||||
if key not in _filter and value is not None
|
||||
}
|
||||
return {key: value for key, value in (self.class_vars | self.__dict__).items() if key not in _filter and value is not None}
|
||||
except Exception as err:
|
||||
logger.warning(err)
|
||||
|
||||
|
||||
@@ -114,9 +114,7 @@ class Config:
|
||||
**kwargs: Object attributes and values as keyword arguments
|
||||
"""
|
||||
if kwargs.pop("root", None) is not None:
|
||||
logger.warning(
|
||||
"Tried setting root from set_attributes. Use load_config to change project root"
|
||||
)
|
||||
logger.warning("Tried setting root from set_attributes. Use load_config to change project root")
|
||||
[setattr(self, key, value) for key, value in kwargs.items()]
|
||||
|
||||
@staticmethod
|
||||
@@ -145,14 +143,7 @@ class Config:
|
||||
logger.debug(f"Error finding config file: {err}")
|
||||
return
|
||||
|
||||
def load_config(
|
||||
self,
|
||||
*,
|
||||
file: str | Path = None,
|
||||
filename: str = None,
|
||||
root: str | Path = None,
|
||||
**kwargs,
|
||||
) -> Self:
|
||||
def load_config(self, *, file: str | Path = None, filename: str = None, root: str | Path = None, **kwargs) -> Self:
|
||||
"""Load configuration settings from a file.
|
||||
|
||||
Keyword Args:
|
||||
@@ -190,17 +181,9 @@ class Config:
|
||||
self.set_attributes(**data)
|
||||
|
||||
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.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
@@ -3,17 +3,7 @@ from logging import getLogger
|
||||
from typing import Literal, TypeVar
|
||||
|
||||
from numpy import ndarray
|
||||
from MetaTrader5 import (
|
||||
Tick,
|
||||
SymbolInfo,
|
||||
AccountInfo,
|
||||
TerminalInfo,
|
||||
TradeOrder,
|
||||
TradePosition,
|
||||
TradeDeal,
|
||||
OrderCheckResult,
|
||||
OrderSendResult,
|
||||
)
|
||||
from MetaTrader5 import Tick, SymbolInfo, AccountInfo, TerminalInfo, TradeOrder, TradePosition, TradeDeal, OrderCheckResult, OrderSendResult
|
||||
|
||||
from .meta_trader import MetaTrader
|
||||
from .constants import TimeFrame, CopyTicks, OrderType
|
||||
@@ -49,73 +39,29 @@ class MetaBackTester(MetaTrader):
|
||||
return await super().last_error()
|
||||
|
||||
async def initialize(
|
||||
self,
|
||||
*,
|
||||
path: str = "",
|
||||
login: int = 0,
|
||||
password: str = "",
|
||||
server: str = "",
|
||||
timeout: int | None = None,
|
||||
portable=False,
|
||||
self, *, path: str = "", login: int = 0, password: str = "", server: str = "", timeout: int | None = None, portable=False
|
||||
) -> bool:
|
||||
if self.config.use_terminal_for_backtesting:
|
||||
return await super().initialize(
|
||||
path=path,
|
||||
login=login,
|
||||
password=password,
|
||||
server=server,
|
||||
timeout=timeout,
|
||||
)
|
||||
return await super().initialize(path=path, login=login, password=password, server=server, timeout=timeout)
|
||||
|
||||
return True
|
||||
|
||||
def initialize_sync(
|
||||
self,
|
||||
*,
|
||||
path: str = "",
|
||||
login: int = 0,
|
||||
password: str = "",
|
||||
server: str = "",
|
||||
timeout: int | None = None,
|
||||
portable=False,
|
||||
self, *, path: str = "", login: int = 0, password: str = "", server: str = "", timeout: int | None = None, portable=False
|
||||
) -> bool:
|
||||
if self.config.use_terminal_for_backtesting:
|
||||
return super().initialize_sync(
|
||||
path=path,
|
||||
login=login,
|
||||
password=password,
|
||||
server=server,
|
||||
timeout=timeout,
|
||||
)
|
||||
return super().initialize_sync(path=path, login=login, password=password, server=server, timeout=timeout)
|
||||
|
||||
return True
|
||||
|
||||
def login_sync(
|
||||
self,
|
||||
*,
|
||||
login: int = 0,
|
||||
password: str = "",
|
||||
server: str = "",
|
||||
timeout: int = 60000,
|
||||
) -> bool:
|
||||
def login_sync(self, *, login: int = 0, password: str = "", server: str = "", timeout: int = 60000) -> bool:
|
||||
if self.config.use_terminal_for_backtesting:
|
||||
return super().login_sync(
|
||||
login=login, password=password, server=server, timeout=timeout
|
||||
)
|
||||
return super().login_sync(login=login, password=password, server=server, timeout=timeout)
|
||||
return True
|
||||
|
||||
async def login(
|
||||
self,
|
||||
*,
|
||||
login: int = 0,
|
||||
password: str = "",
|
||||
server: str = "",
|
||||
timeout: int = 60000,
|
||||
) -> bool:
|
||||
async def login(self, *, login: int = 0, password: str = "", server: str = "", timeout: int = 60000) -> bool:
|
||||
if self.config.use_terminal_for_backtesting:
|
||||
return await super().login(
|
||||
login=login, password=password, server=server, timeout=timeout
|
||||
)
|
||||
return await super().login(login=login, password=password, server=server, timeout=timeout)
|
||||
return True
|
||||
|
||||
async def shutdown(self) -> None:
|
||||
@@ -153,56 +99,28 @@ class MetaBackTester(MetaTrader):
|
||||
return tick
|
||||
|
||||
@error_handler(msg="test data not available", exe=AttributeError)
|
||||
async def copy_rates_from(
|
||||
self, symbol: str, timeframe: TimeFrame, date_from: datetime | float, count: int
|
||||
) -> ndarray | None:
|
||||
rates = await self.backtest_engine.get_rates_from(
|
||||
symbol=symbol, timeframe=timeframe, date_from=date_from, count=count
|
||||
)
|
||||
async def copy_rates_from(self, symbol: str, timeframe: TimeFrame, date_from: datetime | float, count: int) -> ndarray | None:
|
||||
rates = await self.backtest_engine.get_rates_from(symbol=symbol, timeframe=timeframe, date_from=date_from, count=count)
|
||||
return rates
|
||||
|
||||
@error_handler(msg="test data not available", exe=AttributeError)
|
||||
async def copy_rates_from_pos(
|
||||
self, symbol: str, timeframe: TimeFrame, start_pos: int, count: int
|
||||
) -> ndarray | None:
|
||||
rates = await self.backtest_engine.get_rates_from_pos(
|
||||
symbol=symbol, timeframe=timeframe, start_pos=start_pos, count=count
|
||||
)
|
||||
async def copy_rates_from_pos(self, symbol: str, timeframe: TimeFrame, start_pos: int, count: int) -> ndarray | None:
|
||||
rates = await self.backtest_engine.get_rates_from_pos(symbol=symbol, timeframe=timeframe, start_pos=start_pos, count=count)
|
||||
return rates
|
||||
|
||||
@error_handler(msg="test data not available", exe=AttributeError)
|
||||
async def copy_rates_range(
|
||||
self,
|
||||
symbol: str,
|
||||
timeframe: TimeFrame,
|
||||
date_from: datetime | float,
|
||||
date_to: datetime | float,
|
||||
) -> ndarray | None:
|
||||
rates = await self.backtest_engine.get_rates_range(
|
||||
symbol=symbol, timeframe=timeframe, date_from=date_from, date_to=date_to
|
||||
)
|
||||
async def copy_rates_range(self, symbol: str, timeframe: TimeFrame, date_from: datetime | float, date_to: datetime | float) -> ndarray | None:
|
||||
rates = await self.backtest_engine.get_rates_range(symbol=symbol, timeframe=timeframe, date_from=date_from, date_to=date_to)
|
||||
return rates
|
||||
|
||||
@error_handler(msg="test data not available", exe=AttributeError)
|
||||
async def copy_ticks_from(
|
||||
self, symbol: str, date_from: datetime | float, count: int, flags: CopyTicks
|
||||
) -> ndarray | None:
|
||||
ticks = await self.backtest_engine.get_ticks_from(
|
||||
symbol=symbol, date_from=date_from, count=count, flags=flags
|
||||
)
|
||||
async def copy_ticks_from(self, symbol: str, date_from: datetime | float, count: int, flags: CopyTicks) -> ndarray | None:
|
||||
ticks = await self.backtest_engine.get_ticks_from(symbol=symbol, date_from=date_from, count=count, flags=flags)
|
||||
return ticks
|
||||
|
||||
@error_handler(msg="test data not available", exe=AttributeError)
|
||||
async def copy_ticks_range(
|
||||
self,
|
||||
symbol: str,
|
||||
date_from: datetime | float,
|
||||
date_to: datetime | float,
|
||||
flags: CopyTicks,
|
||||
) -> ndarray | None:
|
||||
ticks = await self.backtest_engine.get_ticks_range(
|
||||
symbol=symbol, date_from=date_from, date_to=date_to, flags=flags
|
||||
)
|
||||
async def copy_ticks_range(self, symbol: str, date_from: datetime | float, date_to: datetime | float, flags: CopyTicks) -> ndarray | None:
|
||||
ticks = await self.backtest_engine.get_ticks_range(symbol=symbol, date_from=date_from, date_to=date_to, flags=flags)
|
||||
return ticks
|
||||
|
||||
@error_handler(msg="test data not available", exe=AttributeError)
|
||||
@@ -210,40 +128,19 @@ class MetaBackTester(MetaTrader):
|
||||
return self.backtest_engine.get_orders_total()
|
||||
|
||||
@error_handler(msg="test data not available", exe=AttributeError)
|
||||
async def orders_get(
|
||||
self, group: str = "", ticket: int = 0, symbol: str = ""
|
||||
) -> tuple[TradeOrder, ...] | None:
|
||||
kwargs = {
|
||||
key: value
|
||||
for key, value in (("group", group), ("ticket", ticket), ("symbol", symbol))
|
||||
if value
|
||||
}
|
||||
async def orders_get(self, group: str = "", ticket: int = 0, symbol: str = "") -> tuple[TradeOrder, ...] | None:
|
||||
kwargs = {key: value for key, value in (("group", group), ("ticket", ticket), ("symbol", symbol)) if value}
|
||||
return self.backtest_engine.get_orders(**kwargs)
|
||||
|
||||
@error_handler(msg="test data not available", exe=AttributeError)
|
||||
async def order_calc_margin(
|
||||
self, action: OrderType, symbol: str, volume: float, price: float
|
||||
) -> float | None:
|
||||
res = await self.backtest_engine.order_calc_margin(
|
||||
action=action, symbol=symbol, volume=volume, price=price
|
||||
)
|
||||
async def order_calc_margin(self, action: OrderType, symbol: str, volume: float, price: float) -> float | None:
|
||||
res = await self.backtest_engine.order_calc_margin(action=action, symbol=symbol, volume=volume, price=price)
|
||||
return res
|
||||
|
||||
@error_handler(msg="test data not available", exe=AttributeError)
|
||||
async def order_calc_profit(
|
||||
self,
|
||||
action: Literal[0, 1],
|
||||
symbol: str,
|
||||
volume: float,
|
||||
price_open: float,
|
||||
price_close: float,
|
||||
) -> float | None:
|
||||
async def order_calc_profit(self, action: Literal[0, 1], symbol: str, volume: float, price_open: float, price_close: float) -> float | None:
|
||||
profit = await self.backtest_engine.order_calc_profit(
|
||||
action=action,
|
||||
symbol=symbol,
|
||||
volume=volume,
|
||||
price_open=price_open,
|
||||
price_close=price_close,
|
||||
action=action, symbol=symbol, volume=volume, price_open=price_open, price_close=price_close
|
||||
)
|
||||
return profit
|
||||
|
||||
@@ -261,66 +158,30 @@ class MetaBackTester(MetaTrader):
|
||||
return self.backtest_engine.get_positions_total()
|
||||
|
||||
@error_handler(msg="test data not available", exe=AttributeError)
|
||||
async def positions_get(
|
||||
self, group: str = "", ticket: int = None, symbol: str = ""
|
||||
) -> tuple[TradePosition, ...] | None:
|
||||
kwargs = {
|
||||
key: value
|
||||
for key, value in (("group", group), ("ticket", ticket), ("symbol", symbol))
|
||||
if value
|
||||
}
|
||||
async def positions_get(self, group: str = "", ticket: int = None, symbol: str = "") -> tuple[TradePosition, ...] | None:
|
||||
kwargs = {key: value for key, value in (("group", group), ("ticket", ticket), ("symbol", symbol)) if value}
|
||||
return self.backtest_engine.get_positions(**kwargs)
|
||||
|
||||
@error_handler(msg="test data not available", exe=AttributeError)
|
||||
async def history_orders_total(
|
||||
self, date_from: datetime | float, date_to: datetime | float
|
||||
) -> int | None:
|
||||
return self.backtest_engine.get_history_orders_total(
|
||||
date_from=date_from, date_to=date_to
|
||||
)
|
||||
async def history_orders_total(self, date_from: datetime | float, date_to: datetime | float) -> int | None:
|
||||
return self.backtest_engine.get_history_orders_total(date_from=date_from, date_to=date_to)
|
||||
|
||||
@error_handler(msg="test data not available", exe=AttributeError)
|
||||
async def history_orders_get(
|
||||
self,
|
||||
date_from: datetime | float = None,
|
||||
date_to: datetime | float = None,
|
||||
group: str = "",
|
||||
ticket: int = None,
|
||||
position: int = None,
|
||||
self, date_from: datetime | float = None, date_to: datetime | float = None, group: str = "", ticket: int = None, position: int = None
|
||||
) -> tuple[TradeOrder, ...] | None:
|
||||
args = (
|
||||
("date_from", date_from),
|
||||
("date_to", date_to),
|
||||
("group", group),
|
||||
("ticket", ticket),
|
||||
("position", position),
|
||||
)
|
||||
args = (("date_from", date_from), ("date_to", date_to), ("group", group), ("ticket", ticket), ("position", position))
|
||||
kwargs = {key: value for key, value in args if value}
|
||||
return self.backtest_engine.get_history_orders(**kwargs)
|
||||
|
||||
@error_handler(msg="test data not available", exe=AttributeError)
|
||||
async def history_deals_total(
|
||||
self, date_from: datetime | float, date_to: datetime | float
|
||||
) -> int | None:
|
||||
return self.backtest_engine.get_history_deals_total(
|
||||
date_from=date_from, date_to=date_to
|
||||
)
|
||||
async def history_deals_total(self, date_from: datetime | float, date_to: datetime | float) -> int | None:
|
||||
return self.backtest_engine.get_history_deals_total(date_from=date_from, date_to=date_to)
|
||||
|
||||
@error_handler(msg="test data not available", exe=AttributeError)
|
||||
async def history_deals_get(
|
||||
self,
|
||||
date_from: datetime | float = None,
|
||||
date_to: datetime | float = None,
|
||||
group: str = "",
|
||||
ticket: int = None,
|
||||
position: int = None,
|
||||
self, date_from: datetime | float = None, date_to: datetime | float = None, group: str = "", ticket: int = None, position: int = None
|
||||
) -> tuple[TradeDeal, ...] | None:
|
||||
args = (
|
||||
("date_from", date_from),
|
||||
("date_to", date_to),
|
||||
("group", group),
|
||||
("ticket", ticket),
|
||||
("position", position),
|
||||
)
|
||||
args = (("date_from", date_from), ("date_to", date_to), ("group", group), ("ticket", ticket), ("position", position))
|
||||
kwargs = {key: value for key, value in args if value}
|
||||
return self.backtest_engine.get_history_deals(**kwargs)
|
||||
|
||||
+49
-255
@@ -5,18 +5,8 @@ from typing import Literal
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
from MetaTrader5 import (
|
||||
BookInfo,
|
||||
SymbolInfo,
|
||||
AccountInfo,
|
||||
Tick,
|
||||
TerminalInfo,
|
||||
TradeOrder,
|
||||
TradeDeal,
|
||||
TradePosition,
|
||||
OrderSendResult,
|
||||
OrderCheckResult,
|
||||
)
|
||||
from MetaTrader5 import (BookInfo, SymbolInfo, AccountInfo, Tick, TerminalInfo, TradeOrder, TradeDeal, TradePosition,
|
||||
OrderSendResult, OrderCheckResult)
|
||||
import MetaTrader5 as mt5
|
||||
|
||||
from .constants import OrderType, CopyTicks
|
||||
@@ -75,14 +65,7 @@ class MetaTrader(MetaCore):
|
||||
logger.warning(f"{error_msg}:{self.error.description}")
|
||||
return res
|
||||
|
||||
async def login(
|
||||
self,
|
||||
*,
|
||||
login: int = 0,
|
||||
password: str = "",
|
||||
server: str = "",
|
||||
timeout: int = 60000,
|
||||
) -> bool:
|
||||
async def login(self, *, login: int = 0, password: str = "", server: str = "", timeout: int = 60000) -> bool:
|
||||
"""
|
||||
Connects to the MetaTrader terminal using the specified login, password and server.
|
||||
|
||||
@@ -99,18 +82,9 @@ class MetaTrader(MetaCore):
|
||||
login = login or acc_details.get("login", 0)
|
||||
password = password or acc_details.get("password", "")
|
||||
server = server or acc_details.get("server", "")
|
||||
return await asyncio.to_thread(
|
||||
self._login, login, password=password, server=server, timeout=timeout
|
||||
)
|
||||
return await asyncio.to_thread(self._login, login, password=password, server=server, timeout=timeout)
|
||||
|
||||
def login_sync(
|
||||
self,
|
||||
*,
|
||||
login: int = 0,
|
||||
password: str = "",
|
||||
server: str = "",
|
||||
timeout: int = 60000,
|
||||
) -> bool:
|
||||
def login_sync(self, *, login: int = 0, password: str = "", server: str = "", timeout: int = 60000) -> bool:
|
||||
"""
|
||||
Connects to the MetaTrader terminal using the specified login, password and server.
|
||||
|
||||
@@ -131,13 +105,7 @@ class MetaTrader(MetaCore):
|
||||
return res
|
||||
|
||||
async def initialize(
|
||||
self,
|
||||
path: str = None,
|
||||
login: int = 0,
|
||||
password: str = "",
|
||||
server: str = "",
|
||||
timeout: int | None = None,
|
||||
portable=False,
|
||||
self, path: str = None, login: int = 0, password: str = "", server: str = "", timeout: int | None = None, portable=False
|
||||
) -> bool:
|
||||
"""
|
||||
Initializes the connection to the MetaTrader terminal. All parameters are optional.
|
||||
@@ -179,13 +147,7 @@ class MetaTrader(MetaCore):
|
||||
return res
|
||||
|
||||
def initialize_sync(
|
||||
self,
|
||||
path: str = None,
|
||||
login: int = 0,
|
||||
password: str = "",
|
||||
server: str = "",
|
||||
timeout: int | None = None,
|
||||
portable=False,
|
||||
self, path: str = None, login: int = 0, password: str = "", server: str = "", timeout: int | None = None, portable=False
|
||||
) -> bool:
|
||||
"""
|
||||
Initializes the connection to the MetaTrader terminal. All parameters are optional.
|
||||
@@ -244,107 +206,62 @@ class MetaTrader(MetaCore):
|
||||
|
||||
async def account_info(self) -> AccountInfo | None:
|
||||
""""""
|
||||
api = {
|
||||
"func": self._account_info,
|
||||
"error_msg": "Error in obtaining account information",
|
||||
}
|
||||
api = {"func": self._account_info, "error_msg": "Error in obtaining account information"}
|
||||
res = await self._handler(api)
|
||||
return res
|
||||
|
||||
async def terminal_info(self) -> TerminalInfo | None:
|
||||
api = {
|
||||
"func": self._terminal_info,
|
||||
"error_msg": "Error in obtaining terminal information",
|
||||
}
|
||||
api = {"func": self._terminal_info, "error_msg": "Error in obtaining terminal information"}
|
||||
res = await self._handler(api)
|
||||
return res
|
||||
|
||||
async def symbols_total(self) -> int:
|
||||
api = {
|
||||
"func": self._symbols_total,
|
||||
"error_msg": "Error in obtaining total symbols.",
|
||||
}
|
||||
api = {"func": self._symbols_total, "error_msg": "Error in obtaining total symbols."}
|
||||
res = await self._handler(api)
|
||||
return res
|
||||
|
||||
async def symbols_get(self, group: str = "") -> tuple[SymbolInfo] | None:
|
||||
kwargs = {"group": group} if group else {}
|
||||
api = {
|
||||
"func": self._symbols_get,
|
||||
"kwargs": kwargs,
|
||||
"error_msg": "Error in obtaining symbols.",
|
||||
}
|
||||
api = {"func": self._symbols_get, "kwargs": kwargs, "error_msg": "Error in obtaining symbols."}
|
||||
res = await self._handler(api)
|
||||
return res
|
||||
|
||||
async def symbol_info(self, symbol: str) -> SymbolInfo | None:
|
||||
api = {
|
||||
"func": self._symbol_info,
|
||||
"args": (symbol,),
|
||||
"error_msg": f"Error in obtaining information for {symbol}",
|
||||
}
|
||||
api = {"func": self._symbol_info, "args": (symbol,), "error_msg": f"Error in obtaining information for {symbol}"}
|
||||
res = await self._handler(api)
|
||||
return res
|
||||
|
||||
async def symbol_info_tick(self, symbol: str) -> Tick | None:
|
||||
api = {
|
||||
"func": self._symbol_info_tick,
|
||||
"args": (symbol,),
|
||||
"error_msg": f"Error in obtaining tick for {symbol}",
|
||||
}
|
||||
api = {"func": self._symbol_info_tick, "args": (symbol,), "error_msg": f"Error in obtaining tick for {symbol}"}
|
||||
res = await self._handler(api)
|
||||
return res
|
||||
|
||||
async def symbol_select(self, symbol: str, enable: bool) -> bool:
|
||||
api = {
|
||||
"func": self._symbol_select,
|
||||
"args": (symbol, enable),
|
||||
"error_msg": f"Error in selecting {symbol}",
|
||||
}
|
||||
api = {"func": self._symbol_select, "args": (symbol, enable), "error_msg": f"Error in selecting {symbol}"}
|
||||
res = await self._handler(api)
|
||||
return res
|
||||
|
||||
async def market_book_add(self, symbol: str) -> bool:
|
||||
api = {
|
||||
"func": self._market_book_add,
|
||||
"args": (symbol,),
|
||||
"error_msg": f"Error in adding {symbol} to market book",
|
||||
}
|
||||
api = {"func": self._market_book_add, "args": (symbol,), "error_msg": f"Error in adding {symbol} to market book"}
|
||||
res = await self._handler(api)
|
||||
return res
|
||||
|
||||
async def market_book_get(self, symbol: str) -> tuple[BookInfo] | None:
|
||||
api = {
|
||||
"func": self._market_book_get,
|
||||
"args": (symbol,),
|
||||
"error_msg": f"Error in obtaining market depth for {symbol}",
|
||||
}
|
||||
api = {"func": self._market_book_get, "args": (symbol,), "error_msg": f"Error in obtaining market depth for {symbol}"}
|
||||
res = await self._handler(api)
|
||||
return res
|
||||
|
||||
async def market_book_release(self, symbol: str) -> bool:
|
||||
api = {
|
||||
"func": self._market_book_release,
|
||||
"args": (symbol,),
|
||||
"error_msg": f"Error in releasing market depth for {symbol}",
|
||||
}
|
||||
api = {"func": self._market_book_release, "args": (symbol,), "error_msg": f"Error in releasing market depth for {symbol}"}
|
||||
res = await self._handler(api)
|
||||
return res
|
||||
|
||||
async def copy_rates_from(
|
||||
self, symbol: str, timeframe: int, date_from: datetime | float, count: int
|
||||
) -> np.ndarray | None:
|
||||
api = {
|
||||
"func": self._copy_rates_from,
|
||||
"args": (symbol, timeframe, date_from, count),
|
||||
"error_msg": f"Error in obtaining rates for {symbol}",
|
||||
}
|
||||
async def copy_rates_from(self, symbol: str, timeframe: int, date_from: datetime | float, count: int) -> np.ndarray | None:
|
||||
api = {"func": self._copy_rates_from, "args": (symbol, timeframe, date_from, count), "error_msg": f"Error in obtaining rates for {symbol}"}
|
||||
res = await self._handler(api)
|
||||
return res
|
||||
|
||||
async def copy_rates_from_pos(
|
||||
self, symbol: str, timeframe: int, start_pos: int, count: int
|
||||
) -> np.ndarray | None:
|
||||
async def copy_rates_from_pos(self, symbol: str, timeframe: int, start_pos: int, count: int) -> np.ndarray | None:
|
||||
api = {
|
||||
"func": self._copy_rates_from_pos,
|
||||
"args": (symbol, timeframe, start_pos, count),
|
||||
@@ -353,93 +270,39 @@ class MetaTrader(MetaCore):
|
||||
res = await self._handler(api)
|
||||
return res
|
||||
|
||||
async def copy_rates_range(
|
||||
self,
|
||||
symbol: str,
|
||||
timeframe: int,
|
||||
date_from: datetime | float,
|
||||
date_to: datetime | float,
|
||||
) -> np.ndarray | None:
|
||||
api = {
|
||||
"func": self._copy_rates_range,
|
||||
"args": (symbol, timeframe, date_from, date_to),
|
||||
"error_msg": f"Error in obtaining rates for {symbol}",
|
||||
}
|
||||
async def copy_rates_range(self, symbol: str, timeframe: int, date_from: datetime | float, date_to: datetime | float) -> np.ndarray | None:
|
||||
api = {"func": self._copy_rates_range, "args": (symbol, timeframe, date_from, date_to), "error_msg": f"Error in obtaining rates for {symbol}"}
|
||||
res = await self._handler(api)
|
||||
return res
|
||||
|
||||
async def copy_ticks_from(
|
||||
self, symbol: str, date_from: datetime | float, count: int, flags: CopyTicks
|
||||
) -> np.ndarray | None:
|
||||
api = {
|
||||
"func": self._copy_ticks_from,
|
||||
"args": (symbol, date_from, count, flags),
|
||||
"error_msg": f"Error in obtaining ticks for {symbol}",
|
||||
}
|
||||
async def copy_ticks_from(self, symbol: str, date_from: datetime | float, count: int, flags: CopyTicks) -> np.ndarray | None:
|
||||
api = {"func": self._copy_ticks_from, "args": (symbol, date_from, count, flags), "error_msg": f"Error in obtaining ticks for {symbol}"}
|
||||
res = await self._handler(api)
|
||||
return res
|
||||
|
||||
async def copy_ticks_range(
|
||||
self,
|
||||
symbol: str,
|
||||
date_from: datetime | float,
|
||||
date_to: datetime | float,
|
||||
flags: CopyTicks,
|
||||
) -> np.ndarray | None:
|
||||
api = {
|
||||
"func": self._copy_ticks_range,
|
||||
"args": (symbol, date_from, date_to, flags),
|
||||
"error_msg": f"Error in obtaining ticks for {symbol}",
|
||||
}
|
||||
async def copy_ticks_range(self, symbol: str, date_from: datetime | float, date_to: datetime | float, flags: CopyTicks) -> np.ndarray | None:
|
||||
api = {"func": self._copy_ticks_range, "args": (symbol, date_from, date_to, flags), "error_msg": f"Error in obtaining ticks for {symbol}"}
|
||||
res = await self._handler(api)
|
||||
return res
|
||||
|
||||
async def orders_total(self) -> int:
|
||||
api = {
|
||||
"func": self._orders_total,
|
||||
"error_msg": "Error in obtaining total orders.",
|
||||
}
|
||||
api = {"func": self._orders_total, "error_msg": "Error in obtaining total orders."}
|
||||
res = await self._handler(api)
|
||||
return res
|
||||
|
||||
async def orders_get(
|
||||
self, group: str = "", ticket: int = 0, symbol: str = ""
|
||||
) -> tuple[TradeOrder] | None:
|
||||
kwargs = {
|
||||
key: value
|
||||
for key, value in (("group", group), ("ticket", ticket), ("symbol", symbol))
|
||||
if value
|
||||
}
|
||||
api = {
|
||||
"func": self._orders_get,
|
||||
"kwargs": kwargs,
|
||||
"error_msg": "Error in obtaining orders.",
|
||||
}
|
||||
async def orders_get(self, group: str = "", ticket: int = 0, symbol: str = "") -> tuple[TradeOrder] | None:
|
||||
kwargs = {key: value for key, value in (("group", group), ("ticket", ticket), ("symbol", symbol)) if value}
|
||||
api = {"func": self._orders_get, "kwargs": kwargs, "error_msg": "Error in obtaining orders."}
|
||||
res = await self._handler(api)
|
||||
return res
|
||||
|
||||
async def order_calc_margin(
|
||||
self,
|
||||
action: Literal[OrderType.BUY, OrderType.SELL],
|
||||
symbol: str,
|
||||
volume: float,
|
||||
price: float,
|
||||
) -> float | None:
|
||||
api = {
|
||||
"func": self._order_calc_margin,
|
||||
"args": (action, symbol, volume, price),
|
||||
"error_msg": "Error in calculating margin.",
|
||||
}
|
||||
async def order_calc_margin(self, action: Literal[OrderType.BUY, OrderType.SELL], symbol: str, volume: float, price: float) -> float | None:
|
||||
api = {"func": self._order_calc_margin, "args": (action, symbol, volume, price), "error_msg": "Error in calculating margin."}
|
||||
res = await self._handler(api)
|
||||
return res
|
||||
|
||||
async def order_calc_profit(
|
||||
self,
|
||||
action: Literal[OrderType.BUY, OrderType.SELL],
|
||||
symbol: str,
|
||||
volume: float,
|
||||
price_open: float,
|
||||
price_close: float,
|
||||
self, action: Literal[OrderType.BUY, OrderType.SELL], symbol: str, volume: float, price_open: float, price_close: float
|
||||
) -> float | None:
|
||||
api = {
|
||||
"func": self._order_calc_profit,
|
||||
@@ -450,119 +313,50 @@ class MetaTrader(MetaCore):
|
||||
return res
|
||||
|
||||
async def order_check(self, request: dict) -> OrderCheckResult:
|
||||
api = {
|
||||
"func": self._order_check,
|
||||
"args": (request,),
|
||||
"error_msg": "Error in checking order.",
|
||||
}
|
||||
api = {"func": self._order_check, "args": (request,), "error_msg": "Error in checking order."}
|
||||
res = await self._handler(api)
|
||||
return res
|
||||
|
||||
async def order_send(self, request: dict) -> OrderSendResult:
|
||||
api = {
|
||||
"func": self._order_send,
|
||||
"args": (request,),
|
||||
"error_msg": "Error in sending order.",
|
||||
}
|
||||
api = {"func": self._order_send, "args": (request,), "error_msg": "Error in sending order."}
|
||||
res = await self._handler(api)
|
||||
return res
|
||||
|
||||
async def positions_total(self) -> int:
|
||||
api = {
|
||||
"func": self._positions_total,
|
||||
"error_msg": "Error in obtaining total positions.",
|
||||
}
|
||||
api = {"func": self._positions_total, "error_msg": "Error in obtaining total positions."}
|
||||
res = await self._handler(api)
|
||||
return res
|
||||
|
||||
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
|
||||
}
|
||||
api = {
|
||||
"func": self._positions_get,
|
||||
"kwargs": kwargs,
|
||||
"error_msg": "Error in obtaining open positions.",
|
||||
}
|
||||
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}
|
||||
api = {"func": self._positions_get, "kwargs": kwargs, "error_msg": "Error in obtaining open positions."}
|
||||
res = await self._handler(api)
|
||||
return res
|
||||
|
||||
async def history_orders_total(
|
||||
self, date_from: datetime | float, date_to: datetime | float
|
||||
) -> int:
|
||||
api = {
|
||||
"func": self._history_orders_total,
|
||||
"args": (date_from, date_to),
|
||||
"error_msg": "Error in obtaining total history orders.",
|
||||
}
|
||||
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."}
|
||||
res = await self._handler(api)
|
||||
return res
|
||||
|
||||
async def history_orders_get(
|
||||
self,
|
||||
date_from: datetime | float = None,
|
||||
date_to: datetime | float = None,
|
||||
group: str = "",
|
||||
ticket: int = None,
|
||||
position: int = None,
|
||||
self, date_from: datetime | float = None, date_to: datetime | float = None, group: str = "", ticket: int = None, position: int = None
|
||||
) -> tuple[TradeOrder] | None:
|
||||
kwargs = {
|
||||
key: value
|
||||
for key, value in (
|
||||
("group", group),
|
||||
("ticket", ticket),
|
||||
("position", position),
|
||||
)
|
||||
if value
|
||||
}
|
||||
kwargs = {key: value for key, value in (("group", group), ("ticket", ticket), ("position", position)) if value}
|
||||
args = tuple(arg for arg in (date_from, date_to) if arg)
|
||||
api = {
|
||||
"func": self._history_orders_get,
|
||||
"args": args,
|
||||
"kwargs": kwargs,
|
||||
"error_msg": "Error in obtaining history orders",
|
||||
}
|
||||
api = {"func": self._history_orders_get, "args": args, "kwargs": kwargs, "error_msg": "Error in obtaining history orders"}
|
||||
res = await self._handler(api)
|
||||
return res
|
||||
|
||||
async def history_deals_total(
|
||||
self, date_from: datetime | float, date_to: datetime | float
|
||||
) -> int:
|
||||
api = {
|
||||
"func": self._history_deals_total,
|
||||
"args": (date_from, date_to),
|
||||
"error_msg": "Error in obtaining total history deals",
|
||||
}
|
||||
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"}
|
||||
res = await self._handler(api)
|
||||
return res
|
||||
|
||||
async def history_deals_get(
|
||||
self,
|
||||
date_from: datetime | float = None,
|
||||
date_to: datetime | float = None,
|
||||
group: str = "",
|
||||
ticket: int = None,
|
||||
position: int = None,
|
||||
self, date_from: datetime | float = None, date_to: datetime | float = None, group: str = "", ticket: int = None, position: int = None
|
||||
) -> tuple[TradeDeal] | None:
|
||||
kwargs = {
|
||||
key: value
|
||||
for key, value in (
|
||||
("group", group),
|
||||
("ticket", ticket),
|
||||
("position", position),
|
||||
)
|
||||
if value
|
||||
}
|
||||
kwargs = {key: value for key, value in (("group", group), ("ticket", ticket), ("position", position)) if value}
|
||||
args = tuple(arg for arg in (date_from, date_to) if arg)
|
||||
api = {
|
||||
"func": self._history_deals_get,
|
||||
"args": args,
|
||||
"kwargs": kwargs,
|
||||
"error_msg": "Error in obtaining history deals",
|
||||
}
|
||||
api = {"func": self._history_deals_get, "args": args, "kwargs": kwargs, "error_msg": "Error in obtaining history deals"}
|
||||
res = await self._handler(api)
|
||||
return res
|
||||
|
||||
@@ -354,10 +354,7 @@ class SymbolInfo(Base):
|
||||
name: str = ""
|
||||
|
||||
def __repr__(self):
|
||||
return "%(class)s(name=%(name)s)" % {
|
||||
"class": self.__class__.__name__,
|
||||
"name": self.name,
|
||||
}
|
||||
return "%(class)s(name=%(name)s)" % {"class": self.__class__.__name__, "name": self.name}
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
@@ -31,9 +31,7 @@ class QueueItem:
|
||||
self.task_item(*self.args, **self.kwargs)
|
||||
|
||||
except Exception as err:
|
||||
logger.error(
|
||||
f"Error {err} occurred in {self.task_item.__name__} with args {self.args} and kwargs {self.kwargs}"
|
||||
)
|
||||
logger.error(f"Error {err} occurred in {self.task_item.__name__} with args {self.args} and kwargs {self.kwargs}")
|
||||
|
||||
|
||||
class TaskQueue:
|
||||
@@ -84,9 +82,7 @@ class TaskQueue:
|
||||
self.queue.task_done()
|
||||
self.priority_tasks.discard(item)
|
||||
|
||||
if self.stop and (
|
||||
self.on_exit == "cancel" or len(self.priority_tasks) == 0
|
||||
):
|
||||
if self.stop and (self.on_exit == "cancel" or len(self.priority_tasks) == 0):
|
||||
self.cancel()
|
||||
break
|
||||
|
||||
@@ -107,36 +103,26 @@ class TaskQueue:
|
||||
async def run(self, timeout: int = 0):
|
||||
start = time.perf_counter()
|
||||
try:
|
||||
self.tasks.extend(
|
||||
asyncio.create_task(self.worker()) for _ in range(self.workers)
|
||||
)
|
||||
self.tasks.extend(asyncio.create_task(self.worker()) for _ in range(self.workers))
|
||||
timeout = timeout or self.timeout
|
||||
queue_task = asyncio.create_task(self.queue.join())
|
||||
|
||||
if timeout:
|
||||
main_task = asyncio.create_task(
|
||||
asyncio.wait_for(queue_task, timeout=timeout)
|
||||
)
|
||||
main_task = asyncio.create_task(asyncio.wait_for(queue_task, timeout=timeout))
|
||||
else:
|
||||
main_task = queue_task
|
||||
self.tasks.append(main_task)
|
||||
await main_task
|
||||
|
||||
except TimeoutError:
|
||||
logger.warning(
|
||||
"Timed out after %d seconds, %d tasks remaining",
|
||||
time.perf_counter() - start,
|
||||
self.queue.qsize(),
|
||||
)
|
||||
logger.warning("Timed out after %d seconds, %d tasks remaining", time.perf_counter() - start, self.queue.qsize())
|
||||
self.stop = True
|
||||
|
||||
except asyncio.CancelledError as _:
|
||||
logger.warning("Main task cancelled")
|
||||
|
||||
except Exception as err:
|
||||
logger.warning(
|
||||
"%s: An error occurred in %s.run", err, self.__class__.__name__
|
||||
)
|
||||
logger.warning("%s: An error occurred in %s.run", err, self.__class__.__name__)
|
||||
|
||||
finally:
|
||||
await self.clean_up()
|
||||
@@ -149,9 +135,7 @@ class TaskQueue:
|
||||
async def clean_up(self):
|
||||
try:
|
||||
if self.on_exit == "complete_priority" and len(self.priority_tasks) > 0:
|
||||
logger.warning(
|
||||
f"Completing {len(self.priority_tasks)} priority tasks..."
|
||||
)
|
||||
logger.warning(f"Completing {len(self.priority_tasks)} priority tasks...")
|
||||
queue_task = asyncio.create_task(self.queue.join())
|
||||
self.tasks.append(queue_task)
|
||||
await queue_task
|
||||
@@ -161,9 +145,7 @@ class TaskQueue:
|
||||
...
|
||||
|
||||
except Exception as err:
|
||||
logger.error(
|
||||
f"%s: Error occurred in %s.clean_up", err, self.__class__.__name__
|
||||
)
|
||||
logger.error(f"%s: Error occurred in %s.clean_up", err, self.__class__.__name__)
|
||||
|
||||
finally:
|
||||
self.cancel()
|
||||
|
||||
@@ -15,6 +15,7 @@ class Account(_Base, AccountInfo):
|
||||
Attributes:
|
||||
connected (bool): Status of connection to MetaTrader 5 Terminal
|
||||
"""
|
||||
|
||||
_instance: Self
|
||||
connected: bool
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ class BackTester:
|
||||
config (Config): Config instance
|
||||
mt (MetaBackTester): MetaTrader instance
|
||||
"""
|
||||
|
||||
config: Config
|
||||
executor: Executor
|
||||
mt: MetaBackTester
|
||||
@@ -57,18 +58,12 @@ class BackTester:
|
||||
self.backtest_engine.setup_account_sync()
|
||||
self.init_strategies_sync()
|
||||
if (strategies := len(self.executor.strategy_runners)) == 0:
|
||||
logger.warning(
|
||||
"No strategies were added to the backtester. Exiting ..."
|
||||
)
|
||||
logger.warning("No strategies were added to the backtester. Exiting ...")
|
||||
raise Exception("No strategies added to the backtester")
|
||||
self.config.task_queue.worker_timeout = 5
|
||||
self.add_coroutine(
|
||||
coroutine=self.config.task_queue.run, on_separate_thread=True
|
||||
)
|
||||
self.add_coroutine(coroutine=self.config.task_queue.run, on_separate_thread=True)
|
||||
self.add_coroutine(coroutine=self.executor.exit)
|
||||
self.add_coroutine(
|
||||
coroutine=self.backtest_controller.control, on_separate_thread=True
|
||||
)
|
||||
self.add_coroutine(coroutine=self.backtest_controller.control, on_separate_thread=True)
|
||||
parties = strategies + 1
|
||||
self.backtest_controller.set_parties(parties=parties)
|
||||
except Exception as err:
|
||||
@@ -92,31 +87,19 @@ class BackTester:
|
||||
await self.backtest_engine.setup_account()
|
||||
await self.init_strategies()
|
||||
if (strategies := len(self.executor.strategy_runners)) == 0:
|
||||
logger.warning(
|
||||
"No strategies were added to the backtester. Exiting ..."
|
||||
)
|
||||
logger.warning("No strategies were added to the backtester. Exiting ...")
|
||||
raise Exception("No strategies added to the backtester")
|
||||
self.config.task_queue.worker_timeout = 5
|
||||
self.add_coroutine(
|
||||
coroutine=self.config.task_queue.run, on_separate_thread=True
|
||||
)
|
||||
self.add_coroutine(coroutine=self.config.task_queue.run, on_separate_thread=True)
|
||||
self.add_coroutine(coroutine=self.executor.exit)
|
||||
self.add_coroutine(
|
||||
coroutine=self.backtest_controller.control, on_separate_thread=True
|
||||
)
|
||||
self.add_coroutine(coroutine=self.backtest_controller.control, on_separate_thread=True)
|
||||
parties = strategies + 1
|
||||
self.backtest_controller.set_parties(parties=parties)
|
||||
except Exception as err:
|
||||
logger.error(f"{err}. Backtester initialization failed")
|
||||
raise SystemExit
|
||||
|
||||
def add_coroutine(
|
||||
self,
|
||||
*,
|
||||
coroutine: Callable[..., ...] | Coroutine,
|
||||
on_separate_thread=False,
|
||||
**kwargs,
|
||||
):
|
||||
def add_coroutine(self, *, coroutine: Callable[..., ...] | Coroutine, on_separate_thread=False, **kwargs):
|
||||
"""Add a coroutine to the executor.
|
||||
|
||||
Args:
|
||||
@@ -127,9 +110,7 @@ class BackTester:
|
||||
Returns:
|
||||
|
||||
"""
|
||||
self.executor.add_coroutine(
|
||||
coroutine=coroutine, kwargs=kwargs, on_separate_thread=on_separate_thread
|
||||
)
|
||||
self.executor.add_coroutine(coroutine=coroutine, kwargs=kwargs, on_separate_thread=on_separate_thread)
|
||||
|
||||
def execute(self):
|
||||
"""Execute the bot."""
|
||||
@@ -161,14 +142,7 @@ class BackTester:
|
||||
"""
|
||||
[self.add_strategy(strategy=strategy) for strategy in strategies]
|
||||
|
||||
def add_strategy_all(
|
||||
self,
|
||||
*,
|
||||
strategy: Type[Strategy],
|
||||
params: dict | None = None,
|
||||
symbols: list[Symbol] = None,
|
||||
**kwargs,
|
||||
):
|
||||
def add_strategy_all(self, *, strategy: Type[Strategy], params: dict | None = None, symbols: list[Symbol] = None, **kwargs):
|
||||
"""Use this to run a single strategy on multiple symbols with the same parameters and keyword arguments.
|
||||
|
||||
Keyword Args:
|
||||
@@ -177,10 +151,7 @@ class BackTester:
|
||||
symbols (list): A list of symbols to run the strategy on
|
||||
**kwargs: Additional keyword arguments for the strategy
|
||||
"""
|
||||
[
|
||||
self.add_strategy(strategy=strategy(symbol=symbol, params=params, **kwargs))
|
||||
for symbol in symbols
|
||||
]
|
||||
[self.add_strategy(strategy=strategy(symbol=symbol, params=params, **kwargs)) for symbol in symbols]
|
||||
|
||||
async def init_strategy(self, *, strategy: Strategy) -> bool:
|
||||
"""Initialize a single strategy. This method is called internally by the bot."""
|
||||
|
||||
+8
-29
@@ -21,6 +21,7 @@ class Bot:
|
||||
config (Config): Config instance
|
||||
mt (MetaTrader): MetaTrader instance
|
||||
"""
|
||||
|
||||
config: Config
|
||||
executor: Executor
|
||||
mt: MetaTrader
|
||||
@@ -61,9 +62,7 @@ class Bot:
|
||||
raise Exception("Unable to sign in to MetaTrader 5 Terminal")
|
||||
logger.info("Login Successful")
|
||||
await self.init_strategies()
|
||||
self.add_coroutine(
|
||||
coroutine=self.config.task_queue.run, on_separate_thread=True
|
||||
)
|
||||
self.add_coroutine(coroutine=self.config.task_queue.run, on_separate_thread=True)
|
||||
self.add_coroutine(coroutine=self.executor.exit)
|
||||
|
||||
if len(self.executor.strategy_runners) == 0:
|
||||
@@ -87,9 +86,7 @@ class Bot:
|
||||
raise Exception("Unable to sign in to MetaTrader 5 Terminal")
|
||||
logger.info("Login Successful")
|
||||
self.init_strategies_sync()
|
||||
self.add_coroutine(
|
||||
coroutine=self.config.task_queue.run, on_separate_thread=True
|
||||
)
|
||||
self.add_coroutine(coroutine=self.config.task_queue.run, on_separate_thread=True)
|
||||
self.add_coroutine(coroutine=self.executor.exit)
|
||||
|
||||
if len(self.executor.strategy_runners) == 0:
|
||||
@@ -107,13 +104,7 @@ class Bot:
|
||||
"""
|
||||
self.executor.add_function(function=function, kwargs=kwargs)
|
||||
|
||||
def add_coroutine(
|
||||
self,
|
||||
*,
|
||||
coroutine: Callable[..., ...] | Coroutine,
|
||||
on_separate_thread=False,
|
||||
**kwargs,
|
||||
):
|
||||
def add_coroutine(self, *, coroutine: Callable[..., ...] | Coroutine, on_separate_thread=False, **kwargs):
|
||||
"""Add a coroutine to the executor.
|
||||
|
||||
Args:
|
||||
@@ -124,9 +115,7 @@ class Bot:
|
||||
Returns:
|
||||
|
||||
"""
|
||||
self.executor.add_coroutine(
|
||||
coroutine=coroutine, kwargs=kwargs, on_separate_thread=on_separate_thread
|
||||
)
|
||||
self.executor.add_coroutine(coroutine=coroutine, kwargs=kwargs, on_separate_thread=on_separate_thread)
|
||||
|
||||
def execute(self):
|
||||
"""Execute the bot using asyncio.run"""
|
||||
@@ -157,14 +146,7 @@ class Bot:
|
||||
"""
|
||||
[self.add_strategy(strategy=strategy) for strategy in strategies]
|
||||
|
||||
def add_strategy_all(
|
||||
self,
|
||||
*,
|
||||
strategy: Type[Strategy],
|
||||
params: dict | None = None,
|
||||
symbols: list[Symbol] = None,
|
||||
**kwargs,
|
||||
):
|
||||
def add_strategy_all(self, *, strategy: Type[Strategy], params: dict | None = None, symbols: list[Symbol] = None, **kwargs):
|
||||
"""Use this to run a single strategy on multiple symbols with the same parameters and keyword arguments.
|
||||
|
||||
Keyword Args:
|
||||
@@ -173,10 +155,7 @@ class Bot:
|
||||
symbols (list): A list of symbols to run the strategy on
|
||||
**kwargs: Additional keyword arguments for the strategy
|
||||
"""
|
||||
[
|
||||
self.add_strategy(strategy=strategy(symbol=symbol, params=params, **kwargs))
|
||||
for symbol in symbols
|
||||
]
|
||||
[self.add_strategy(strategy=strategy(symbol=symbol, params=params, **kwargs)) for symbol in symbols]
|
||||
|
||||
async def init_strategy(self, *, strategy: Strategy) -> bool:
|
||||
"""Initialize a single strategy. This method is called internally by the bot."""
|
||||
@@ -200,7 +179,7 @@ class Bot:
|
||||
if info is not None and tick is not None:
|
||||
info = info._asdict()
|
||||
info["swap_rollover3days"] = info.get("swap_rollover3days", 0) % 7
|
||||
info['select'] = select
|
||||
info["select"] = select
|
||||
tick = Tick(**tick._asdict())
|
||||
strategy.symbol.tick = tick
|
||||
strategy.symbol.set_attributes(**info)
|
||||
|
||||
+13
-25
@@ -1,4 +1,5 @@
|
||||
"""Candle and Candles classes for handling bars from the MetaTrader 5 terminal."""
|
||||
|
||||
import time
|
||||
from typing import Type, Self, Iterable
|
||||
from logging import getLogger
|
||||
@@ -45,9 +46,7 @@ class Candle:
|
||||
**kwargs: Candle attributes and values as keyword arguments.
|
||||
"""
|
||||
if not all(i in kwargs for i in ["open", "high", "low", "close"]):
|
||||
raise ValueError(
|
||||
"Candle must be instantiated with open, high, low and close prices"
|
||||
)
|
||||
raise ValueError("Candle must be instantiated with open, high, low and close prices")
|
||||
self.time = kwargs.pop("time", time.monotonic_ns())
|
||||
self.Index = kwargs.pop("Index", 0)
|
||||
self.real_volume = kwargs.pop("real_volume", 0)
|
||||
@@ -56,18 +55,15 @@ class Candle:
|
||||
self.set_attributes(**kwargs)
|
||||
|
||||
def __repr__(self):
|
||||
return (
|
||||
"%(class)s(Index=%(Index)s, time=%(time)s, open=%(open)s, high=%(high)s, low=%(low)s, close=%(close)s)"
|
||||
% {
|
||||
"class": self.__class__.__name__,
|
||||
"open": self.open,
|
||||
"high": self.high,
|
||||
"low": self.low,
|
||||
"close": self.close,
|
||||
"time": self.time,
|
||||
"Index": self.Index,
|
||||
}
|
||||
)
|
||||
return "%(class)s(Index=%(Index)s, time=%(time)s, open=%(open)s, high=%(high)s, low=%(low)s, close=%(close)s)" % {
|
||||
"class": self.__class__.__name__,
|
||||
"open": self.open,
|
||||
"high": self.high,
|
||||
"low": self.low,
|
||||
"close": self.close,
|
||||
"time": self.time,
|
||||
"Index": self.Index,
|
||||
}
|
||||
|
||||
def __eq__(self, other: Self):
|
||||
return self.time == other.time
|
||||
@@ -170,13 +166,7 @@ class Candles:
|
||||
timeframe: TimeFrame
|
||||
_data: DataFrame
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
data: DataFrame | Self | Iterable,
|
||||
flip=False,
|
||||
candle_class: Candle = None,
|
||||
):
|
||||
def __init__(self, *, data: DataFrame | Self | Iterable, flip=False, candle_class: Candle = None):
|
||||
"""A container class of Candle objects in chronological order.
|
||||
|
||||
Args:
|
||||
@@ -236,9 +226,7 @@ class Candles:
|
||||
|
||||
if item == "Index":
|
||||
return Series(self._data.index)
|
||||
raise AttributeError(
|
||||
f"Attribute {item} not defined on class {self.__class__.__name__}"
|
||||
)
|
||||
raise AttributeError(f"Attribute {item} not defined on class {self.__class__.__name__}")
|
||||
|
||||
def __iter__(self):
|
||||
return (self.Candle(**row._asdict()) for row in self._data.itertuples())
|
||||
|
||||
@@ -19,6 +19,7 @@ class Executor:
|
||||
coroutines (list[Coroutine]): A list of coroutines to run in the executor
|
||||
functions (dict[Callable, dict]): A dictionary of functions to run in the executor
|
||||
"""
|
||||
|
||||
executor: ThreadPoolExecutor
|
||||
tasks: list[asyncio.Task]
|
||||
config: Config
|
||||
@@ -36,18 +37,10 @@ class Executor:
|
||||
kwargs = kwargs or {}
|
||||
self.functions[function] = kwargs
|
||||
|
||||
def add_coroutine(
|
||||
self,
|
||||
*,
|
||||
coroutine: Callable | Coroutine,
|
||||
kwargs: dict = None,
|
||||
on_separate_thread=False,
|
||||
):
|
||||
def add_coroutine(self, *, coroutine: Callable | Coroutine, kwargs: dict = None, on_separate_thread=False):
|
||||
kwargs = kwargs or {}
|
||||
coroutine = coroutine(**kwargs)
|
||||
self.coroutines.append(
|
||||
coroutine
|
||||
) if on_separate_thread is False else self.coroutine_threads.append(coroutine)
|
||||
self.coroutines.append(coroutine) if on_separate_thread is False else self.coroutine_threads.append(coroutine)
|
||||
|
||||
def add_strategies(self, *, strategies: tuple[Strategy]):
|
||||
"""Add multiple strategies at once
|
||||
@@ -85,9 +78,7 @@ class Executor:
|
||||
|
||||
async def create_coroutines_task(self):
|
||||
""""""
|
||||
task = asyncio.create_task(
|
||||
asyncio.gather(*self.coroutines, return_exceptions=True)
|
||||
)
|
||||
task = asyncio.create_task(asyncio.gather(*self.coroutines, return_exceptions=True))
|
||||
self.tasks.append(task)
|
||||
await task
|
||||
|
||||
@@ -116,9 +107,7 @@ class Executor:
|
||||
start = asyncio.get_event_loop().time()
|
||||
try:
|
||||
while self.config.shutdown is False and self.config.force_shutdown is False:
|
||||
if self.timeout is not None and self.timeout < (
|
||||
asyncio.get_event_loop().time() - start
|
||||
):
|
||||
if self.timeout is not None and self.timeout < (asyncio.get_event_loop().time() - start):
|
||||
self.config.shutdown = True
|
||||
|
||||
for strategy in self.strategy_runners:
|
||||
@@ -140,25 +129,11 @@ class Executor:
|
||||
Notes:
|
||||
No matter the number specified, the executor will always use a minimum of 5 workers.
|
||||
"""
|
||||
workers_ = (
|
||||
len(self.strategy_runners)
|
||||
+ len(self.functions)
|
||||
+ len(self.coroutine_threads)
|
||||
+ 2
|
||||
)
|
||||
workers_ = len(self.strategy_runners) + len(self.functions) + len(self.coroutine_threads) + 2
|
||||
workers = max(workers, workers_)
|
||||
with ThreadPoolExecutor(max_workers=workers) as executor:
|
||||
self.executor = executor
|
||||
[
|
||||
self.executor.submit(self.run_strategy, strategy)
|
||||
for strategy in self.strategy_runners
|
||||
]
|
||||
[
|
||||
self.executor.submit(function, **kwargs)
|
||||
for function, kwargs in self.functions.items()
|
||||
]
|
||||
[
|
||||
self.executor.submit(self.run_coroutine_task, coroutine)
|
||||
for coroutine in self.coroutine_threads
|
||||
]
|
||||
[self.executor.submit(self.run_strategy, strategy) for strategy in self.strategy_runners]
|
||||
[self.executor.submit(function, **kwargs) for function, kwargs in self.functions.items()]
|
||||
[self.executor.submit(self.run_coroutine_task, coroutine) for coroutine in self.coroutine_threads]
|
||||
self.executor.submit(self.run_coroutine_tasks)
|
||||
|
||||
+10
-51
@@ -34,14 +34,7 @@ class History:
|
||||
total_orders: int
|
||||
group: str
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
date_from: datetime | float,
|
||||
date_to: datetime | float,
|
||||
group: str = "",
|
||||
use_utc: bool = True,
|
||||
):
|
||||
def __init__(self, *, date_from: datetime | float, date_to: datetime | float, group: str = "", use_utc: bool = True):
|
||||
"""
|
||||
Args:
|
||||
date_from (datetime, float): Date the orders are requested from. Set by the 'datetime' object or as a
|
||||
@@ -54,16 +47,8 @@ class History:
|
||||
"""
|
||||
self.config = Config()
|
||||
self.mt5 = MetaTrader() if self.config.mode != "backtest" else MetaBackTester()
|
||||
date_from = (
|
||||
date_from
|
||||
if isinstance(date_from, datetime)
|
||||
else datetime.fromtimestamp(date_from)
|
||||
)
|
||||
date_to = (
|
||||
date_to
|
||||
if isinstance(date_to, datetime)
|
||||
else datetime.fromtimestamp(date_to)
|
||||
)
|
||||
date_from = date_from if isinstance(date_from, datetime) else datetime.fromtimestamp(date_from)
|
||||
date_to = date_to if isinstance(date_to, datetime) else datetime.fromtimestamp(date_to)
|
||||
self.date_from = date_from.astimezone(pytz.UTC) if use_utc else date_from
|
||||
self.date_to = date_to.astimezone(pytz.UTC) if use_utc else date_to
|
||||
self.group = group
|
||||
@@ -74,9 +59,7 @@ class History:
|
||||
|
||||
async def initialize(self):
|
||||
"""Get history deals and orders"""
|
||||
deals, orders = await asyncio.gather(
|
||||
self.get_deals(), self.get_orders(), return_exceptions=True
|
||||
)
|
||||
deals, orders = await asyncio.gather(self.get_deals(), self.get_orders(), return_exceptions=True)
|
||||
self.deals = deals if isinstance(deals, tuple) else ()
|
||||
self.orders = orders if isinstance(orders, tuple) else ()
|
||||
self.total_deals = len(self.deals)
|
||||
@@ -89,9 +72,7 @@ class History:
|
||||
Returns:
|
||||
tuple[TradeDeal, ...]: A list of trade deals
|
||||
"""
|
||||
deals = await self.mt5.history_deals_get(
|
||||
date_from=self.date_from, date_to=self.date_to, group=self.group
|
||||
)
|
||||
deals = await self.mt5.history_deals_get(date_from=self.date_from, date_to=self.date_to, group=self.group)
|
||||
if deals is not None:
|
||||
return tuple(TradeDeal(**deal._asdict()) for deal in deals)
|
||||
logger.warning(f"Failed to get deals")
|
||||
@@ -107,12 +88,7 @@ class History:
|
||||
Returns:
|
||||
tuple[TradeDeal]: A tuple of all deals with the order ticket
|
||||
"""
|
||||
return tuple(
|
||||
sorted(
|
||||
(deal for deal in self.deals if deal.order == ticket),
|
||||
key=lambda x: x.time_msc,
|
||||
)
|
||||
)
|
||||
return tuple(sorted((deal for deal in self.deals if deal.order == ticket), key=lambda x: x.time_msc))
|
||||
|
||||
def get_deals_by_position(self, *, position: int = None) -> tuple[TradeDeal, ...]:
|
||||
"""
|
||||
@@ -123,12 +99,7 @@ class History:
|
||||
Returns:
|
||||
tuple[TradeDeal]: A tuple of all deals with the position ticket
|
||||
"""
|
||||
return tuple(
|
||||
sorted(
|
||||
(deal for deal in self.deals if deal.position_id == position),
|
||||
key=lambda x: x.time_msc,
|
||||
)
|
||||
)
|
||||
return tuple(sorted((deal for deal in self.deals if deal.position_id == position), key=lambda x: x.time_msc))
|
||||
|
||||
@backoff_decorator
|
||||
async def get_orders(self) -> tuple[TradeOrder, ...]:
|
||||
@@ -137,9 +108,7 @@ class History:
|
||||
Returns:
|
||||
list[TradeOrder]: A list of trade orders
|
||||
"""
|
||||
orders = await self.mt5.history_orders_get(
|
||||
date_from=self.date_from, date_to=self.date_to, group=self.group
|
||||
)
|
||||
orders = await self.mt5.history_orders_get(date_from=self.date_from, date_to=self.date_to, group=self.group)
|
||||
|
||||
if orders is not None:
|
||||
return tuple(TradeOrder(**order._asdict()) for order in orders)
|
||||
@@ -149,18 +118,8 @@ class History:
|
||||
|
||||
def get_orders_by_ticket(self, *, ticket: int) -> tuple[TradeOrder, ...]:
|
||||
"""filter orders by ticket"""
|
||||
return tuple(
|
||||
sorted(
|
||||
(order for order in self.orders if order.ticket == ticket),
|
||||
key=lambda x: x.time_done_msc,
|
||||
)
|
||||
)
|
||||
return tuple(sorted((order for order in self.orders if order.ticket == ticket), key=lambda x: x.time_done_msc))
|
||||
|
||||
def get_orders_by_position(self, *, position: int) -> tuple[TradeOrder, ...]:
|
||||
"""filter orders by position"""
|
||||
return tuple(
|
||||
sorted(
|
||||
(order for order in self.orders if order.position_id == position),
|
||||
key=lambda x: x.time_done_msc,
|
||||
)
|
||||
)
|
||||
return tuple(sorted((order for order in self.orders if order.position_id == position), key=lambda x: x.time_done_msc))
|
||||
|
||||
+7
-34
@@ -25,12 +25,7 @@ class Order(_Base, TradeRequest):
|
||||
type_time (OrderTime.DAY): Order time
|
||||
type_filling (OrderFilling.FOK): Order filling
|
||||
"""
|
||||
kwargs = {
|
||||
"action": TradeAction.DEAL,
|
||||
"type_time": OrderTime.DAY,
|
||||
"type_filling": OrderFilling.FOK,
|
||||
**kwargs,
|
||||
}
|
||||
kwargs = {"action": TradeAction.DEAL, "type_time": OrderTime.DAY, "type_filling": OrderFilling.FOK, **kwargs}
|
||||
super().__init__(**kwargs)
|
||||
|
||||
async def orders_total(self):
|
||||
@@ -111,9 +106,7 @@ class Order(_Base, TradeRequest):
|
||||
Returns:
|
||||
float: Returns float value if successful
|
||||
"""
|
||||
res = await self.mt5.order_calc_margin(
|
||||
self.type, self.symbol, self.volume, self.price
|
||||
)
|
||||
res = await self.mt5.order_calc_margin(self.type, self.symbol, self.volume, self.price)
|
||||
return res
|
||||
|
||||
@error_handler(response=0, log_error_msg=False)
|
||||
@@ -124,16 +117,8 @@ class Order(_Base, TradeRequest):
|
||||
float: Returns float value if successful
|
||||
None: If not successful
|
||||
"""
|
||||
action, symbol, volume, price_open, price_close = (
|
||||
self.type,
|
||||
self.symbol,
|
||||
self.volume,
|
||||
self.price,
|
||||
self.tp,
|
||||
)
|
||||
res = await self.mt5.order_calc_profit(
|
||||
action, symbol, volume, price_open, price_close
|
||||
)
|
||||
action, symbol, volume, price_open, price_close = (self.type, self.symbol, self.volume, self.price, self.tp)
|
||||
res = await self.mt5.order_calc_profit(action, symbol, volume, price_open, price_close)
|
||||
return res
|
||||
|
||||
@error_handler(response=0, log_error_msg=False)
|
||||
@@ -144,23 +129,11 @@ class Order(_Base, TradeRequest):
|
||||
float: Returns float value if successful
|
||||
None: If not successful
|
||||
"""
|
||||
action, symbol, volume, price_open, price_close = (
|
||||
self.type,
|
||||
self.symbol,
|
||||
self.volume,
|
||||
self.price,
|
||||
self.sl,
|
||||
)
|
||||
res = await self.mt5.order_calc_profit(
|
||||
action, symbol, volume, price_open, price_close
|
||||
)
|
||||
action, symbol, volume, price_open, price_close = (self.type, self.symbol, self.volume, self.price, self.sl)
|
||||
res = await self.mt5.order_calc_profit(action, symbol, volume, price_open, price_close)
|
||||
return res
|
||||
|
||||
@property
|
||||
def request(self) -> dict:
|
||||
"""Return the order request as a dictionary."""
|
||||
return {
|
||||
key: value
|
||||
for key, value in self.dict.items()
|
||||
if key in self.mt5.TradeRequest.__match_args__
|
||||
}
|
||||
return {key: value for key, value in self.dict.items() if key in self.mt5.TradeRequest.__match_args__}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"""Handle Open positions."""
|
||||
|
||||
import asyncio
|
||||
from logging import getLogger
|
||||
|
||||
@@ -70,9 +71,7 @@ class Positions:
|
||||
return tuple(TradePosition(**pos._asdict()) for pos in (positions or ()))
|
||||
|
||||
@staticmethod
|
||||
async def close(
|
||||
*, ticket: int, symbol: str, price: float, volume: float, order_type: OrderType
|
||||
) -> OrderSendResult:
|
||||
async def close(*, ticket: int, symbol: str, price: float, volume: float, order_type: OrderType) -> OrderSendResult:
|
||||
"""Close an open position for the trading account using the ticket and other parameters.
|
||||
|
||||
Args:
|
||||
@@ -82,14 +81,7 @@ class Positions:
|
||||
volume (float): Volume to close.
|
||||
order_type (OrderType): Order type.
|
||||
"""
|
||||
order = Order(
|
||||
action=TradeAction.DEAL,
|
||||
price=price,
|
||||
position=ticket,
|
||||
symbol=symbol,
|
||||
volume=volume,
|
||||
type=order_type.opposite,
|
||||
)
|
||||
order = Order(action=TradeAction.DEAL, price=price, position=ticket, symbol=symbol, volume=volume, type=order_type.opposite)
|
||||
return await order.send()
|
||||
|
||||
async def close_position_by_ticket(self, *, ticket: int) -> OrderSendResult | None:
|
||||
@@ -127,14 +119,5 @@ class Positions:
|
||||
int: Return number of positions closed.
|
||||
"""
|
||||
positions = self.positions or await self.get_positions()
|
||||
results = await asyncio.gather(
|
||||
*(self.close_position(position=position) for position in positions),
|
||||
return_exceptions=True,
|
||||
)
|
||||
return len(
|
||||
[
|
||||
res
|
||||
for res in results
|
||||
if (isinstance(res, OrderSendResult) and res.retcode == 10009)
|
||||
]
|
||||
)
|
||||
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)])
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"""Risk Assessment and Management"""
|
||||
|
||||
from .account import Account
|
||||
from .positions import Positions
|
||||
|
||||
|
||||
@@ -35,12 +35,8 @@ class Result:
|
||||
self.name = name or self.parameters.get("name", "Trades")
|
||||
|
||||
def get_data(self) -> dict:
|
||||
res = self.result.get_dict(
|
||||
exclude={"retcode", "comment", "retcode_external", "request_id", "request"}
|
||||
)
|
||||
return (
|
||||
self.parameters | res | {"actual_profit": 0, "closed": False, "win": False}
|
||||
)
|
||||
res = self.result.get_dict(exclude={"retcode", "comment", "retcode_external", "request_id", "request"})
|
||||
return self.parameters | res | {"actual_profit": 0, "closed": False, "win": False}
|
||||
|
||||
async def save(self, *, trade_record_mode: Literal["csv", "json"] = None):
|
||||
"""Record trade results as a csv or json file
|
||||
@@ -71,9 +67,7 @@ class Result:
|
||||
headers.update(data.keys())
|
||||
read_file.close()
|
||||
with file.open("w", newline="") as write_file:
|
||||
writer = csv.DictWriter(
|
||||
write_file, fieldnames=headers, restval=None, extrasaction="ignore"
|
||||
)
|
||||
writer = csv.DictWriter(write_file, fieldnames=headers, restval=None, extrasaction="ignore")
|
||||
writer.writeheader()
|
||||
writer.writerows(rows)
|
||||
except Exception as err:
|
||||
|
||||
+11
-46
@@ -23,12 +23,7 @@ def delta(obj: time) -> timedelta:
|
||||
Args:
|
||||
obj (datetime.time): A datetime.time object.
|
||||
"""
|
||||
return timedelta(
|
||||
hours=obj.hour,
|
||||
minutes=obj.minute,
|
||||
seconds=obj.second,
|
||||
microseconds=obj.microsecond,
|
||||
)
|
||||
return timedelta(hours=obj.hour, minutes=obj.minute, seconds=obj.second, microseconds=obj.microsecond)
|
||||
|
||||
|
||||
async def backtest_sleep(secs):
|
||||
@@ -58,9 +53,7 @@ class Session:
|
||||
*,
|
||||
start: int | time,
|
||||
end: int | time,
|
||||
on_start: Literal[
|
||||
"close_all", "close_win", "close_loss", "custom_start"
|
||||
] = None,
|
||||
on_start: Literal["close_all", "close_win", "close_loss", "custom_start"] = None,
|
||||
on_end: Literal["close_all", "close_win", "close_loss", "custom_end"] = None,
|
||||
custom_start: Callable = None,
|
||||
custom_end: Callable = None,
|
||||
@@ -79,11 +72,7 @@ class Session:
|
||||
custom_end (Callable): A custom function to call when the session ends. Default is None.
|
||||
name (str): A name for the session. Default is a combination of start and end.
|
||||
"""
|
||||
self.start = (
|
||||
start.replace(tzinfo=UTC)
|
||||
if isinstance(start, time)
|
||||
else time(hour=start, tzinfo=UTC)
|
||||
)
|
||||
self.start = start.replace(tzinfo=UTC) if isinstance(start, time) else time(hour=start, tzinfo=UTC)
|
||||
self.end = end if isinstance(end, time) else time(hour=end, tzinfo=UTC)
|
||||
self.on_start = on_start
|
||||
self.on_end = on_end
|
||||
@@ -112,9 +101,7 @@ class Session:
|
||||
now = (
|
||||
datetime.now(tz=UTC).time()
|
||||
if self.config.mode == "live"
|
||||
else datetime.fromtimestamp(
|
||||
self.config.backtest_engine.cursor.time, tz=UTC
|
||||
).time()
|
||||
else datetime.fromtimestamp(self.config.backtest_engine.cursor.time, tz=UTC).time()
|
||||
)
|
||||
return now in self
|
||||
|
||||
@@ -133,13 +120,7 @@ class Session:
|
||||
return Duration(hours=hours, minutes=minutes, seconds=seconds)
|
||||
|
||||
async def close_positions(self, *, positions: tuple[TradePosition, ...]):
|
||||
results = asyncio.gather(
|
||||
*(
|
||||
self.positions_manager.close_position(position=position)
|
||||
for position in positions
|
||||
),
|
||||
return_exceptions=True,
|
||||
)
|
||||
results = asyncio.gather(*(self.positions_manager.close_position(position=position) for position in positions), return_exceptions=True)
|
||||
closed = pending = 0
|
||||
for result in results:
|
||||
if isinstance(result, OrderSendResult) and result.retcode == 10009:
|
||||
@@ -155,16 +136,12 @@ class Session:
|
||||
|
||||
async def close_win(self):
|
||||
open_positions = await self.positions_manager.get_positions()
|
||||
positions = tuple(
|
||||
position for position in open_positions if position.profit >= 0
|
||||
)
|
||||
positions = tuple(position for position in open_positions if position.profit >= 0)
|
||||
await self.close_positions(positions=positions)
|
||||
|
||||
async def close_loss(self):
|
||||
open_positions = await self.positions_manager.get_positions()
|
||||
positions = tuple(
|
||||
position for position in open_positions if position.profit < 0
|
||||
)
|
||||
positions = tuple(position for position in open_positions if position.profit < 0)
|
||||
await self.close_positions(positions=positions)
|
||||
|
||||
async def action(self, *, action):
|
||||
@@ -198,9 +175,7 @@ class Session:
|
||||
def until(self):
|
||||
"""Get the seconds until the session starts from the current time in seconds."""
|
||||
if self.config.mode == "backtest":
|
||||
now = datetime.fromtimestamp(
|
||||
self.config.backtest_engine.cursor.time, tz=UTC
|
||||
).time()
|
||||
now = datetime.fromtimestamp(self.config.backtest_engine.cursor.time, tz=UTC).time()
|
||||
secs = (delta(self.start) - delta(now)).seconds
|
||||
else:
|
||||
secs = (delta(self.start) - delta(datetime.now(tz=UTC).time())).seconds
|
||||
@@ -237,11 +212,7 @@ class Sessions:
|
||||
moment = (
|
||||
moment or datetime.now(tz=UTC).time()
|
||||
if self.config.mode == "live"
|
||||
else (
|
||||
datetime.fromtimestamp(
|
||||
self.config.backtest_engine.cursor.time, tz=UTC
|
||||
).time()
|
||||
)
|
||||
else (datetime.fromtimestamp(self.config.backtest_engine.cursor.time, tz=UTC).time())
|
||||
)
|
||||
for session in self.sessions:
|
||||
if moment in session:
|
||||
@@ -260,11 +231,7 @@ class Sessions:
|
||||
moment = (
|
||||
moment or datetime.now(tz=UTC).time()
|
||||
if self.config.mode == "live"
|
||||
else (
|
||||
datetime.fromtimestamp(
|
||||
self.config.backtest_engine.cursor.time, tz=UTC
|
||||
).time()
|
||||
)
|
||||
else (datetime.fromtimestamp(self.config.backtest_engine.cursor.time, tz=UTC).time())
|
||||
)
|
||||
for session in self.sessions:
|
||||
if delta(moment) < delta(session.start):
|
||||
@@ -287,9 +254,7 @@ class Sessions:
|
||||
return
|
||||
|
||||
if self.config.mode == "backtest":
|
||||
now = datetime.fromtimestamp(
|
||||
self.config.backtest_engine.cursor.time, tz=UTC
|
||||
).time()
|
||||
now = datetime.fromtimestamp(self.config.backtest_engine.cursor.time, tz=UTC).time()
|
||||
else:
|
||||
now = datetime.now(tz=UTC).time()
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"""The base class for creating strategies."""
|
||||
|
||||
import asyncio
|
||||
from time import time
|
||||
from typing import TypeVar
|
||||
@@ -46,9 +47,7 @@ class Strategy(ABC):
|
||||
backtest_controller = BackTestController
|
||||
current_session = Session
|
||||
|
||||
def __init__(
|
||||
self, *, symbol: Symbol, params: dict = None, sessions: Sessions = None, name=""
|
||||
):
|
||||
def __init__(self, *, symbol: Symbol, params: dict = None, sessions: Sessions = None, name=""):
|
||||
"""Initiate the parameters dict and add name and symbol fields.
|
||||
Use class name as strategy name if name is not provided
|
||||
|
||||
@@ -62,9 +61,7 @@ class Strategy(ABC):
|
||||
self.parameters["symbol"] = symbol.name
|
||||
self.parameters["name"] = self.name
|
||||
self.running = True
|
||||
self.sessions = sessions or Sessions(
|
||||
sessions=[Session(start=0, end=dtime(hour=23, minute=59, second=59))]
|
||||
)
|
||||
self.sessions = sessions or Sessions(sessions=[Session(start=0, end=dtime(hour=23, minute=59, second=59))])
|
||||
self.config = Config()
|
||||
self.mt5 = MetaTrader() if self.config.mode != "backtest" else MetaBackTester()
|
||||
self.backtest_controller = BackTestController()
|
||||
@@ -165,9 +162,7 @@ class Strategy(ABC):
|
||||
async def backtest_strategy(self):
|
||||
"""Backtest the strategy."""
|
||||
async with self as _:
|
||||
logger.info(
|
||||
"Testing %s strategy on %s with Backtester", self.name, self.symbol.name
|
||||
)
|
||||
logger.info("Testing %s strategy on %s with Backtester", self.name, self.symbol.name)
|
||||
while self.running:
|
||||
try:
|
||||
await self.sessions.check()
|
||||
|
||||
+12
-48
@@ -1,4 +1,5 @@
|
||||
"""Symbol class for handling a financial instrument."""
|
||||
|
||||
from datetime import datetime
|
||||
from logging import getLogger
|
||||
|
||||
@@ -157,10 +158,7 @@ class Symbol(_Base, SymbolInfo):
|
||||
if check := self.volume_min <= volume <= self.volume_max:
|
||||
return check, volume
|
||||
else:
|
||||
return (
|
||||
check,
|
||||
self.volume_min if volume <= self.volume_min else self.volume_max,
|
||||
)
|
||||
return (check, self.volume_min if volume <= self.volume_min else self.volume_max)
|
||||
|
||||
def round_off_volume(self, *, volume: float, round_down: bool = False) -> float:
|
||||
"""Round off the volume to the nearest volume step.
|
||||
@@ -177,11 +175,7 @@ class Symbol(_Base, SymbolInfo):
|
||||
async def amount_in_quote_currency(self, *, amount: float) -> float:
|
||||
"""Convert the amount to the quote currency of the symbol."""
|
||||
if self.currency_profit != self.account.currency:
|
||||
amount = await self.convert_currency(
|
||||
amount=amount,
|
||||
from_currency=self.account.currency,
|
||||
to_currency=self.currency_profit,
|
||||
)
|
||||
amount = await self.convert_currency(amount=amount, from_currency=self.account.currency, to_currency=self.currency_profit)
|
||||
return amount
|
||||
|
||||
async def compute_volume(self) -> float:
|
||||
@@ -194,9 +188,7 @@ class Symbol(_Base, SymbolInfo):
|
||||
"""
|
||||
return self.volume_min
|
||||
|
||||
async def convert_currency(
|
||||
self, *, amount: float, from_currency: str, to_currency: str
|
||||
) -> float:
|
||||
async def convert_currency(self, *, amount: float, from_currency: str, to_currency: str) -> float:
|
||||
"""Convert a given amount from one currency to the other.
|
||||
Args:
|
||||
amount: Amount to convert
|
||||
@@ -215,14 +207,10 @@ class Symbol(_Base, SymbolInfo):
|
||||
if tick is not None:
|
||||
return round(amount / tick.ask, 2)
|
||||
except Exception as err:
|
||||
logger.warning(
|
||||
f"{err}: Currency conversion failed: Unable to convert {amount} in {quote} to {base}"
|
||||
)
|
||||
logger.warning(f"{err}: Currency conversion failed: Unable to convert {amount} in {quote} to {base}")
|
||||
|
||||
@backoff_decorator
|
||||
async def copy_rates_from(
|
||||
self, *, timeframe: TimeFrame, date_from: datetime | int, count: int = 500
|
||||
) -> Candles:
|
||||
async def copy_rates_from(self, *, timeframe: TimeFrame, date_from: datetime | int, count: int = 500) -> Candles:
|
||||
"""
|
||||
Get bars from the MetaTrader 5 terminal starting from the specified date.
|
||||
|
||||
@@ -246,9 +234,7 @@ class Symbol(_Base, SymbolInfo):
|
||||
raise ValueError(f"Could not get rates for {self.name}.")
|
||||
|
||||
@backoff_decorator
|
||||
async def copy_rates_from_pos(
|
||||
self, *, timeframe: TimeFrame, count: int = 500, start_position: int = 0
|
||||
) -> Candles:
|
||||
async def copy_rates_from_pos(self, *, timeframe: TimeFrame, count: int = 500, start_position: int = 0) -> Candles:
|
||||
"""Get bars from the MetaTrader 5 terminal starting from the specified index.
|
||||
|
||||
Args:
|
||||
@@ -265,21 +251,13 @@ class Symbol(_Base, SymbolInfo):
|
||||
Raises:
|
||||
ValueError: If request was unsuccessful and None was returned
|
||||
"""
|
||||
rates = await self.mt5.copy_rates_from_pos(
|
||||
self.name, timeframe, start_position, count
|
||||
)
|
||||
rates = await self.mt5.copy_rates_from_pos(self.name, timeframe, start_position, count)
|
||||
if rates is not None:
|
||||
return Candles(data=rates)
|
||||
raise ValueError(f"Could not get rates for {self.name}.")
|
||||
|
||||
@backoff_decorator
|
||||
async def copy_rates_range(
|
||||
self,
|
||||
*,
|
||||
timeframe: TimeFrame,
|
||||
date_from: datetime | int,
|
||||
date_to: datetime | int,
|
||||
) -> Candles:
|
||||
async def copy_rates_range(self, *, timeframe: TimeFrame, date_from: datetime | int, date_to: datetime | int) -> Candles:
|
||||
"""Get bars in the specified date range from the MetaTrader 5 terminal.
|
||||
|
||||
Args:
|
||||
@@ -299,21 +277,13 @@ class Symbol(_Base, SymbolInfo):
|
||||
Raises:
|
||||
ValueError: If request was unsuccessful and None was returned
|
||||
"""
|
||||
rates = await self.mt5.copy_rates_range(
|
||||
symbol=self.name, timeframe=timeframe, date_from=date_from, date_to=date_to
|
||||
)
|
||||
rates = await self.mt5.copy_rates_range(symbol=self.name, timeframe=timeframe, date_from=date_from, date_to=date_to)
|
||||
if rates is not None:
|
||||
return Candles(data=rates)
|
||||
raise ValueError(f"Could not get rates for {self.name}.")
|
||||
|
||||
@backoff_decorator
|
||||
async def copy_ticks_from(
|
||||
self,
|
||||
*,
|
||||
date_from: datetime | int,
|
||||
count: int = 100,
|
||||
flags: CopyTicks = CopyTicks.ALL,
|
||||
) -> Ticks:
|
||||
async def copy_ticks_from(self, *, date_from: datetime | int, count: int = 100, flags: CopyTicks = CopyTicks.ALL) -> Ticks:
|
||||
"""
|
||||
Get ticks from the MetaTrader 5 terminal starting from the specified date.
|
||||
|
||||
@@ -336,13 +306,7 @@ class Symbol(_Base, SymbolInfo):
|
||||
raise ValueError(f"Could not get ticks for {self.name}.")
|
||||
|
||||
@backoff_decorator
|
||||
async def copy_ticks_range(
|
||||
self,
|
||||
*,
|
||||
date_from: datetime | int,
|
||||
date_to: datetime | int,
|
||||
flags: CopyTicks = CopyTicks.ALL,
|
||||
) -> Ticks:
|
||||
async def copy_ticks_range(self, *, date_from: datetime | int, date_to: datetime | int, flags: CopyTicks = CopyTicks.ALL) -> Ticks:
|
||||
"""Get ticks for the specified date range from the MetaTrader 5 terminal.
|
||||
|
||||
Args:
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"""Terminal related functions and properties"""
|
||||
|
||||
from typing import NamedTuple
|
||||
from logging import getLogger
|
||||
|
||||
@@ -7,9 +8,7 @@ from ..core.base import _Base
|
||||
|
||||
logger = getLogger(__name__)
|
||||
|
||||
Version = NamedTuple(
|
||||
"Version", (("version", str), ("build", int), ("release_date", str))
|
||||
)
|
||||
Version = NamedTuple("Version", (("version", str), ("build", int), ("release_date", str)))
|
||||
|
||||
|
||||
class Terminal(_Base, TerminalInfo):
|
||||
|
||||
+11
-18
@@ -38,27 +38,22 @@ class Tick:
|
||||
"""Initialize the Tick class. Set attributes from keyword arguments. bid, ask, last and volume must be
|
||||
present"""
|
||||
if not all(key in kwargs for key in ["bid", "ask", "last", "volume"]):
|
||||
raise ValueError(
|
||||
"bid, ask, last and volume, time must be present in the keyword arguments"
|
||||
)
|
||||
raise ValueError("bid, ask, last and volume, time must be present in the keyword arguments")
|
||||
self.Index = kwargs.pop("Index", 0)
|
||||
self.time = kwargs.pop("time", time.monotonic())
|
||||
self.time_msc = int(self.time * 1000)
|
||||
self.set_attributes(**kwargs)
|
||||
|
||||
def __repr__(self):
|
||||
return (
|
||||
"%(class)s(Index=%(Index)s, time=%(time)s, bid=%(bid)s, ask=%(ask)s, last=%(last)s, volume=%(volume)s)"
|
||||
% {
|
||||
"class": self.__class__.__name__,
|
||||
"time": self.time,
|
||||
"bid": self.bid,
|
||||
"ask": self.ask,
|
||||
"last": self.last,
|
||||
"volume": self.volume,
|
||||
"Index": self.Index,
|
||||
}
|
||||
)
|
||||
return "%(class)s(Index=%(Index)s, time=%(time)s, bid=%(bid)s, ask=%(ask)s, last=%(last)s, volume=%(volume)s)" % {
|
||||
"class": self.__class__.__name__,
|
||||
"time": self.time,
|
||||
"bid": self.bid,
|
||||
"ask": self.ask,
|
||||
"last": self.last,
|
||||
"volume": self.volume,
|
||||
"Index": self.Index,
|
||||
}
|
||||
|
||||
def __eq__(self, other: Self):
|
||||
return self.time == other.time
|
||||
@@ -148,9 +143,7 @@ class Ticks:
|
||||
def __getattr__(self, item):
|
||||
if item in list(self._data.columns.values):
|
||||
return self._data[item]
|
||||
raise AttributeError(
|
||||
f"Attribute {item} not defined on class {self.__class__.__name__}"
|
||||
)
|
||||
raise AttributeError(f"Attribute {item} not defined on class {self.__class__.__name__}")
|
||||
|
||||
def __getitem__(self, index) -> Tick | Self:
|
||||
if isinstance(index, slice):
|
||||
|
||||
@@ -73,12 +73,7 @@ class TradeRecords:
|
||||
rows = await self.update_rows(rows=rows)
|
||||
|
||||
with open(file, mode="w", newline="") as fw:
|
||||
writer = csv.DictWriter(
|
||||
fw,
|
||||
fieldnames=reader.fieldnames,
|
||||
extrasaction="ignore",
|
||||
restval=None,
|
||||
)
|
||||
writer = csv.DictWriter(fw, fieldnames=reader.fieldnames, extrasaction="ignore", restval=None)
|
||||
writer.writeheader()
|
||||
writer.writerows(rows)
|
||||
except Exception as err:
|
||||
@@ -119,12 +114,7 @@ class TradeRecords:
|
||||
deals = [
|
||||
deal
|
||||
for deal in deals
|
||||
if (
|
||||
deal.order != deal.position_id
|
||||
and deal.position_id == order
|
||||
and deal.entry == 1
|
||||
and deal.position_id not in position_ids
|
||||
)
|
||||
if (deal.order != deal.position_id and deal.position_id == order and deal.entry == 1 and deal.position_id not in position_ids)
|
||||
]
|
||||
deals.sort(key=lambda deal: deal.time_msc)
|
||||
deal = deals[-1]
|
||||
@@ -157,16 +147,12 @@ class TradeRecords:
|
||||
|
||||
async def update_csv_records(self):
|
||||
"""Update csv trade records in the records_dir folder."""
|
||||
records = [
|
||||
self.read_update_csv(file=record) for record in self.get_csv_records()
|
||||
]
|
||||
records = [self.read_update_csv(file=record) for record in self.get_csv_records()]
|
||||
await asyncio.gather(*records)
|
||||
|
||||
async def update_json_records(self):
|
||||
"""Update json trade records in the records_dir folder."""
|
||||
records = [
|
||||
self.read_update_json(file=record) for record in self.get_json_records()
|
||||
]
|
||||
records = [self.read_update_json(file=record) for record in self.get_json_records()]
|
||||
await asyncio.gather(*records)
|
||||
|
||||
async def update_csv_record(self, *, file: Path | str):
|
||||
|
||||
+12
-53
@@ -60,17 +60,11 @@ class Trader(ABC):
|
||||
sl, tp = pips, pips * (risk_to_reward or self.ram.risk_to_reward)
|
||||
price = self.order.price
|
||||
if self.order.type == OrderType.BUY:
|
||||
self.order.sl, self.order.tp = round(price - sl, self.symbol.digits), round(
|
||||
price + tp, self.symbol.digits
|
||||
)
|
||||
self.order.sl, self.order.tp = round(price - sl, self.symbol.digits), round(price + tp, self.symbol.digits)
|
||||
elif self.order.type == OrderType.SELL:
|
||||
self.order.sl, self.order.tp = round(price + sl, self.symbol.digits), round(
|
||||
price - tp, self.symbol.digits
|
||||
)
|
||||
self.order.sl, self.order.tp = round(price + sl, self.symbol.digits), round(price - tp, self.symbol.digits)
|
||||
|
||||
def set_trade_stop_levels_points(
|
||||
self, *, points: float, risk_to_reward: float = None
|
||||
):
|
||||
def set_trade_stop_levels_points(self, *, points: float, risk_to_reward: float = None):
|
||||
"""Set the stop loss and take profit levels of the order based on the points and the risk to reward ratio.
|
||||
It is assumed that order_type and price are already set before calling this method.
|
||||
|
||||
@@ -83,23 +77,12 @@ class Trader(ABC):
|
||||
price, digits = self.order.price, self.symbol.digits
|
||||
|
||||
if self.order.type == OrderType.BUY:
|
||||
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)
|
||||
|
||||
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
|
||||
calculate the volume.
|
||||
|
||||
@@ -115,18 +98,9 @@ class Trader(ABC):
|
||||
tick = await self.symbol.info_tick()
|
||||
price = tick.ask if order_type == OrderType.BUY else tick.bid
|
||||
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.
|
||||
|
||||
@@ -146,18 +120,9 @@ class Trader(ABC):
|
||||
dtp = dsl * (risk_to_reward or self.ram.risk_to_reward)
|
||||
tp = price + dtp if order_type == OrderType.BUY else price - dtp
|
||||
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.
|
||||
|
||||
Args:
|
||||
@@ -218,9 +183,7 @@ class Trader(ABC):
|
||||
return result
|
||||
|
||||
@error_handler
|
||||
async def record_trade(
|
||||
self, *, result: OrderSendResult, parameters: dict = None, name: str = ""
|
||||
):
|
||||
async def record_trade(self, *, result: OrderSendResult, parameters: dict = None, name: str = ""):
|
||||
"""Record the trade in csv or json.
|
||||
Args:
|
||||
result (OrderSendResult): Result of the order send
|
||||
@@ -232,11 +195,7 @@ class Trader(ABC):
|
||||
params = {**parameters} or {}
|
||||
profit = await self.order.calc_profit()
|
||||
params["expected_profit"] = profit
|
||||
date = (
|
||||
datetime.now(tz=UTC)
|
||||
if self.config.mode == "live"
|
||||
else datetime.fromtimestamp(self.config.backtest_engine.cursor.time, tz=UTC)
|
||||
)
|
||||
date = datetime.now(tz=UTC) if self.config.mode == "live" else datetime.fromtimestamp(self.config.backtest_engine.cursor.time, tz=UTC)
|
||||
params["date"] = date.strftime("%Y-%m-%d %H:%M:%S.%f")
|
||||
res = Result(result=result, parameters=params, name=name)
|
||||
self.config.task_queue.add(item=QueueItem(res.save), must_complete=True)
|
||||
|
||||
@@ -32,11 +32,7 @@ async def close_all_positions():
|
||||
positions = await mt.positions_get()
|
||||
tasks = []
|
||||
for position in positions:
|
||||
order_type = (
|
||||
mt.ORDER_TYPE_BUY
|
||||
if position.type == mt.ORDER_TYPE_SELL
|
||||
else mt.ORDER_TYPE_SELL
|
||||
)
|
||||
order_type = mt.ORDER_TYPE_BUY if position.type == mt.ORDER_TYPE_SELL else mt.ORDER_TYPE_SELL
|
||||
req = {
|
||||
"action": mt.TRADE_ACTION_DEAL,
|
||||
"symbol": position.symbol,
|
||||
@@ -54,9 +50,7 @@ async def close_all_positions():
|
||||
@pytest.fixture(scope="package", autouse=True)
|
||||
async def config(request):
|
||||
Path("tests/backtest/configs").mkdir(exist_ok=True)
|
||||
with open("aiomql.json", "r") as fh, open(
|
||||
"tests/backtest/configs/test2.json", "w"
|
||||
) as fh1, open("tests/backtest/test.json", "w") as fh2:
|
||||
with open("aiomql.json", "r") as fh, open("tests/backtest/configs/test2.json", "w") as fh1, open("tests/backtest/test.json", "w") as fh2:
|
||||
data = json.load(fh)
|
||||
data["mode"] = "backtest"
|
||||
json.dump(data, fh1, indent=2)
|
||||
@@ -77,19 +71,14 @@ async def mt():
|
||||
|
||||
@pytest.fixture(scope="package")
|
||||
async def period():
|
||||
return {
|
||||
"start": datetime(2024, 2, 1, hour=8, tzinfo=UTC),
|
||||
"end": datetime(2024, 2, 7, hour=16, tzinfo=UTC),
|
||||
}
|
||||
return {"start": datetime(2024, 2, 1, hour=8, tzinfo=UTC), "end": datetime(2024, 2, 7, hour=16, tzinfo=UTC)}
|
||||
|
||||
|
||||
@pytest.fixture(scope="package")
|
||||
async def backtest_engine(period):
|
||||
start = period["start"]
|
||||
end = period["end"]
|
||||
return BackTestEngine(
|
||||
start=start, end=end, name="backtest_data", assign_to_config=True, preload=False
|
||||
)
|
||||
return BackTestEngine(start=start, end=end, name="backtest_data", assign_to_config=True, preload=False)
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
|
||||
@@ -27,9 +27,7 @@ async def make_buy_sell_orders():
|
||||
return {"buy": Order(**buy_req), "sell": Order(**sell_req)}
|
||||
|
||||
|
||||
def test_trade_mode(
|
||||
config, backtest_engine, history, positions, order_sell, order_buy, btc_usd
|
||||
):
|
||||
def test_trade_mode(config, backtest_engine, history, positions, order_sell, order_buy, btc_usd):
|
||||
assert config.mode == "backtest"
|
||||
assert isinstance(backtest_engine, BackTestEngine)
|
||||
assert isinstance(history.mt5, MetaBackTester)
|
||||
@@ -79,26 +77,16 @@ async def test_history(backtest_engine, history, order_sell, order_buy, position
|
||||
async def test_margin(backtest_engine, order_sell, order_buy):
|
||||
await backtest_engine.setup_account(balance=100)
|
||||
so_margin = await backtest_engine.order_calc_margin(
|
||||
action=order_sell.action,
|
||||
volume=order_sell.volume,
|
||||
symbol=order_sell.symbol,
|
||||
price=order_sell.price,
|
||||
action=order_sell.action, volume=order_sell.volume, symbol=order_sell.symbol, price=order_sell.price
|
||||
)
|
||||
bo_margin = await backtest_engine.order_calc_margin(
|
||||
action=order_buy.action,
|
||||
volume=order_buy.volume,
|
||||
symbol=order_buy.symbol,
|
||||
price=order_buy.price,
|
||||
action=order_buy.action, volume=order_buy.volume, symbol=order_buy.symbol, price=order_buy.price
|
||||
)
|
||||
total_margin = so_margin + bo_margin
|
||||
await backtest_engine.order_send(request=order_sell.request)
|
||||
await backtest_engine.order_send(request=order_buy.request)
|
||||
# noinspection PyTestUnpassedFixture
|
||||
assert (
|
||||
backtest_engine.positions.margin
|
||||
== total_margin
|
||||
== backtest_engine._account.margin
|
||||
)
|
||||
assert backtest_engine.positions.margin == total_margin == backtest_engine._account.margin
|
||||
backtest_engine.reset(clear_data=True)
|
||||
|
||||
|
||||
@@ -124,11 +112,7 @@ async def test_account(backtest_engine, positions):
|
||||
deal = backtest_engine.deals.history_deals_get(position=bo.order)
|
||||
bo_profit = deal[-1].profit
|
||||
assert len(all_pos) == 1
|
||||
assert (
|
||||
backtest_engine.positions.margin
|
||||
== backtest_engine._account.margin
|
||||
== backtest_engine.positions.margins[so.order]
|
||||
)
|
||||
assert backtest_engine.positions.margin == backtest_engine._account.margin == backtest_engine.positions.margins[so.order]
|
||||
profit = sum([pos.profit for pos in all_pos])
|
||||
n_balance = backtest_engine._account.balance
|
||||
n_equity = backtest_engine._account.equity
|
||||
@@ -155,9 +139,7 @@ async def test_wrapup(positions, buy_order, sell_order, backtest_engine, config)
|
||||
last_equity = backtest_engine._account.equity
|
||||
last_profit = backtest_engine._account.profit
|
||||
tdata = GetData.load_data(name=config.backtest_dir / f"{backtest_engine.name}.pkl")
|
||||
new_bte = BackTestEngine(
|
||||
data=tdata, restart=False, assign_to_config=False, preload=False
|
||||
)
|
||||
new_bte = BackTestEngine(data=tdata, restart=False, assign_to_config=False, preload=False)
|
||||
assert new_bte._account.balance == last_balance
|
||||
assert new_bte._account.equity == last_equity
|
||||
assert new_bte._account.profit == last_profit
|
||||
|
||||
@@ -21,8 +21,4 @@ async def test_deals_manager(backtest_engine, sell_order, buy_order, period, pos
|
||||
deals = backtest_engine.deals.history_deals_get(position=bo.order)
|
||||
assert len(deals) <= 2
|
||||
orders = backtest_engine.deals.get_deals_range(date_from=start, date_to=end)
|
||||
assert (
|
||||
len(orders)
|
||||
== backtest_engine.deals.history_deals_total(date_from=start, date_to=end)
|
||||
== len(backtest_engine.deals._data.keys())
|
||||
)
|
||||
assert len(orders) == backtest_engine.deals.history_deals_total(date_from=start, date_to=end) == len(backtest_engine.deals._data.keys())
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# noinspection PyTestUnpassedFixture
|
||||
async def test_orders_manager(
|
||||
backtest_engine, sell_order, buy_order, period, positions
|
||||
):
|
||||
async def test_orders_manager(backtest_engine, sell_order, buy_order, period, positions):
|
||||
backtest_engine.reset(clear_data=True)
|
||||
await backtest_engine.setup_account(balance=100)
|
||||
backtest_engine.fast_forward(steps=100)
|
||||
@@ -23,8 +21,4 @@ async def test_orders_manager(
|
||||
orders = backtest_engine.orders.history_orders_get(position=bo.order)
|
||||
assert len(orders) <= 2
|
||||
orders = backtest_engine.orders.get_orders_range(date_from=start, date_to=end)
|
||||
assert (
|
||||
len(orders)
|
||||
== backtest_engine.orders.history_orders_total(date_from=start, date_to=end)
|
||||
== len(backtest_engine.orders._data.keys())
|
||||
)
|
||||
assert len(orders) == backtest_engine.orders.history_orders_total(date_from=start, date_to=end) == len(backtest_engine.orders._data.keys())
|
||||
|
||||
@@ -29,11 +29,7 @@ async def close_all_positions():
|
||||
positions = await mt.positions_get()
|
||||
tasks = []
|
||||
for position in positions:
|
||||
order_type = (
|
||||
mt.ORDER_TYPE_BUY
|
||||
if position.type == mt.ORDER_TYPE_SELL
|
||||
else mt.ORDER_TYPE_SELL
|
||||
)
|
||||
order_type = mt.ORDER_TYPE_BUY if position.type == mt.ORDER_TYPE_SELL else mt.ORDER_TYPE_SELL
|
||||
req = {
|
||||
"action": mt.TRADE_ACTION_DEAL,
|
||||
"symbol": position.symbol,
|
||||
@@ -51,9 +47,7 @@ async def close_all_positions():
|
||||
@pytest.fixture(scope="package", autouse=True)
|
||||
async def config(request):
|
||||
Path("tests/live/configs").mkdir(exist_ok=True)
|
||||
with open("aiomql.json", "r") as fh, open(
|
||||
"tests/live/configs/test2.json", "w"
|
||||
) as fh1, open("tests/live/test.json", "w") as fh2:
|
||||
with open("aiomql.json", "r") as fh, open("tests/live/configs/test2.json", "w") as fh1, open("tests/live/test.json", "w") as fh2:
|
||||
data = json.load(fh)
|
||||
json.dump(data, fh1, indent=2)
|
||||
json.dump(data, fh2, indent=2)
|
||||
|
||||
@@ -6,10 +6,7 @@ from aiomql.contrib.symbols import ForexSymbol
|
||||
|
||||
|
||||
async def test_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 = ["BTCUSD", "SOLUSD", "ETHUSD"]
|
||||
symbols = [ForexSymbol(name=sym) for sym in syms]
|
||||
strategies = [Chaos(symbol=symbol, name="test_chaos") for symbol in symbols]
|
||||
|
||||
@@ -6,10 +6,7 @@ from aiomql.contrib.symbols import ForexSymbol
|
||||
|
||||
|
||||
def test_bot_sync():
|
||||
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 = ["BTCUSD", "SOLUSD", "ETHUSD"]
|
||||
symbols = [ForexSymbol(name=sym) for sym in syms]
|
||||
strategies = [Chaos(symbol=symbol, name="test_chaos") for symbol in symbols]
|
||||
|
||||
@@ -36,13 +36,7 @@ class TestRecordsAndResults:
|
||||
async def sell(self, mt):
|
||||
sym = "BTCUSD"
|
||||
sym_info = await mt.symbol_info(sym)
|
||||
return {
|
||||
"action": mt.TRADE_ACTION_DEAL,
|
||||
"symbol": sym,
|
||||
"volume": sym_info.volume_min,
|
||||
"type": mt.ORDER_TYPE_SELL,
|
||||
"price": sym_info.bid,
|
||||
}
|
||||
return {"action": mt.TRADE_ACTION_DEAL, "symbol": sym, "volume": sym_info.volume_min, "type": mt.ORDER_TYPE_SELL, "price": sym_info.bid}
|
||||
|
||||
@pytest.fixture(scope="class", autouse=True)
|
||||
async def setup(self, sell, buy, mt):
|
||||
@@ -50,24 +44,11 @@ class TestRecordsAndResults:
|
||||
buy_res_2 = await mt.order_send(buy)
|
||||
sell_res = await mt.order_send(sell)
|
||||
sell_res_2 = await mt.order_send(sell)
|
||||
buy_res = Result(
|
||||
result=OrderSendResult(**buy_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"
|
||||
)
|
||||
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"),
|
||||
)
|
||||
buy_res = Result(result=OrderSendResult(**buy_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")
|
||||
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 Positions().close_all()
|
||||
|
||||
def test_records_dir(self):
|
||||
@@ -90,9 +71,7 @@ class TestRecordsAndResults:
|
||||
|
||||
async def test_json_records(self):
|
||||
json_records = self.trade_records.get_json_records()
|
||||
matched_recs = [
|
||||
record for record in json_records if record.match("test_result.json")
|
||||
]
|
||||
matched_recs = [record for record in json_records if record.match("test_result.json")]
|
||||
assert len(matched_recs) == 1
|
||||
record = matched_recs[0]
|
||||
record_data = json.load(record.open())
|
||||
@@ -106,9 +85,7 @@ class TestRecordsAndResults:
|
||||
|
||||
async def test_csv_records(self):
|
||||
csv_records = self.trade_records.get_csv_records()
|
||||
matched_recs = [
|
||||
record for record in csv_records if record.match("test_result.csv")
|
||||
]
|
||||
matched_recs = [record for record in csv_records if record.match("test_result.csv")]
|
||||
assert len(matched_recs) == 1
|
||||
record = matched_recs[0]
|
||||
record_data = DictReader(record.open())
|
||||
|
||||
@@ -14,40 +14,20 @@ class TestBackTestEngine:
|
||||
def setup_class(cls):
|
||||
cls.start = datetime(2024, 2, 1)
|
||||
cls.end = datetime(2024, 2, 7)
|
||||
cls.g_data = GetData(
|
||||
start=cls.start,
|
||||
end=cls.end,
|
||||
symbols=["BTCUSD", "SOLUSD"],
|
||||
timeframes=[TimeFrame.H1, TimeFrame.H2],
|
||||
name="test_engine",
|
||||
)
|
||||
cls.bte = BackTestEngine(
|
||||
start=cls.start, end=cls.end, assign_to_config=True, preload=False
|
||||
)
|
||||
cls.g_data = GetData(start=cls.start, end=cls.end, symbols=["BTCUSD", "SOLUSD"], timeframes=[TimeFrame.H1, TimeFrame.H2], name="test_engine")
|
||||
cls.bte = BackTestEngine(start=cls.start, end=cls.end, assign_to_config=True, preload=False)
|
||||
|
||||
@pytest.fixture(scope="class")
|
||||
async def bte2(self):
|
||||
await self.g_data.get_data()
|
||||
bte2 = BackTestEngine(
|
||||
start=self.start,
|
||||
end=self.end,
|
||||
data=self.g_data.data,
|
||||
use_terminal=False,
|
||||
preload=False,
|
||||
)
|
||||
bte2 = BackTestEngine(start=self.start, end=self.end, data=self.g_data.data, use_terminal=False, preload=False)
|
||||
await bte2.setup_account(balance=100)
|
||||
return bte2
|
||||
|
||||
@pytest.fixture(scope="class")
|
||||
async def sell_order(self):
|
||||
sym = await self.bte.get_symbol_info(symbol="BTCUSD")
|
||||
request = {
|
||||
"type": OrderType.SELL,
|
||||
"symbol": "BTCUSD",
|
||||
"volume": sym.volume_min,
|
||||
"price": sym.bid,
|
||||
"action": TradeAction.DEAL,
|
||||
}
|
||||
request = {"type": OrderType.SELL, "symbol": "BTCUSD", "volume": sym.volume_min, "price": sym.bid, "action": TradeAction.DEAL}
|
||||
return request
|
||||
|
||||
@pytest.fixture(scope="class")
|
||||
@@ -67,16 +47,11 @@ class TestBackTestEngine:
|
||||
}
|
||||
return request
|
||||
|
||||
def modify_stops(self, order):
|
||||
...
|
||||
def modify_stops(self, order): ...
|
||||
|
||||
def test_span_and_range(self):
|
||||
assert self.bte.range == range(
|
||||
0, int((self.end - self.start).total_seconds()), self.bte.speed
|
||||
)
|
||||
assert self.bte.span == range(
|
||||
int(self.start.timestamp()), int(self.end.timestamp()), self.bte.speed
|
||||
)
|
||||
assert self.bte.range == range(0, int((self.end - self.start).total_seconds()), self.bte.speed)
|
||||
assert self.bte.span == range(int(self.start.timestamp()), int(self.end.timestamp()), self.bte.speed)
|
||||
assert len(self.bte.span) == len(self.bte.range)
|
||||
|
||||
def test_cursor(self):
|
||||
@@ -158,16 +133,12 @@ class TestBackTestEngine:
|
||||
|
||||
async def test_get_rates_from(self):
|
||||
start = datetime(2024, 2, 3, 12, 43, tzinfo=UTC)
|
||||
rates = await self.bte.get_rates_from(
|
||||
symbol="BTCUSD", timeframe=TimeFrame.H1, date_from=start, count=24
|
||||
)
|
||||
rates = await self.bte.get_rates_from(symbol="BTCUSD", timeframe=TimeFrame.H1, date_from=start, count=24)
|
||||
assert len(rates) == 24
|
||||
|
||||
async def test_get_rates_from_2(self, bte2):
|
||||
start = datetime(2024, 2, 3, 12, 12, tzinfo=UTC)
|
||||
rates = await bte2.get_rates_from(
|
||||
symbol="BTCUSD", timeframe=TimeFrame.H1, date_from=start, count=24
|
||||
)
|
||||
rates = await bte2.get_rates_from(symbol="BTCUSD", timeframe=TimeFrame.H1, date_from=start, count=24)
|
||||
assert len(rates) == 24
|
||||
|
||||
async def test_get_rates_from_pos(self):
|
||||
@@ -175,51 +146,37 @@ class TestBackTestEngine:
|
||||
self.bte.go_to(time=now)
|
||||
tf = TimeFrame.H2
|
||||
start_pos = 2
|
||||
rates = await self.bte.get_rates_from_pos(
|
||||
symbol="BTCUSD", timeframe=tf, start_pos=start_pos, count=24
|
||||
)
|
||||
rates = await self.bte.get_rates_from_pos(symbol="BTCUSD", timeframe=tf, start_pos=start_pos, count=24)
|
||||
assert len(rates) == 24
|
||||
assert int(rates[-1][0]) == round_down(
|
||||
int(now.replace(hour=now.hour - start_pos).timestamp()), tf.seconds
|
||||
)
|
||||
assert int(rates[-1][0]) == round_down(int(now.replace(hour=now.hour - start_pos).timestamp()), tf.seconds)
|
||||
|
||||
async def test_get_rates_from_pos2(self, bte2):
|
||||
now = datetime(2024, 2, 4, 12, 15, tzinfo=UTC)
|
||||
bte2.go_to(time=now)
|
||||
tf = TimeFrame.H1
|
||||
start_pos = 2
|
||||
rates = await bte2.get_rates_from_pos(
|
||||
symbol="BTCUSD", timeframe=tf, start_pos=start_pos, count=24
|
||||
)
|
||||
assert int(rates[-1][0]) == round_down(
|
||||
int(now.replace(hour=10).timestamp()), tf.seconds
|
||||
)
|
||||
rates = await bte2.get_rates_from_pos(symbol="BTCUSD", timeframe=tf, start_pos=start_pos, count=24)
|
||||
assert int(rates[-1][0]) == round_down(int(now.replace(hour=10).timestamp()), tf.seconds)
|
||||
# assert int(rates[-1][0]) == round_up(int(now.timestamp()), tf.seconds) - start_pos * tf.seconds
|
||||
assert len(rates) == 24
|
||||
|
||||
async def test_get_rates_range(self):
|
||||
start = datetime(2024, 2, 3, 12, tzinfo=UTC)
|
||||
end = datetime(2024, 2, 4, 18, tzinfo=UTC)
|
||||
rates = await self.bte.get_rates_range(
|
||||
symbol="BTCUSD", timeframe=TimeFrame.H1, date_from=start, date_to=end
|
||||
)
|
||||
rates = await self.bte.get_rates_range(symbol="BTCUSD", timeframe=TimeFrame.H1, date_from=start, date_to=end)
|
||||
assert len(rates) == 31
|
||||
assert int(rates[-1][0]) == int(end.timestamp())
|
||||
|
||||
async def test_get_rates_range2(self, bte2):
|
||||
start = datetime(2024, 2, 3, 12, tzinfo=UTC)
|
||||
end = datetime(2024, 2, 4, 18, tzinfo=UTC)
|
||||
rates = await bte2.get_rates_range(
|
||||
symbol="BTCUSD", timeframe=TimeFrame.H1, date_from=start, date_to=end
|
||||
)
|
||||
rates = await bte2.get_rates_range(symbol="BTCUSD", timeframe=TimeFrame.H1, date_from=start, date_to=end)
|
||||
assert len(rates) == 31
|
||||
assert int(rates[-1][0]) == int(end.timestamp())
|
||||
|
||||
async def test_get_ticks_from(self):
|
||||
start = datetime(2024, 2, 3, 12, tzinfo=UTC)
|
||||
ticks = await self.bte.get_ticks_from(
|
||||
symbol="BTCUSD", date_from=start, count=24
|
||||
)
|
||||
ticks = await self.bte.get_ticks_from(symbol="BTCUSD", date_from=start, count=24)
|
||||
assert len(ticks) == 24
|
||||
|
||||
async def test_get_ticks_from2(self, bte2):
|
||||
@@ -230,23 +187,15 @@ class TestBackTestEngine:
|
||||
async def test_get_ticks_range(self):
|
||||
start = datetime(2024, 2, 3, 12, tzinfo=UTC)
|
||||
end = datetime(2024, 2, 3, 15, tzinfo=UTC)
|
||||
ticks = await self.bte.get_ticks_range(
|
||||
symbol="BTCUSD", date_from=start, date_to=end
|
||||
)
|
||||
approx_total = (
|
||||
end - start
|
||||
).total_seconds() // 2 # assuming 2 ticks per second at least
|
||||
ticks = await self.bte.get_ticks_range(symbol="BTCUSD", date_from=start, date_to=end)
|
||||
approx_total = (end - start).total_seconds() // 2 # assuming 2 ticks per second at least
|
||||
assert len(ticks) >= approx_total
|
||||
|
||||
async def test_get_ticks_range2(self, bte2):
|
||||
start = datetime(2024, 2, 3, 12, tzinfo=UTC)
|
||||
end = datetime(2024, 2, 3, 15, tzinfo=UTC)
|
||||
ticks = await bte2.get_ticks_range(
|
||||
symbol="BTCUSD", date_from=start, date_to=end
|
||||
)
|
||||
approx_total = (
|
||||
end - start
|
||||
).total_seconds() // 2 # assuming 2 ticks per second at least
|
||||
ticks = await bte2.get_ticks_range(symbol="BTCUSD", date_from=start, date_to=end)
|
||||
approx_total = (end - start).total_seconds() // 2 # assuming 2 ticks per second at least
|
||||
assert len(ticks) >= approx_total
|
||||
|
||||
async def test_price_tick(self, bte2):
|
||||
@@ -287,22 +236,14 @@ class TestBackTestEngine:
|
||||
tp = sym_info.ask + dsl
|
||||
|
||||
profit = await self.bte.order_calc_profit(
|
||||
action=OrderType.BUY,
|
||||
symbol=sym,
|
||||
volume=sym_info.volume_min,
|
||||
price_open=sym_info.ask,
|
||||
price_close=tp,
|
||||
action=OrderType.BUY, symbol=sym, volume=sym_info.volume_min, price_open=sym_info.ask, price_close=tp
|
||||
)
|
||||
assert profit > 0
|
||||
sym_info2 = await bte2.get_symbol_info(symbol=sym)
|
||||
dsl2 = (sym_info2.trade_stops_level + sym_info2.spread) * 2 * sym_info2.point
|
||||
tp2 = sym_info2.ask + dsl2
|
||||
profit2 = await bte2.order_calc_profit(
|
||||
action=OrderType.BUY,
|
||||
symbol=sym,
|
||||
volume=sym_info2.volume_min,
|
||||
price_open=sym_info2.ask,
|
||||
price_close=tp2,
|
||||
action=OrderType.BUY, symbol=sym, volume=sym_info2.volume_min, price_open=sym_info2.ask, price_close=tp2
|
||||
)
|
||||
assert ceil(profit) == ceil(profit2)
|
||||
|
||||
@@ -314,20 +255,10 @@ class TestBackTestEngine:
|
||||
bte2.go_to(time=moment)
|
||||
sym = "BTCUSD"
|
||||
sym_info = await self.bte.get_symbol_info(symbol=sym)
|
||||
margin = await self.bte.order_calc_margin(
|
||||
action=OrderType.SELL,
|
||||
symbol=sym,
|
||||
volume=sym_info.volume_min,
|
||||
price=sym_info.bid,
|
||||
)
|
||||
margin = await self.bte.order_calc_margin(action=OrderType.SELL, symbol=sym, volume=sym_info.volume_min, price=sym_info.bid)
|
||||
assert margin > 0
|
||||
sym_info2 = await self.bte.get_symbol_info(symbol=sym)
|
||||
margin2 = await bte2.order_calc_margin(
|
||||
action=OrderType.SELL,
|
||||
symbol=sym,
|
||||
volume=sym_info2.volume_min,
|
||||
price=sym_info2.bid,
|
||||
)
|
||||
margin2 = await bte2.order_calc_margin(action=OrderType.SELL, symbol=sym, volume=sym_info2.volume_min, price=sym_info2.bid)
|
||||
assert margin2 > 0
|
||||
|
||||
async def test_order_check(self, buy_order, sell_order):
|
||||
|
||||
@@ -11,12 +11,8 @@ from aiomql.core.constants import TimeFrame
|
||||
class TestCandle:
|
||||
@classmethod
|
||||
def setup_class(cls):
|
||||
cls.bullish_candle = Candle(
|
||||
open=1.3421, high=1.3462, low=1.3405, close=1.3452, time=0, Index=0
|
||||
)
|
||||
cls.bearish_candle = Candle(
|
||||
open=1.3452, high=1.3405, low=1.3462, close=1.3421, time=1, Index=1
|
||||
)
|
||||
cls.bullish_candle = Candle(open=1.3421, high=1.3462, low=1.3405, close=1.3452, time=0, Index=0)
|
||||
cls.bearish_candle = Candle(open=1.3452, high=1.3405, low=1.3462, close=1.3421, time=1, Index=1)
|
||||
|
||||
def test_repr(self):
|
||||
repr_str = repr(self.bearish_candle)
|
||||
|
||||
@@ -14,13 +14,7 @@ class TestGetData:
|
||||
cls.end = datetime(2024, 2, 2, tzinfo=UTC)
|
||||
cls.symbols = ["BTCUSD", "ETHUSD"]
|
||||
cls.timeframes = [TimeFrame.H1, TimeFrame.H2]
|
||||
cls.g_data = GetData(
|
||||
start=cls.start,
|
||||
end=cls.end,
|
||||
symbols=cls.symbols,
|
||||
timeframes=cls.timeframes,
|
||||
name="test_data",
|
||||
)
|
||||
cls.g_data = GetData(start=cls.start, end=cls.end, symbols=cls.symbols, timeframes=cls.timeframes, name="test_data")
|
||||
|
||||
@pytest.fixture(scope="class", autouse=True)
|
||||
async def get_data(self):
|
||||
@@ -34,9 +28,7 @@ class TestGetData:
|
||||
assert self.g_data.timeframes == set(self.timeframes)
|
||||
assert self.g_data.name == "test_data"
|
||||
assert self.g_data.range == range(int((self.end - self.start).total_seconds()))
|
||||
assert self.g_data.span == range(
|
||||
int(self.start.timestamp()), int(self.end.timestamp())
|
||||
)
|
||||
assert self.g_data.span == range(int(self.start.timestamp()), int(self.end.timestamp()))
|
||||
|
||||
async def test_get_data(self):
|
||||
assert self.g_data.data.fully_loaded is True
|
||||
|
||||
@@ -111,19 +111,13 @@ class TestMetaTrader:
|
||||
assert res.shape[0] == 10
|
||||
|
||||
async def test_copy_ticks_from(self):
|
||||
res = await self.mt.copy_ticks_from(
|
||||
self.symbol, self.start, 10, self.mt.COPY_TICKS_ALL
|
||||
)
|
||||
res = await self.mt.copy_ticks_from(self.symbol, self.start, 10, self.mt.COPY_TICKS_ALL)
|
||||
assert res is not None
|
||||
assert res.shape[0] == 10
|
||||
|
||||
async def test_copy_ticks_range(self):
|
||||
res = await self.mt.copy_ticks_range(
|
||||
self.symbol, self.start, self.end, self.mt.COPY_TICKS_ALL
|
||||
)
|
||||
res2 = self.mt5.copy_ticks_range(
|
||||
self.symbol, self.start, self.end, self.mt5.COPY_TICKS_ALL
|
||||
)
|
||||
res = await self.mt.copy_ticks_range(self.symbol, self.start, self.end, self.mt.COPY_TICKS_ALL)
|
||||
res2 = self.mt5.copy_ticks_range(self.symbol, self.start, self.end, self.mt5.COPY_TICKS_ALL)
|
||||
assert res is not None
|
||||
assert res.shape[0] == res2.shape[0]
|
||||
|
||||
@@ -149,9 +143,7 @@ class TestMetaTrader:
|
||||
price_open = buy_order["price"]
|
||||
price_close = buy_order["tp"]
|
||||
type_ = buy_order["type"]
|
||||
res = await self.mt.order_calc_profit(
|
||||
type_, self.symbol, volume, price_open, price_close
|
||||
)
|
||||
res = await self.mt.order_calc_profit(type_, self.symbol, volume, price_open, price_close)
|
||||
assert isinstance(res, float)
|
||||
|
||||
async def test_order_check(self, buy_order):
|
||||
|
||||
@@ -36,9 +36,7 @@ class TestResult:
|
||||
|
||||
async def test_json(self, order_results):
|
||||
res1, res2 = order_results
|
||||
await asyncio.gather(
|
||||
res1.save(trade_record_mode="json"), res2.save(trade_record_mode="json")
|
||||
)
|
||||
await asyncio.gather(res1.save(trade_record_mode="json"), res2.save(trade_record_mode="json"))
|
||||
assert res1.config.records_dir.exists()
|
||||
record = res1.config.records_dir / f"{res1.name}.json"
|
||||
assert record.exists()
|
||||
|
||||
@@ -15,9 +15,7 @@ class TestSessions:
|
||||
def make_session(self):
|
||||
end = time(hour=16, minute=59, second=59, microsecond=999_999, tzinfo=UTC)
|
||||
london = Session(start=8, end=end, name="London", on_end="close_all")
|
||||
start, end = time(hour=0, tzinfo=UTC), time(
|
||||
hour=23, minute=59, second=59, tzinfo=UTC
|
||||
)
|
||||
start, end = time(hour=0, tzinfo=UTC), time(hour=23, minute=59, second=59, tzinfo=UTC)
|
||||
all_day = Session(start=start, end=end, name="AllDay", on_end="close_all")
|
||||
end = time(hour=6, minute=59, second=59, microsecond=999_999, tzinfo=UTC)
|
||||
over_night = Session(start=18, end=end, name="OverNight", on_end="close_all")
|
||||
|
||||
@@ -35,19 +35,13 @@ class TestSymbol:
|
||||
async def test_rates(self, btc):
|
||||
start = datetime(year=2023, month=10, day=5)
|
||||
end = start + timedelta(hours=9)
|
||||
rates_from = await btc.copy_rates_from(
|
||||
timeframe=btc.mt5.TIMEFRAME_H1, date_from=start, count=10
|
||||
)
|
||||
rates_from = await btc.copy_rates_from(timeframe=btc.mt5.TIMEFRAME_H1, date_from=start, count=10)
|
||||
assert isinstance(rates_from, Candles)
|
||||
assert len(rates_from) == 10
|
||||
rates_from_pos = await btc.copy_rates_from_pos(
|
||||
timeframe=btc.mt5.TIMEFRAME_H1, count=10, start_position=0
|
||||
)
|
||||
rates_from_pos = await btc.copy_rates_from_pos(timeframe=btc.mt5.TIMEFRAME_H1, count=10, start_position=0)
|
||||
assert isinstance(rates_from_pos, Candles)
|
||||
assert len(rates_from_pos) == 10
|
||||
rates_range = await btc.copy_rates_range(
|
||||
timeframe=btc.mt5.TIMEFRAME_H1, date_from=start, date_to=end
|
||||
)
|
||||
rates_range = await btc.copy_rates_range(timeframe=btc.mt5.TIMEFRAME_H1, date_from=start, date_to=end)
|
||||
assert isinstance(rates_range, Candles)
|
||||
assert len(rates_range) == 10
|
||||
ticks_from = await btc.copy_ticks_from(date_from=start, count=10)
|
||||
|
||||
@@ -27,9 +27,7 @@ class TestTrader:
|
||||
assert res.retcode == 10009
|
||||
|
||||
async def test_create_order_with_sl(self):
|
||||
sl = (
|
||||
self.trader.symbol.trade_stops_level * 2 + self.trader.symbol.spread
|
||||
) * self.trader.symbol.point
|
||||
sl = (self.trader.symbol.trade_stops_level * 2 + self.trader.symbol.spread) * self.trader.symbol.point
|
||||
tick = await self.trader.symbol.info_tick()
|
||||
sl = tick.bid + sl
|
||||
await self.trader.create_order_with_sl(order_type=OrderType.SELL, sl=sl)
|
||||
@@ -44,9 +42,7 @@ class TestTrader:
|
||||
|
||||
async def test_create_order_with_points(self):
|
||||
points = self.trader.symbol.trade_stops_level * 2 + self.trader.symbol.spread
|
||||
await self.trader.create_order_with_points(
|
||||
order_type=OrderType.BUY, points=points
|
||||
)
|
||||
await self.trader.create_order_with_points(order_type=OrderType.BUY, points=points)
|
||||
res = await self.trader.order.send()
|
||||
profit = floor(await self.trader.order.calc_profit())
|
||||
loss = -floor(abs(await self.trader.order.calc_loss()))
|
||||
@@ -57,16 +53,12 @@ class TestTrader:
|
||||
assert res.retcode == 10009
|
||||
|
||||
async def test_create_order_with_stops(self):
|
||||
sl = (
|
||||
self.trader.symbol.trade_stops_level * 2 + self.trader.symbol.spread
|
||||
) * self.trader.symbol.point
|
||||
sl = (self.trader.symbol.trade_stops_level * 2 + self.trader.symbol.spread) * self.trader.symbol.point
|
||||
tp = sl * self.trader.ram.risk_to_reward
|
||||
tick = await self.trader.symbol.info_tick()
|
||||
sl = tick.ask - sl
|
||||
tp = tick.ask + tp
|
||||
await self.trader.create_order_with_stops(
|
||||
order_type=OrderType.BUY, sl=sl, tp=tp
|
||||
)
|
||||
await self.trader.create_order_with_stops(order_type=OrderType.BUY, sl=sl, tp=tp)
|
||||
res = await self.trader.order.send()
|
||||
profit = floor(await self.trader.order.calc_profit())
|
||||
loss = -floor(abs(await self.trader.order.calc_loss()))
|
||||
|
||||
Reference in New Issue
Block a user