This commit is contained in:
Ichinga Samuel
2024-09-25 05:53:46 +01:00
parent d80212c270
commit 14aae0cfef
3 changed files with 100 additions and 50 deletions
+94 -47
View File
@@ -10,6 +10,7 @@ from functools import cached_property
import pandas as pd import pandas as pd
import pytz import pytz
import numpy as np import numpy as np
from debugpy.common.timestamp import current
from pandas import DataFrame from pandas import DataFrame
from MetaTrader5 import (Tick, SymbolInfo, AccountInfo, TradeOrder, TradePosition, TradeDeal, from MetaTrader5 import (Tick, SymbolInfo, AccountInfo, TradeOrder, TradePosition, TradeDeal,
TradeRequest, OrderCheckResult, OrderSendResult, TerminalInfo) TradeRequest, OrderCheckResult, OrderSendResult, TerminalInfo)
@@ -17,6 +18,7 @@ from MetaTrader5 import (Tick, SymbolInfo, AccountInfo, TradeOrder, TradePositio
from ...core.meta_trader import MetaTrader from ...core.meta_trader import MetaTrader
from ...core.constants import TimeFrame, CopyTicks, OrderType, TradeAction, AccountStopOutMode from ...core.constants import TimeFrame, CopyTicks, OrderType, TradeAction, AccountStopOutMode
from ...core.config import Config from ...core.config import Config
from ...lib.strategies.finger_trap import logger
from ...utils import round_down, round_up, error_handler, error_handler_sync, async_cache from ...utils import round_down, round_up, error_handler, error_handler_sync, async_cache
from .get_data import Data, GetData from .get_data import Data, GetData
@@ -123,12 +125,15 @@ class TestData:
print(err) print(err)
@async_cache @async_cache
async def get_price_tick(self, symbol: str, time: int) -> Tick | None: async def get_price_tick(self, *, symbol: str, time: int) -> Tick | None:
if self.config.use_terminal_for_backtesting: try:
tick = await self.mt5.copy_ticks_from(symbol, time, 1, CopyTicks.ALL) if self.config.use_terminal_for_backtesting:
return Tick(tick[-1]) if tick else None tick = await self.mt5.copy_ticks_from(symbol, time, 1, CopyTicks.ALL)
tick = self.prices[symbol].loc[self.cursor.time] return Tick(tick[-1]) if tick else None
return Tick(tick) tick = self.prices[symbol].loc[self.cursor.time]
return Tick(tick)
except Exception as exe:
logger.error(f"Error Getting Price Tick: {exe}")
@error_handler @error_handler
async def check_order(self, ticket: int): async def check_order(self, ticket: int):
@@ -157,14 +162,18 @@ class TestData:
profit = await self.order_calc_profit(order_type, symbol, volume, price_open, price_current, use_terminal) profit = await self.order_calc_profit(order_type, symbol, volume, price_open, price_current, use_terminal)
self.positions.update(ticket=pos.ticket, profit=profit, price_current=price_current, time_update=self.cursor.time) self.positions.update(ticket=pos.ticket, profit=profit, price_current=price_current, time_update=self.cursor.time)
def close_position(self, ticket: int): @error_handler(response=False)
def close_position(self, *, ticket: int) -> bool:
position = self.positions.pop(ticket) position = self.positions.pop(ticket)
margin = self.margins.pop(position.ticket) margin = self.margins.pop(position.ticket)
del self.orders[ticket] del self.orders[ticket]
del self.positions[ticket]
self.orders.update(ticket=ticket, time_done=self.cursor.time) self.orders.update(ticket=ticket, time_done=self.cursor.time)
self.update_account(gain=position.profit, margin=-margin) # ToDo: Create a deal object here? modify update account self.update_account(gain=position.profit, margin=-margin)
return True
def modify_stops(self, ticket: int, sl: int = None, tp: int = None): @error_handler(response=False)
def modify_stops(self, *, ticket: int, sl: int = None, tp: int = None) -> bool:
pos = self.positions[ticket] pos = self.positions[ticket]
order = self.orders[ticket] order = self.orders[ticket]
sl = sl or pos.sl sl = sl or pos.sl
@@ -172,7 +181,8 @@ class TestData:
self.positions.update(ticket=ticket, sl=sl, tp=tp, time_update=self.cursor.time) self.positions.update(ticket=ticket, sl=sl, tp=tp, time_update=self.cursor.time)
sl = sl or order.sl sl = sl or order.sl
tp = tp or order.tp tp = tp or order.tp
self.order.update(ticket=ticket, sl=sl, tp=tp, time_update=self.cursor.time) self.orders.update(ticket=ticket, sl=sl, tp=tp, time_update=self.cursor.time)
return True
def update_account(self, *, profit: float = None, margin: float = 0, gain: float = 0): def update_account(self, *, profit: float = None, margin: float = 0, gain: float = 0):
self._account.balance += gain self._account.balance += gain
@@ -229,45 +239,76 @@ class TestData:
return symbols return symbols
@error_handler @error_handler
async def order_send(self, request: dict, use_terminal: bool = True) -> OrderSendResult: async def order_send(self, *, request: dict, use_terminal: bool = True) -> OrderSendResult:
print('sending orders') order_ticket = random.randint(100_000_000, 999_999_999)
ticket = random.randint(100_000_000, 999_999_999) deal_ticket = random.randint(100_000_000, 999_999_999)
osr = {'retcode': 10009, 'comment': 'Request completed', 'request': TradeRequest(request)}
if (position := request.get('position')) in self.open_positions:
pos = self.open_positions[position]
order_type = OrderType(request['type'])
pos_type = OrderType(pos.type)
if order_type.opposite == pos_type: # ToDo: is there another way to check if the order is a close order?
# close position
self.close_position(pos.ticket)
self.to_json(osr) # ToDo: remove later
return OrderSendResult(osr) # ToDo: Create a deal object here
action = request['action']
if action == TradeAction.SLTP:
self.modify_stops(position, request['sl'], request['tp'])
return OrderSendResult(osr)
if (action := request.get('action')) == TradeAction.DEAL: osr = {'retcode': 10013, 'comment': 'Invalid request',
ocr = await self.order_check(request, use_terminal=use_terminal) 'request': TradeRequest(request.get(k, (0 if k != 'comment' else '')) for k in
if ocr.retcode != 0: TradeRequest.__match_args__)}
osr.update({'comment': ocr.comment, 'retcode': ocr.retcode})
self.to_json(osr) # ToDo: remove later
return OrderSendResult(osr)
deal_ticket = random.randint(100_000_000, 999_999_999) trade_order = {'ticket': order_ticket, 'time_setup': self.cursor.time,
tick = self.get_symbol_info_tick(request['symbol']) 'time_setup_msc': self.cursor.time * 1000,
**{k: v for k, v in request.items() if k in TradeOrder.__match_args__}}
order_type, symbol, action, position_ticket = (request.get('type'), request.get('symbol', ''),
request.get('action'), request.get('position'))
order_type = OrderType(order_type)
current_position = self.positions.get(position_ticket)
# closing an order by an opposite order using a position ticket and Deal action
if action == TradeAction.DEAL and current_position and order_type.opposite == current_position.type:
res = self.close_position(current_position.ticket)
if res:
trade_order.update({'comment': 'Done', 'position_id': deal_ticket,
'position_by_id': current_position.ticket})
# ToDo: Create a deal object here?
# ToDo: Update trade order with more information?
order = TradeOrder((trade_order.get(k, 0) for k in TradeOrder.__match_args__))
self.orders[order.ticket] = order
del self.orders[order.ticket]
osr.update({'comment': 'Request completed', 'retcode': 10009,
'order': order_ticket, 'deal': deal_ticket,})
# ToDo: remove later
self.to_json(osr)
return OrderSendResult((osr.get(k, 0) for k in OrderSendResult.__match_args__))
if action == TradeAction.SLTP and current_position:
check = await self.order_check(position_ticket)
if check.retcode != 0:
osr = {'retcode': check.retcode, 'comment': check.comment, 'request': check.request}
return OrderSendResult((osr.get(k, 0) for k in OrderSendResult.__match_args__))
res = self.modify_stops(ticket=position_ticket, sl=request.get('sl'), tp=request.get('tp'))
if res:
# ToDo: Create a deal object here
osr.update({'comment': 'Request completed', 'retcode': 10009, 'order': order_ticket, 'deal': deal_ticket,})
self.to_json(osr) # ToDo: remove later
return OrderSendResult((osr.get(k, 0) for k in OrderSendResult.__match_args__))
if action == TradeAction.DEAL and order_type in (OrderType.BUY, OrderType.SELL):
check = await self.order_check(request=request)
if check.retcode != 0:
osr = {'retcode': check.retcode, 'comment': check.comment, 'request': check.request}
return OrderSendResult((osr.get(k, 0) for k in OrderSendResult.__match_args__))
self.to_json(osr) # ToDo: remove later
return OrderSendResult(osr)
order_type = request['type'] order_type = request['type']
price = tick.ask if request['type'] == OrderType.BUY else tick.bid price = tick.ask if request['type'] == OrderType.BUY else tick.bid
volume = request['volume'] volume = request['volume']
sl, tp = request.get('sl', 0), request.get('tp', 0) sl, tp = request.get('sl', 0), request.get('tp', 0)
symbol = request['symbol'] symbol = request['symbol']
pos = {'comment': 'open position', 'ticket': ticket, 'symbol': symbol, 'volume': volume, pos = {'comment': 'open position', 'ticket': order_ticket, 'symbol': symbol, 'volume': volume,
'price_open': price, 'price_current': price, 'type': order_type, 'profit': 0, 'price_open': price, 'price_current': price, 'type': order_type, 'profit': 0,
'sl': sl, 'tp': tp, 'time': tick.time, 'sl': sl, 'tp': tp, 'time': tick.time,
'time_msc': tick.time_msc} 'time_msc': tick.time_msc}
order = {'ticket': ticket, 'symbol': symbol, 'volume': volume, 'price': price, 'price_current': price, order = {'ticket': order_ticket, 'symbol': symbol, 'volume': volume, 'price': price, 'price_current': price,
'price_open': price, 'type': order_type, 'time_setup': tick.time, 'price_open': price, 'type': order_type, 'time_setup': tick.time,
'time_setup_msc': tick.time_msc, 'volume_current': volume, 'sl': sl, 'tp': tp, } 'time_setup_msc': tick.time_msc, 'volume_current': volume, 'sl': sl, 'tp': tp, }
@@ -278,10 +319,10 @@ class TestData:
self.open_orders[order.ticket] = order self.open_orders[order.ticket] = order
self.orders.setdefault(order.symbol, {})[order.ticket] = order self.orders.setdefault(order.symbol, {})[order.ticket] = order
self.positions.setdefault(pos.symbol, {})[pos.ticket] = pos self.positions.setdefault(pos.symbol, {})[pos.ticket] = pos
osr.update({'order': ticket, 'price': price, 'volume': volume, 'bid': tick.bid, osr.update({'order': order_ticket, 'price': price, 'volume': volume, 'bid': tick.bid,
'ask': tick.ask, 'deal': deal_ticket}) 'ask': tick.ask, 'deal': deal_ticket})
margin = await self.order_calc_margin(action, symbol, volume, price, use_terminal=use_terminal) margin = await self.order_calc_margin(action, symbol, volume, price, use_terminal=use_terminal)
self.margins[ticket] = margin self.margins[order_ticket] = margin
self.update_account(margin=margin) self.update_account(margin=margin)
self.to_json(osr) # ToDo: remove later self.to_json(osr) # ToDo: remove later
return OrderSendResult(osr) return OrderSendResult(osr)
@@ -290,7 +331,8 @@ class TestData:
async def order_check(self, request: dict) -> OrderCheckResult: async def order_check(self, request: dict) -> OrderCheckResult:
ocr = {'retcode': 10013, 'balance': 0, 'profit': 0, 'margin': 0, 'equity': 0, 'margin_free': 0, ocr = {'retcode': 10013, 'balance': 0, 'profit': 0, 'margin': 0, 'equity': 0, 'margin_free': 0,
'margin_level': 0, 'comment': 'Invalid request', 'margin_level': 0, 'comment': 'Invalid request',
'request': TradeRequest(request.get(k, (0 if k != 'comment' else 0)) for k in TradeRequest.__match_args__)} 'request': TradeRequest(request.get(k, (0 if k != 'comment' else '')) for k in
TradeRequest.__match_args__)}
action, symbol, volume = request.get('action'), request.get('symbol'), request.get('volume') action, symbol, volume = request.get('action'), request.get('symbol'), request.get('volume')
@@ -301,7 +343,7 @@ class TestData:
return OrderCheckResult((ocr.get(k, 0) for k in OrderCheckResult.__match_args__)) return OrderCheckResult((ocr.get(k, 0) for k in OrderCheckResult.__match_args__))
# check margin and confirm order can go through # check margin and confirm order can go through
if order_type in (OrderType.BUY, OrderType.SELL): if action == TradeAction.DEAL and order_type in (OrderType.BUY, OrderType.SELL):
margin = await self.order_calc_margin(action, symbol, volume, price) margin = await self.order_calc_margin(action, symbol, volume, price)
if margin is None: if margin is None:
return OrderCheckResult((ocr.get(k, 0) for k in OrderCheckResult.__match_args__)) return OrderCheckResult((ocr.get(k, 0) for k in OrderCheckResult.__match_args__))
@@ -321,14 +363,14 @@ class TestData:
return OrderCheckResult((ocr.get(k, 0) for k in OrderCheckResult.__match_args__)) return OrderCheckResult((ocr.get(k, 0) for k in OrderCheckResult.__match_args__))
# check if the stops level is valid # check if the stops level is valid
sym = await self.get_symbol_info(symbol) sym = await self.get_symbol_info(symbol=symbol)
sl, tp = request.get('sl', 0), request.get('tp', 0) sl, tp = request.get('sl', 0), request.get('tp', 0)
current_price = price current_price = price
if tp or sl: if tp or sl:
if action == TradeAction.SLTP: if action == TradeAction.SLTP:
pos = self.positions.get(request.get('position')) pos = self.positions.get(request.get('position'))
sym = await self.get_symbol_info(pos.symbol) sym = sym or await self.get_symbol_info(pos.symbol)
current_tick = await self.get_price_tick(sym, self.cursor.time) current_tick = sym or await self.get_price_tick(pos.symbol, self.cursor.time)
current_price = current_tick.bid if pos.type == OrderType.BUY else current_tick.ask current_price = current_tick.bid if pos.type == OrderType.BUY else current_tick.ask
min_sl = min(sl, tp) min_sl = min(sl, tp)
@@ -339,11 +381,16 @@ class TestData:
ocr['comment'] = 'Invalid stops' ocr['comment'] = 'Invalid stops'
return OrderCheckResult((ocr.get(k, 0) for k in OrderCheckResult.__match_args__)) return OrderCheckResult((ocr.get(k, 0) for k in OrderCheckResult.__match_args__))
elif action == TradeAction.SLTP:
ocr['comment'] = 'Done'
ocr['retcode'] = 0
return OrderCheckResult((ocr.get(k, 0) for k in OrderCheckResult.__match_args__))
if self.mt5.config.use_terminal_for_backtesting: if self.mt5.config.use_terminal_for_backtesting:
ocr_t = await self.mt5.order_check(request) ocr_t = await self.mt5.order_check(request)
if ocr_t.retcode in (10013, 10014): if ocr_t.retcode in (10013, 10014):
return ocr_t return ocr_t
else: elif action == TradeAction.DEAL and order_type in (OrderType.BUY, OrderType.SELL):
# check volume # check volume
if volume < sym.volume_min or volume > sym.volume_max: if volume < sym.volume_min or volume > sym.volume_max:
ocr['retcode'] = 10014 ocr['retcode'] = 10014
@@ -388,12 +435,12 @@ class TestData:
return AccountInfo(self._account.asdict().values()) return AccountInfo(self._account.asdict().values())
@error_handler @error_handler
async def get_symbol_info_tick(self, symbol: str) -> Tick | None: async def get_symbol_info_tick(self, *, symbol: str) -> Tick | None:
tick = await self.get_price_tick(symbol, self.cursor.time) tick = await self.get_price_tick(symbol, self.cursor.time)
return tick return tick
@error_handler @error_handler
async def get_symbol_info(self, symbol: str) -> SymbolInfo: async def get_symbol_info(self, *, symbol: str) -> SymbolInfo:
if self.config.use_terminal_for_backtesting: if self.config.use_terminal_for_backtesting:
info = await self.mt5.symbol_info(symbol) info = await self.mt5.symbol_info(symbol)
else: else:
+3 -1
View File
@@ -1,9 +1,11 @@
from typing import Generic
from MetaTrader5 import TradePosition, TradeOrder, TradeDeal from MetaTrader5 import TradePosition, TradeOrder, TradeDeal
from aiomql.utils import logger from aiomql.utils import logger
class TradingData: class TradingData[Generic]:
_data: dict[int, TradePosition | TradeOrder | TradeDeal] _data: dict[int, TradePosition | TradeOrder | TradeDeal]
_open_items: set[int] _open_items: set[int]
+3 -2
View File
@@ -46,9 +46,9 @@ def backoff_decorator(func=None, *, max_retries: int = 5, retries: int = 0, erro
return wrapper return wrapper
def error_handler(func=None, *, msg='', exe = Exception): def error_handler(func=None, *, msg='', exe = Exception, response=None):
if func is None: if func is None:
return partial(error_handler, msg=msg, exe=exe) return partial(error_handler, msg=msg, exe=exe, response=response)
@wraps(func) @wraps(func)
async def wrapper(*args, **kwargs): async def wrapper(*args, **kwargs):
@@ -57,6 +57,7 @@ def error_handler(func=None, *, msg='', exe = Exception):
return res return res
except exe as err: except exe as err:
logger.error(f'Error in {func.__name__}: {msg or err}') logger.error(f'Error in {func.__name__}: {msg or err}')
return response
return wrapper return wrapper