This commit is contained in:
Ichinga Samuel
2024-11-11 05:56:08 +01:00
parent 110c7c39c7
commit 72e902c4c5
58 changed files with 569 additions and 2079 deletions
+1
View File
@@ -43,6 +43,7 @@ htmlcov/
.coverage
.coverage.*
.cache
.ruff_cache
.pytest_cache
nosetests.xml
coverage.xml
-20
View File
@@ -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())
+71
View File
@@ -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
+6 -5
View File
@@ -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 |
-2
View File
@@ -1,4 +1,2 @@
line-length = 150
target-version = "py311"
[format]
skip-magic-trailing-comma=true
+3 -11
View File
@@ -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
View File
@@ -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
View File
@@ -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):
+2 -12
View File
@@ -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]
+11 -41
View File
@@ -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__}")
+15 -60
View File
@@ -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")
+2 -6
View File
@@ -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)
+2 -6
View File
@@ -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}")
+2 -6
View File
@@ -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}")
+3 -15
View File
@@ -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
+12 -54
View File
@@ -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()
+14 -56
View File
@@ -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
View File
@@ -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)
+4 -21
View File
@@ -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)
+35 -174
View File
@@ -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
View File
@@ -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
+1 -4
View File
@@ -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
+8 -26
View File
@@ -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()
+1
View File
@@ -15,6 +15,7 @@ class Account(_Base, AccountInfo):
Attributes:
connected (bool): Status of connection to MetaTrader 5 Terminal
"""
_instance: Self
connected: bool
+11 -40
View File
@@ -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
View File
@@ -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
View File
@@ -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())
+9 -34
View File
@@ -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
View File
@@ -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
View File
@@ -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__}
+5 -22
View File
@@ -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
View File
@@ -1,4 +1,5 @@
"""Risk Assessment and Management"""
from .account import Account
from .positions import Positions
+3 -9
View File
@@ -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
View File
@@ -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()
+4 -9
View File
@@ -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
View File
@@ -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:
+2 -3
View File
@@ -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
View File
@@ -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):
+4 -18
View File
@@ -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
View File
@@ -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)
+4 -15
View File
@@ -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")
+6 -24
View File
@@ -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
+1 -5
View File
@@ -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())
+2 -8
View File
@@ -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())
+2 -8
View File
@@ -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)
+1 -4
View File
@@ -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]
+1 -4
View File
@@ -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]
+8 -31
View File
@@ -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())
+24 -93
View File
@@ -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):
+2 -6
View File
@@ -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)
+2 -10
View File
@@ -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
+4 -12
View File
@@ -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):
+1 -3
View File
@@ -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()
+1 -3
View File
@@ -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")
+3 -9
View File
@@ -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)
+4 -12
View File
@@ -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()))