Python-MetaTrader 5 Strategy Tester (Part 03): MT5-Like Trading Operations — Handling and Managing
Contents
- Introduction
- The order_send method
- Trade validation class
- All validators inside order_send
- The CTrade class inside a simulator
- Performing trading actions in a simulator
- Managing orders and positions in a simulator
- Conclusion
Introduction
In the previous article, we implemented similar syntax and functions to those offered by the Python-MetaTrader module in our simulator. With similar orders, deals, positions, and structures. In this post, we will implement a very close to MetaTrader 5 approach of handling such structures (trading operations).

The order_send Method
All trading actions for MetaTrader 5, such as placing pending orders, opening buy or sell positions, modifying orders, and deleting them, are results obtained after calling a single boilerplate function.
In MQL5 that function is called OrderSend, in Python-MetaTrader 5 it is called order_send.
According to the documentation.
The method order_send sends a request to perform a trading operation from the terminal to the trade server. It is similar to OrderSend.
order_send(
request // request structure
); It takes a single parameter called request. The MqlTradeRequest type structure describes a required trading action.
The following table represents the fields the "request structure" must hold.
| Field | Description |
|---|---|
| action | Trading operation type. The value can be one of the values of the TRADE_REQUEST_ACTIONS enumeration |
| magic | EA ID. Allows arranging the analytical handling of trading orders. Each EA can set a unique ID when sending a trading request |
| order | Order ticket. Required for modifying pending orders |
| symbol | The name of the trading instrument, for which the order is placed. Not required when modifying orders and closing positions |
| volume | Requested volume of a deal in lots. A real volume when making a deal depends on an order execution type. |
| price | Price at which an order should be executed. The price is not set in case of market orders for instruments of the "Market Execution" (SYMBOL_TRADE_EXECUTION_MARKET) type having the TRADE_ACTION_DEAL type |
| stoplimit | A price pending limit order is set when the price reaches the 'price' value (this condition is mandatory). The pending order is not passed to the trading system until that moment |
| sl | A price at which a stop loss order is activated when the price moves in an unfavorable direction |
| tp | A price at which a take profit order is activated when the price moves in a favorable direction |
| deviation | Maximum acceptable deviation from the requested price, specified in points |
| type | Order type. The value can be one of the values of the ORDER_TYPE enumeration |
| type_filling | Order filling type. The value can be one of the ORDER_TYPE_FILLING values |
| type_time | Order type by expiration. The value can be one of the ORDER_TYPE_TIME values. |
| expiration | Pending order expiration time (for TIME_SPECIFIED type orders) |
| comment | Comment to an order |
| position | Position ticket. Fill it when changing and closing a position for its clear identification. Usually, it is the same as the ticket of the order that opened the position. |
| position_by | Opposite position ticket. It is used when closing a position by opening a position in the opposite direction (at the same symbol). |
We need a similar function in our class.
def order_send(self, request: dict): """ Sends a request to perform a trading operation from the terminal to the trade server. The function is similar to OrderSend in MQL5. """ if not self.IS_TESTER: result = self.mt5_instance.order_send(request) if result is None or result.retcode != self.mt5_instance.TRADE_RETCODE_DONE: self.__GetLogger().warning(f"MT5 failed: {self.mt5_instance.last_error()}") return None return result
In the previous article, we introduced the strategy tester mode to the simulator class, i.e., when the variable IS_TESTER = True. When not in this mode, the simulator relies on information from the MetaTrader 5 client, opens and manages all trading operations there.
The above code snippet sends an order request to MetaTrader 5 when a user is not in tester mode.
Otherwise, we extract the request's credentials.
action = request.get("action") order_type = request.get("type") symbol = request.get("symbol") volume = float(request.get("volume", 0)) price = float(request.get("price", 0)) sl = float(request.get("sl", 0)) tp = float(request.get("tp", 0)) ticket = int(request.get("ticket", -1)) ticks_info = self.tick_cache[symbol] now = utils.ensure_utc(ticks_info.time) ts = int(now.timestamp()) msc = int(now.timestamp() * 1000)
We have to handle all operations manually inside this function, including writing (opening & closing of positions), deals to a container (basically doing MetaTrader 5's work).
I: Placing Pending Orders
In a simulator, a pending order is nothing but information about an order stored in a temporary orders container array (self.__orders_container__).
if action == self.mt5_instance.TRADE_ACTION_PENDING: order_ticket = self.__generate_order_ticket() order = self.TradeOrder( ticket=order_ticket, time_setup=ts, time_setup_msc=msc, time_done=0, time_done_msc=0, time_expiration=request.get("expiration", 0), type=order_type, type_time=request.get("type_time", 0), type_filling=request.get("type_filling", 0), state=self.mt5_instance.ORDER_STATE_PLACED, magic=request.get("magic", 0), position_id=0, position_by_id=0, reason=self.mt5_instance.DEAL_REASON_EXPERT, volume_initial=volume, volume_current=volume, price_open=price, sl=sl, tp=tp, price_current=price, price_stoplimit=request.get("price_stoplimit", 0), symbol=symbol, comment=request.get("comment", ""), external_id="", )
After creating an order, we add it to the orders container array and log it to the orders history array (container) as well.
self.__orders_container__.append(order) self.__orders_history_container__.append(order) return { "retcode": self.mt5_instance.TRADE_RETCODE_DONE, "order": order_ticket, }
II: Opening Positions
In MetaTrader 5, Positions are contracts, bought or sold on a financial instrument. A long position (long) is formed as a result of buying anticipating a price increase; a short position (Short) is the result of the sale of an asset, in anticipation of a future price decrease.
In a simulator, a position is a bunch of "position-like" information stored in a container.
if action == self.mt5_instance.TRADE_ACTION_DEAL: position_ticket = self.__generate_position_ticket() order_ticket = self.__generate_order_ticket() deal_ticket = self.__generate_deal_ticket() position = self.TradePosition( ticket=position_ticket, time=ts, time_msc=msc, time_update=ts, time_update_msc=msc, type=order_type, magic=request.get("magic", 0), identifier=position_ticket, reason=self.mt5_instance.DEAL_REASON_EXPERT, volume=volume, price_open=price, sl=sl, tp=tp, price_current=price, swap=0, profit=0, symbol=symbol, comment=request.get("comment", ""), external_id="", ) self.__positions_container__.append(position)
The processes of opening and closing positions have the action TRADE_ACTION_DEAL. The result of such an operation can be termed as a deal, so we have to log such a record into a deals container.
self.__deals_history_container__.append( self.TradeDeal( ticket=deal_ticket, order=order_ticket, time=ts, time_msc=msc, type=order_type, entry=self.mt5_instance.DEAL_ENTRY_IN, magic=request.get("magic", 0), position_id=position_ticket, reason=self.mt5_instance.DEAL_REASON_EXPERT, volume=volume, price=price, commission=self.__calc_commission(), swap=0, profit=0, fee=0, symbol=symbol, comment=request.get("comment", ""), external_id="", ) ) return { "retcode": self.mt5_instance.TRADE_RETCODE_DONE, "deal": deal_ticket, "order": order_ticket, "position": position_ticket, }
A deal is simply a record (history) of the opening and closing of positions in the MetaTrader 5 terminal.

III: Closing Positions
The request for closing a position is very similar to the one for opening a new one. They are both deals, but with different entries.
To detect a request for closing a position, we check if it has a position key in its request (a ticket of an existing position).
if action == self.mt5_instance.TRADE_ACTION_DEAL: # ---------- CLOSE POSITION ---------- ticket = request.get("position", -1) if ticket != -1: pos = next( (p for p in self.__positions_container__ if p.ticket == ticket), None, ) if not pos: return {"retcode": self.mt5_instance.TRADE_RETCODE_INVALID} self.__positions_container__.remove(pos) deal_ticket = self.__generate_deal_ticket() self.__deals_history_container__.append( self.TradeDeal( ticket=deal_ticket, order=0, time=ts, time_msc=msc, type=order_type, entry=self.mt5_instance.DEAL_ENTRY_OUT, magic=request.get("magic", 0), position_id=pos.ticket, reason=self.mt5_instance.DEAL_REASON_EXPERT, volume=volume, price=price, commission=self.__calc_commission(), swap=0, profit=0, fee=0, symbol=symbol, comment=request.get("comment", ""), external_id="", ) ) return { "retcode": self.mt5_instance.TRADE_RETCODE_DONE, "deal": deal_ticket, }
However, we cannot accept every request for closing a position blindly; we have to validate them.
For now, two crucial details we have to check before writing a deal and removing a position from the container include.
- Checking if a request has a valid price, In MetaTrader 5, buy positions are closed at the bid price while sell positions are closed at the ask price.
- We check if a given order type in the request (position type) is the opposite of an existing order, i.e., if a request is sent for an existing buy order ORDER_TYPE_BUY, it should be eligible for closing when given ORDER_TYPE_SELL.
if action == self.mt5_instance.TRADE_ACTION_DEAL: # ---------- CLOSE POSITION ---------- ticket = request.get("position", -1) if ticket != -1: pos = next( (p for p in self.__positions_container__ if p.ticket == ticket), None, ) if not pos: return {"retcode": self.mt5_instance.TRADE_RETCODE_INVALID} # validate position close request if pos.type == order_type: self.__GetLogger().critical("Failed to close an order. Order type must be the opposite") return None if order_type == self.mt5_instance.ORDER_TYPE_BUY: # For a sell order/position if not TradeValidators.price_equal(a=price, b=ticks_info.ask, eps=pow(10, -symbol_info.digits)): self.__GetLogger().critical(f"Failed to close ORDER_TYPE_SELL. Price {price} is not equal to bid {ticks_info.bid}") return None elif order_type == self.mt5_instance.ORDER_TYPE_SELL: # For a buy order/position if not TradeValidators.price_equal(a=price, b=ticks_info.bid, eps=pow(10, -symbol_info.digits)): self.__GetLogger().critical(f"Failed to close ORDER_TYPE_BUY. Price {price} is not equal to bid {ticks_info.bid}") return None self.__positions_container__.remove(pos) deal_ticket = self.__generate_deal_ticket() self.__deals_history_container__.append( self.TradeDeal( ticket=deal_ticket, order=0, time=ts, time_msc=msc, type=order_type, entry=self.mt5_instance.DEAL_ENTRY_OUT, magic=request.get("magic", 0), position_id=pos.ticket, reason=self.mt5_instance.DEAL_REASON_EXPERT, volume=volume, price=price, commission=self.__calc_commission(), swap=0, profit=0, fee=0, symbol=symbol, comment=request.get("comment", ""), external_id="", ) ) return { "retcode": self.mt5_instance.TRADE_RETCODE_DONE, "deal": deal_ticket, }
IV: Modifying Positions
To modify a position, we focus on two details only. Stop loss and take profit values.
elif action == self.mt5_instance.TRADE_ACTION_SLTP: ticket = request.get("position", -1) pos = next((p for p in self.__positions_container__ if p.ticket == ticket), None) if not pos: return {"retcode": self.mt5_instance.TRADE_RETCODE_INVALID} # --- Correct reference prices --- entry_price = pos.price_open market_price = ticks_info.bid if pos.type == self.mt5_instance.POSITION_TYPE_BUY else ticks_info.ask # --- Validate SL / TP relative to ENTRY --- if sl > 0: if not trade_validators.is_valid_sl(entry=entry_price, sl=sl, order_type=pos.type): return None if tp > 0: if not trade_validators.is_valid_tp(entry=entry_price, tp=tp, order_type=pos.type): return None # --- Validate freeze level against MARKET --- if sl > 0: if not trade_validators.is_valid_freeze_level(entry=market_price, stop_price=sl, order_type=pos.type): return None if tp > 0: if not trade_validators.is_valid_freeze_level(entry=market_price, stop_price=tp, order_type=pos.type): return None # --- APPLY MODIFICATION --- idx = self.__positions_container__.index(pos) updated_pos = pos._replace( sl=sl, tp=tp, time_update=ts, time_update_msc=msc ) self.__positions_container__[idx] = updated_pos return {"retcode": self.mt5_instance.TRADE_RETCODE_DONE}
V: Deleting Pending Orders
This is a straightforward process of removing an order from its container array. No validations required.
if action == self.mt5_instance.TRADE_ACTION_REMOVE: ticket = request.get("order", -1) self.__orders_container__ = [ o for o in self.__orders_container__ if o.ticket != ticket ] return {"retcode": self.mt5_instance.TRADE_RETCODE_DONE}
VI: Modifying Pending Orders
To modify a pending order, we focus on five crucial details. Order's opening price, stop loss, take profit, time expiration, and stop limit.
elif action == self.mt5_instance.TRADE_ACTION_SLTP: ticket = request.get("position", -1) pos = next((p for p in self.__positions_container__ if p.ticket == ticket), None) if not pos: return {"retcode": self.mt5_instance.TRADE_RETCODE_INVALID} # --- Correct reference prices --- entry_price = pos.price_open market_price = ticks_info.bid if pos.type == self.mt5_instance.POSITION_TYPE_BUY else ticks_info.ask # --- Validate SL / TP relative to ENTRY --- if sl > 0: if not trade_validators.is_valid_sl(entry=entry_price, sl=sl, order_type=pos.type): return None if tp > 0: if not trade_validators.is_valid_tp(entry=entry_price, tp=tp, order_type=pos.type): return None # --- Validate freeze level against MARKET --- if sl > 0: if not trade_validators.is_valid_freeze_level(entry=market_price, stop_price=sl, order_type=pos.type): return None if tp > 0: if not trade_validators.is_valid_freeze_level(entry=market_price, stop_price=tp, order_type=pos.type): return None # --- APPLY MODIFICATION --- idx = self.__positions_container__.index(pos) updated_pos = pos._replace( sl=sl, tp=tp, time_update=ts, time_update_msc=msc ) self.__positions_container__[idx] = updated_pos return {"retcode": self.mt5_instance.TRADE_RETCODE_DONE}
But, all these actions are wrong without a core method(s) for validating the request's credentials in the first place.
In the first simulator we implemented in this article series, we had an unorganized way of validating orders' credentials. This time, we improve it by implementing a separate class for the task.
The Trade Validation Class
As we know, MetaTrader 5 does not accept all requests passed. It checks for invalid credentials and throws an error, rejecting such orders when that happens.
These credentials are validated according to a given instrument specification, an account type, broker needs, and sometimes MetaTrader 5 client limits.
I: Checking for the right Lot Size
For a lot size to be accepted by MetaTrader 5:
- it has to be greater than the minimum allowed lot size (volume) for a particular instrument (symbol)
- It has to be smaller than the maximum allowed lot size for a particular instrument
- It has to be a multiple of the step size volume
Inside validators.py
class TradeValidators: def __init__(self, symbol_info: namedtuple, ticks_info: any, logger: any, mt5_instance: mt5=mt5): self.symbol_info = symbol_info self.ticks_info = ticks_info self.logger = logger self.mt5_instance = mt5_instance def is_valid_lotsize(self, lotsize: float) -> bool: # Validate lotsize if lotsize < self.symbol_info.volume_min: # check if the received lotsize is smaller than minimum accepted lot of a symbol self.logger.info(f"Trade validation failed: lotsize ({lotsize}) is less than minimum allowed ({self.symbol_info.volume_min})") return False if lotsize > self.symbol_info.volume_max: # check if the received lotsize is greater than the maximum accepted lot self.logger.info(f"Trade validation failed: lotsize ({lotsize}) is greater than maximum allowed ({self.symbol_info.volume_max})") return False step_count = lotsize / self.symbol_info.volume_step if abs(step_count - round(step_count)) > 1e-7: # check if the stoploss is a multiple of the step size self.logger.info(f"Trade validation failed: lotsize ({lotsize}) must be a multiple of step size ({self.symbol_info.volume_step})") return False return True
II: Ensuring There is Enough Money for a New Position
The MetaTrader 5 terminal checks if there is enough free margin on the account to accommodate a new position.
Below is a similar function for the task.
def is_there_enough_money(self, margin_required: float, free_margin: float) -> bool: if margin_required < 0: self.logger.info("Trade validation failed: Cannot calculate margin requirements") return False # Check free margin if margin_required > free_margin: self.logger.info(f'Trade validation failed: Not enough money to open trade. ' f'Required: {margin_required:.2f}, ' f'Free margin: {free_margin:.2f}') return False return True
III: Checking to Ensure A Valid Entry is Given
For a buy position, its price must be equal to the ask price, and for a sell position, its price must be equal to the bid price. This check is for positions only.
def is_valid_entry(self, price: float, order_type: int) -> bool: eps = pow(10, -self.symbol_info.digits) if order_type == self.mt5_instance.ORDER_TYPE_BUY: # BUY if not self.price_equal(a=price, b=self.ticks_info.ask, eps=eps): self.logger.info(f"Trade validation failed: Buy price {price} != ask {self.ticks_info.ask}") return False elif order_type == self.mt5_instance.ORDER_TYPE_SELL: # SELL if not self.price_equal(a=price, b=self.ticks_info.bid, eps=eps): self.logger.info(f"Trade validation failed: Sell price {price} != bid {self.ticks_info.bid}") return False else: self.logger.error("Unknown MetaTrader 5 position type") return False return True
IV: Ensuring Stop Loss and Take Profit Values Aren't Too Close to the Market
All symbols come with a small threshold value indicating the minimum distance where stop loss and take profit values must be placed from the market.
This threshold value is called SYMBOL_TRADE_STOPS_LEVEL.
def is_valid_stops_level(self, entry: float, stop_price: float, stops_type: str='') -> bool: point = self.symbol_info.point stop_level = self.symbol_info.trade_stops_level * point distance = abs(entry-stop_price) if stop_price <= 0: return True if distance < stop_level: self.logger.info(f"{'Either SL or TP' if stops_type=='' else stops_type} is too close to the market. Min allowed distance = {stop_level}") return False return True
V: Checking For Valid Stop Loss and Take Profit Values
For a buy order, a stop loss must be below the entry price, while the takeprofit must be above the entry price.
For a sell order, a take profit value must be below the entry price, while the stop loss must be above the entry price.
def is_valid_sl(self, entry: float, sl: float, order_type: int) -> bool: if not self.is_valid_stops_level(entry, sl, "Stoploss"): # check for stops levels return False if sl > 0: if order_type in self.BUY_ACTIONS: # buy action if sl >= entry: self.logger.info(f"Trade validation failed: Buy-based order's stop loss ({sl}) must be below order opening price ({entry})") return False elif order_type in self.SELL_ACTIONS: # sell action if sl <= entry: self.logger.info(f"Trade validation failed: Sell-based order's stop loss ({sl}) must be above order opening price ({entry})") return False else: self.logger.error("Unknown MetaTrader 5 order type") return False return True def is_valid_tp(self, entry: float, tp: float, order_type: int) -> bool: if not self.is_valid_stops_level(entry, tp, "Takeprofit"): # check for stops and freeze levels return False if tp > 0: if order_type in self.BUY_ACTIONS: # buy position if tp <= entry: self.logger.info(f"Trade validation failed: {self.ORDER_TYPES_MAP[order_type]} take profit ({tp}) must be above order opening price ({entry})") return False elif order_type in self.SELL_ACTIONS: # sell position if tp >= entry: self.logger.info(f"Trade validation failed: {self.ORDER_TYPES_MAP[order_type]} take profit ({tp}) must be below order opening price ({entry})") return False else: self.logger.error("Unknown MetaTrader 5 order type") return False return True
VI: A Check To Ensure Maximum Lot Size Isn't Reached for an Instrument
Some symbols in some brokers have a limit for the total lot size in individual opened orders and positions.
def is_symbol_volume_reached(self, symbol_volume: float, volume_limit: float) -> bool: """Checks if the maximum allowed volume is reached for a particular instrument Returns: bool: True if the condition is reached and False when it is not. """ if symbol_volume >= volume_limit and volume_limit > 0: self.logger.critical(f"Symbol Volume limit of {volume_limit} is reached!") return True return False
VII: A Check To Ensure the Maximum Number of orders isn't reached
Some accounts tend to have a limit in the number of pending orders that can be opened at a time. We have to check for that to ensure that we don't violate a simulated account, just like the MetaTrader 5 platform wouldn't let us violate a real one.
def is_max_orders_reached(self, open_orders: int, ac_limit_orders: int) -> bool: """Checks whether the maximum number of orders for the account is reached Args: open_orders (int): The number of opened orders ac_limit_orders (int): Maximum number of orders allowed for the account Returns: bool: True if the threshold is reached, otherwise, it returns false. """ if open_orders >= ac_limit_orders and ac_limit_orders > 0: self.logger.critical(f"Pending Orders limit of {ac_limit_orders} is reached!") return True return False
VIII: Checking for the Freeze Level
| Type of order/position | Activation price | Check |
|---|---|---|
| Buy Limit order | Ask | Ask-OpenPrice >= SYMBOL_TRADE_FREEZE_LEVEL |
| Buy Stop order | Ask | OpenPrice-Ask >= SYMBOL_TRADE_FREEZE_LEVEL |
| Sell Limit order | Bid | OpenPrice-Bid >= SYMBOL_TRADE_FREEZE_LEVEL |
| Sell Stop order | Bid | Bid-OpenPrice >= SYMBOL_TRADE_FREEZE_LEVEL |
| Buy position | Bid | TakeProfit-Bid >= SYMBOL_TRADE_FREEZE_LEVEL Bid-StopLoss >= SYMBOL_TRADE_FREEZE_LEVEL |
| Sell position | Ask | Ask-TakeProfit >= SYMBOL_TRADE_FREEZE_LEVEL StopLoss-Ask >= SYMBOL_TRADE_FREEZE_LEVEL |
def is_valid_freeze_level(self, entry: float, stop_price: float, order_type: int) -> bool: """ Check SYMBOL_TRADE_FREEZE_LEVEL for pending orders and open positions. """ freeze_level = self.symbol_info.trade_freeze_level if freeze_level <= 0: return True # No freeze restriction point = self.symbol_info.point freeze_distance = freeze_level * point bid = self.ticks_info.bid ask = self.ticks_info.ask def log_fail(msg: str, dist: float): self.logger.info( f"{msg} | distance={dist/point:.1f} pts < " f"freeze_level={freeze_level} pts" ) # ---------------- Pending Orders ---------------- if order_type == self.mt5_instance.ORDER_TYPE_BUY_LIMIT: dist = ask - entry if dist < freeze_distance: log_fail("BuyLimit cannot be modified: Ask - OpenPrice", dist) return False return True if order_type == self.mt5_instance.ORDER_TYPE_SELL_LIMIT: dist = entry - bid if dist < freeze_distance: log_fail("SellLimit cannot be modified: OpenPrice - Bid", dist) return False return True if order_type == self.mt5_instance.ORDER_TYPE_BUY_STOP: dist = entry - ask if dist < freeze_distance: log_fail("BuyStop cannot be modified: OpenPrice - Ask", dist) return False return True if order_type == self.mt5_instance.ORDER_TYPE_SELL_STOP: dist = bid - entry if dist < freeze_distance: log_fail("SellStop cannot be modified: Bid - OpenPrice", dist) return False return True # ---------------- Open Positions (SL / TP modification) ---------------- # Buy position if order_type == self.mt5_instance.ORDER_TYPE_BUY: if stop_price <= 0: return True if stop_price < entry: # StopLoss dist = bid - stop_price if dist < freeze_distance: log_fail("Buy position SL cannot be modified: Bid - SL", dist) return False else: # TakeProfit dist = stop_price - bid if dist < freeze_distance: log_fail("Buy position TP cannot be modified: TP - Bid", dist) return False return True # Sell position if order_type == self.mt5_instance.ORDER_TYPE_SELL: if stop_price <= 0: return True if stop_price > entry: # StopLoss dist = stop_price - ask if dist < freeze_distance: log_fail("Sell position SL cannot be modified: SL - Ask", dist) return False else: # TakeProfit dist = ask - stop_price if dist < freeze_distance: log_fail("Sell position TP cannot be modified: Ask - TP", dist) return False return True self.logger.error("Unknown MetaTrader 5 order type") return False
All Validators inside order_send (TL;DR)
With all these functions inside a class named TradeValidators within a file validators.py applied to the function order_send, below is how everything fits:
def order_send(self, request: dict): """ Sends a request to perform a trading operation from the terminal to the trade server. The function is similar to OrderSend in MQL5. """ # ----------------------------------------------------- if not self.IS_TESTER: result = self.mt5_instance.order_send(request) if result is None or result.retcode != self.mt5_instance.TRADE_RETCODE_DONE: self.__GetLogger().warning(f"MT5 failed: {self.mt5_instance.last_error()}") return None return result # -------------------- Extract request ----------------------------- action = request.get("action") order_type = request.get("type") symbol = request.get("symbol") volume = float(request.get("volume", 0)) price = float(request.get("price", 0)) sl = float(request.get("sl", 0)) tp = float(request.get("tp", 0)) ticket = int(request.get("ticket", -1)) ticks_info = self.tick_cache[symbol] symbol_info = self.symbol_info(symbol) ac_info = self.account_info() now = ticks_info.time ts = int(now) msc = int(now * 1000) if order_type not in self.ORDER_TYPES: return {"retcode": self.mt5_instance.TRADE_RETCODE_INVALID} trade_validators = TradeValidators(symbol_info=symbol_info, ticks_info=ticks_info, logger=self.__GetLogger(), mt5_instance=self.mt5_instance) # -------------------- REMOVE pending order ------------------------ if action == self.mt5_instance.TRADE_ACTION_REMOVE: ticket = request.get("order", -1) self.__orders_container__ = [ o for o in self.__orders_container__ if o.ticket != ticket ] return {"retcode": self.mt5_instance.TRADE_RETCODE_DONE} # --------------------- PENDING order -------------------------- if action == self.mt5_instance.TRADE_ACTION_PENDING: if trade_validators.is_max_orders_reached(open_orders=len(self.__orders_container__), ac_limit_orders=ac_info.limit_orders): return None if not trade_validators.is_valid_sl(entry=price, sl=sl, order_type=order_type) or not trade_validators.is_valid_tp(entry=price, tp=tp, order_type=order_type): return None total_volume = sum([pos.volume for pos in self.__positions_container__]) + sum([order.volume for order in self.__orders_container__]) if trade_validators.is_symbol_volume_reached(symbol_volume=total_volume, volume_limit=symbol_info.volume_limit): return None order_ticket = self.__generate_order_ticket() order = self.TradeOrder( ticket=order_ticket, time_setup=ts, time_setup_msc=msc, time_done=0, time_done_msc=0, time_expiration=request.get("expiration", 0), type=order_type, type_time=request.get("type_time", 0), type_filling=request.get("type_filling", 0), state=self.mt5_instance.ORDER_STATE_PLACED, magic=request.get("magic", 0), position_id=0, position_by_id=0, reason=self.mt5_instance.DEAL_REASON_EXPERT, volume_initial=volume, volume_current=volume, price_open=price, sl=sl, tp=tp, price_current=price, price_stoplimit=request.get("price_stoplimit", 0), symbol=symbol, comment=request.get("comment", ""), external_id="", ) self.__orders_container__.append(order) self.__orders_history_container__.append(order) return { "retcode": self.mt5_instance.TRADE_RETCODE_DONE, "order": order_ticket, } # ------------------ MARKET DEAL (open or close) ------------------ if action == self.mt5_instance.TRADE_ACTION_DEAL: # ---------- CLOSE POSITION ---------- ticket = request.get("position", -1) if ticket != -1: pos = next( (p for p in self.__positions_container__ if p.ticket == ticket), None, ) if not pos: return {"retcode": self.mt5_instance.TRADE_RETCODE_INVALID} # validate position close request if pos.type == order_type: self.__GetLogger().critical("Failed to close an order. Order type must be the opposite") return None if order_type == self.mt5_instance.ORDER_TYPE_BUY: # For a sell order/position if not TradeValidators.price_equal(a=price, b=ticks_info.ask, eps=pow(10, -symbol_info.digits)): self.__GetLogger().critical(f"Failed to close ORDER_TYPE_SELL. Price {price} is not equal to bid {ticks_info.bid}") return None elif order_type == self.mt5_instance.ORDER_TYPE_SELL: # For a buy order/position if not TradeValidators.price_equal(a=price, b=ticks_info.bid, eps=pow(10, -symbol_info.digits)): self.__GetLogger().critical(f"Failed to close ORDER_TYPE_BUY. Price {price} is not equal to bid {ticks_info.bid}") return None self.__positions_container__.remove(pos) deal_ticket = self.__generate_deal_ticket() self.__deals_history_container__.append( self.TradeDeal( ticket=deal_ticket, order=0, time=ts, time_msc=msc, type=order_type, entry=self.mt5_instance.DEAL_ENTRY_OUT, magic=request.get("magic", 0), position_id=pos.ticket, reason=self.mt5_instance.DEAL_REASON_EXPERT, volume=volume, price=price, commission=self.__calc_commission(), swap=0, profit=0, fee=0, symbol=symbol, comment=request.get("comment", ""), external_id="", ) ) return { "retcode": self.mt5_instance.TRADE_RETCODE_DONE, "deal": deal_ticket, } # ---------- OPEN POSITION ---------- # validate new stops if not trade_validators.is_valid_sl(entry=price, sl=sl, order_type=order_type): return None if not trade_validators.is_valid_tp(entry=price, tp=tp, order_type=order_type): return None # validate the lotsize if not trade_validators.is_valid_lotsize(lotsize=volume): return None total_volume = sum([pos.volume for pos in self.__positions_container__]) + sum([order.volume for order in self.__orders_container__]) if trade_validators.is_symbol_volume_reached(symbol_volume=total_volume, volume_limit=symbol_info.volume_limit): return None if not trade_validators.is_there_enough_money(margin_required=self.order_calc_margin(order_type=order_type, symbol=symbol, volume=volume, price=price), free_margin=ac_info.margin_free): return None position_ticket = self.__generate_position_ticket() order_ticket = self.__generate_order_ticket() deal_ticket = self.__generate_deal_ticket() position = self.TradePosition( ticket=position_ticket, time=ts, time_msc=msc, time_update=ts, time_update_msc=msc, type=order_type, magic=request.get("magic", 0), identifier=position_ticket, reason=self.mt5_instance.DEAL_REASON_EXPERT, volume=volume, price_open=price, sl=sl, tp=tp, price_current=price, swap=0, profit=0, symbol=symbol, comment=request.get("comment", ""), external_id="", ) self.__positions_container__.append(position) self.__deals_history_container__.append( self.TradeDeal( ticket=deal_ticket, order=order_ticket, time=ts, time_msc=msc, type=order_type, entry=self.mt5_instance.DEAL_ENTRY_IN, magic=request.get("magic", 0), position_id=position_ticket, reason=self.mt5_instance.DEAL_REASON_EXPERT, volume=volume, price=price, commission=self.__calc_commission(), swap=0, profit=0, fee=0, symbol=symbol, comment=request.get("comment", ""), external_id="", ) ) return { "retcode": self.mt5_instance.TRADE_RETCODE_DONE, "deal": deal_ticket, "order": order_ticket, "position": position_ticket, } elif action == self.mt5_instance.TRADE_ACTION_MODIFY: # Modifying pending orders ticket = request.get("order", -1) order = next( (o for o in self.__orders_container__ if o.ticket == ticket), None, ) if not order: return {"retcode": self.mt5_instance.TRADE_RETCODE_INVALID} # validate new stops if not trade_validators.is_valid_freeze_level(entry=price, stop_price=sl, order_type=order_type): return None if not trade_validators.is_valid_freeze_level(entry=price, stop_price=tp, order_type=order_type): return None # Modify ONLY allowed fields order.price_open = price order.sl = sl order.tp = tp order.time_expiration = request.get("expiration", order.time_expiration) order.price_stoplimit = request.get("price_stoplimit", order.price_stoplimit) return {"retcode": self.mt5_instance.TRADE_RETCODE_DONE} elif action == self.mt5_instance.TRADE_ACTION_SLTP: # Modifying an open position ticket = request.get("position", -1) pos = next( (p for p in self.__positions_container__ if p.ticket == ticket), None, ) if not pos: return {"retcode": self.mt5_instance.TRADE_RETCODE_INVALID} # Check for valid stoplosses and TPs ensuring they are not too close to the market if pos.type == self.mt5_instance.ORDER_TYPE_BUY: if not trade_validators.is_valid_sl(entry=ticks_info.bid, sl=sl, order_type=order_type) or not trade_validators.is_valid_tp(entry=ticks_info.bid, tp=tp, order_type=order_type): return None elif pos.type == self.mt5_instance.ORDER_TYPE_SELL: if not trade_validators.is_valid_sl(entry=ticks_info.ask, sl=sl, order_type=order_type) or not trade_validators.is_valid_tp(entry=ticks_info.ask, tp=tp, order_type=order_type): return None if not trade_validators.is_valid_freeze_level(entry=price, stop_price=sl, order_type=order_type): return None if not trade_validators.is_valid_freeze_level(entry=price, stop_price=sl, order_type=order_type): return None pos.sl = sl pos.tp = tp pos.time_update = ts pos.time_update_msc = msc return {"retcode": self.mt5_instance.TRADE_RETCODE_DONE} return { "retcode": self.mt5_instance.TRADE_RETCODE_INVALID, "comment": "Unsupported trade action", }
The CTrade Class Inside a Simulator
Just like MQL5, the Python-MetaTrader 5 module feels like a low-level module that lets us communicate with the MetaTrader 5 terminal. As we've just seen, it takes more than what's required to send a request for opening positions and orders (a long, tiresome process).
In MQL5, we have the so-called trade classes, which provide us with a simple interface for opening and managing the trades. In Python, we created a similar classes, let us adapt the CTrade class to fit our simulator needs.
import MetaTrader5 as mt5 from datetime import datetime, timezone import config class CTrade: def __init__(self, simulator, magic_number: int, filling_type_symbol: str, deviation_points: int): self.simulator = simulator self.mt5_instance = simulator.mt5_instance self.magic_number = magic_number self.deviation_points = deviation_points self.filling_type = self._get_type_filling(filling_type_symbol) if self.filling_type == -1: print("Failed to initialize the class, Invalid filling type. Check your symbol") return
We give our class a simulator instance for extracting some methods from it instead of directly from MetaTrader 5, which helps our class to use overridden methods discussed in the prior article.
The CTrade class comes with plenty of methods; they all rely on a method called order_send that comes with the Python-MetaTrader 5 module.
Throughout the class, instead of calling methods from the Python-MetaTrader 5 module, we call overridden methods from a Simulator class.
class CTrade: def __init__(self, simulator, magic_number: int, filling_type_symbol: str, deviation_points: int): self.simulator = simulator self.mt5_instance = simulator.mt5_instance self.magic_number = magic_number self.deviation_points = deviation_points self.filling_type = self._get_type_filling(filling_type_symbol) if self.filling_type == -1: print("Failed to initialize the class, Invalid filling type. Check your symbol") return def _get_type_filling(self, symbol): symbol_info = self.simulator.symbol_info(symbol) if symbol_info is None: print(f"Failed to get symbol info for {symbol}") filling_map = { 1: self.mt5_instance.ORDER_FILLING_FOK, 2: self.mt5_instance.ORDER_FILLING_IOC, 4: self.mt5_instance.ORDER_FILLING_BOC, 8: self.mt5_instance.ORDER_FILLING_RETURN } return filling_map.get(symbol_info.filling_mode, f"Unknown Filling type") def __GetLogger(self): if self.simulator.IS_TESTER: return config.tester_logger return config.simulator_logger def position_open(self, symbol: str, volume: float, order_type: int, price: float, sl: float=0.0, tp: float=0.0, comment: str="") -> bool: """ Open a market position (instant execution). Executes either a buy or sell order at the current market price. This is for immediate position opening, not pending orders. Args: symbol: Trading symbol (e.g., "EURUSD", "GBPUSD") volume: Trade volume in lots (e.g., 0.1 for micro lot) order_type: Trade direction (either ORDER_TYPE_BUY or ORDER_TYPE_SELL) price: Execution price. For market orders, this should be the current: - Ask price for BUY orders - Bid price for SELL orders sl: Stop loss price (set to 0.0 to disable) tp: Take profit price (set to 0.0 to disable) comment: Optional order comment (max 31 characters, will be truncated automatically) Returns: bool: True if position was opened successfully, False otherwise """ request = { "action": self.mt5_instance.TRADE_ACTION_DEAL, "symbol": symbol, "volume": volume, "type": order_type, "price": price, "deviation": self.deviation_points, "magic": self.magic_number, "comment": comment, "type_time": self.mt5_instance.ORDER_TIME_GTC, "type_filling": self.filling_type, } if sl > 0.0: request["sl"] = sl if tp > 0.0: request["tp"] = tp if self.simulator.order_send(request) is None: return False self.__GetLogger().info(f"Position Opened successfully!") return True def order_open(self, symbol: str, volume: float, order_type: int, price: float, sl: float = 0.0, tp: float = 0.0, type_time: int = mt5.ORDER_TIME_GTC, expiration: datetime = None, comment: str = "") -> bool: """ Opens a pending order with full control over order parameters. Args: symbol: Trading symbol (e.g., "EURUSD") volume: Order volume in lots order_type: Order type (ORDER_TYPE_BUY_LIMIT, ORDER_TYPE_SELL_STOP, etc.) price: Activation price for pending order sl: Stop loss price (0 to disable) tp: Take profit price (0 to disable) type_time: Order expiration type (default: ORDER_TIME_GTC). Possible values: - ORDER_TIME_GTC (Good-Til-Canceled) - ORDER_TIME_DAY (Good for current day) - ORDER_TIME_SPECIFIED (expires at specific datetime) - ORDER_TIME_SPECIFIED_DAY (expires at end of specified day) expiration: Expiration datetime (required for ORDER_TIME_SPECIFIED types) comment: Optional order comment (max 31 characters) Returns: bool: True if order was placed successfully, False otherwise """ # Validate expiration for time-specific orders if type_time in (self.mt5_instance.ORDER_TIME_SPECIFIED, self.mt5_instance.ORDER_TIME_SPECIFIED_DAY) and expiration is None: print(f"Expiration required for order type {type_time}") return False request = { "action": self.mt5_instance.TRADE_ACTION_PENDING, "symbol": symbol, "volume": volume, "type": order_type, "price": price, "sl": sl, "tp": tp, "deviation": self.deviation_points, "magic": self.magic_number, "comment": comment[:31], # MT5 comment max length is 31 chars "type_time": type_time, "type_filling": self.filling_type, } # Add expiration if required if type_time in (self.mt5_instance.ORDER_TIME_SPECIFIED, self.mt5_instance.ORDER_TIME_SPECIFIED_DAY) and expiration is not None: # Convert to broker's expected format (UTC timestamp in milliseconds) expiration_utc = expiration.astimezone(timezone.utc) if expiration.tzinfo else expiration.replace(tzinfo=timezone.utc) request["expiration"] = int(expiration_utc.timestamp() * 1000) # Send order if self.simulator.order_send(request) is None: return False self.__GetLogger().info(f"Order opened successfully!") return True def buy(self, volume: float, symbol: str, price: float, sl: float=0.0, tp: float=0.0, comment: str="") -> bool: """ Opens a buy (market) position. Args: volume: Trade volume (lot size) symbol: Trading symbol (e.g., "EURUSD") price: Execution price sl: Stop loss price (optional, default=0.0) tp: Take profit price (optional, default=0.0) comment: Position comment (optional, default="") Returns: bool: True if order was sent successfully, False otherwise """ return self.position_open(symbol=symbol, volume=volume, order_type=self.mt5_instance.ORDER_TYPE_BUY, price=price, sl=sl, tp=tp, comment=comment)
With this module adapted to fit our simulator needs, we now have an easy way of opening positions and pending orders. Before testing these methods, let us understand the changes made to our class and what should be done to run a simulator successfully.
In the previous article, we introduced a method called Start, which sets our simulator class instance to a strategy tester mode that makes everything and almost all data in the class virtual.
def Start(self, IS_TESTER: bool) -> bool: # simulator start self.IS_TESTER = IS_TESTER
With this mode chosen (set to True) the simulator imitates the MetaTrader 5 strategy tester behavior, set to False, a simulator is no longer as it relies on the MetaTrader 5 client directly for all the information and opens the trades there, this mode was introduced in the class for testing purposes, ensuring what is conducted in a simulator resembles what's carried out in the MetaTrader 5 client.
With new changes implemented, this method is removed. By default, the class is called in strategy tester mode (simulator). To get into the MetaTrader 5 mode (usually for debugging purposes) a user must pass an argument --mt5 when calling the final script.
(venv) C:\Users\Omega Joctan\OneDrive\Documents\PyMetaTester>python test.py --mt5
Performing Trading Actions in a Simulator
You must follow the steps below to test the current version of the simulator (files attached at the end of this article):
Inside test.py
01: Initialize the MetaTrader 5 terminal
import MetaTrader5 as mt5 from Trade.Trade import CTrade from datetime import datetime, timedelta import time import pytz from simulator import Simulator, CTrade if not mt5.initialize(): # Initialize MetaTrader5 instance print(f"Failed to Initialize MetaTrader5. Error = {mt5.last_error()}") mt5.shutdown() quit()
02: Calling the Simulator Instance
We call the simulator class instance, giving it the MetaTrader 5 app instance, the account's balance labelled as deposit, and the leverage value.
sim = Simulator(simulator_name="MySimulator", mt5_instance=mt5, deposit=1078.30, leverage="1:500")
We then use a simulator instance for the CTrade class.
03: (Optional) Instantiating the CTrade Class
symbol = "EURUSD"
timeframe = mt5.TIMEFRAME_H1 m_trade = CTrade(simulator=sim, magic_number=112233, filling_type_symbol=symbol, deviation_points=100)
04: We Assign Tick Information to the Simulator and Extract it Back
mt5_ticks = mt5.symbol_info_tick(symbol) # tick source sim.TickUpdate(symbol=symbol, tick=mt5_ticks) # very important tick_from_sim = sim.symbol_info_tick(symbol=symbol) # we get ticks back from a class ask = tick_from_sim.ask bid = tick_from_sim.bid
05: Finally, Some Trading Operations
symbol_info = sim.symbol_info(symbol=symbol) lotsize = symbol_info.volume_min m_trade.buy( volume=lotsize, symbol=symbol, price=ask, sl=ask - 100 * symbol_info.point, tp=ask + 150 * symbol_info.point, comment="Market Buy" ) m_trade.sell( volume=lotsize, symbol=symbol, price=bid, sl=bid + 100 * symbol_info.point, tp=bid - 150 * symbol_info.point, comment="Market Sell" ) buy_limit_price = ask - 200 * symbol_info.point m_trade.buy_limit( volume=lotsize, symbol=symbol, price=buy_limit_price, sl=buy_limit_price - 100 * symbol_info.point, tp=buy_limit_price + 200 * symbol_info.point, comment="Buy Limit" ) sell_limit_price = bid + 200 * symbol_info.point m_trade.sell_limit( volume=lotsize, symbol=symbol, price=sell_limit_price, sl=sell_limit_price + 100 * symbol_info.point, tp=sell_limit_price - 200 * symbol_info.point, comment="Sell Limit" ) buy_stop_price = ask + 150 * symbol_info.point m_trade.buy_stop( volume=lotsize, symbol=symbol, price=buy_stop_price, sl=buy_stop_price - 100 * symbol_info.point, tp=buy_stop_price + 300 * symbol_info.point, comment="Buy Stop" ) sell_stop_price = bid - 150 * symbol_info.point m_trade.sell_stop( volume=lotsize, symbol=symbol, price=sell_stop_price, sl=sell_stop_price + 100 * symbol_info.point, tp=sell_stop_price - 300 * symbol_info.point, comment="Sell Stop" )
We can check to see if these opened positions and orders exist in our simulator.
print(f"positions in a simulator = {sim.positions_total()}:\n",sim.positions_get()) print(f"orders in a simulator = {sim.orders_total()}:\n", sim.orders_get())
Outputs (Tester Mode):
(venv) C:\Users\Omega Joctan\OneDrive\Documents\PyMetaTester>python test.py
2026-01-05 15:09:24,504 | INFO | tester | [Trade.py:85 - position_open() ] => Position Opened successfully!
2026-01-05 15:09:24,513 | INFO | tester | [Trade.py:85 - position_open() ] => Position Opened successfully!
2026-01-05 15:09:24,515 | INFO | tester | [Trade.py:148 - order_open() ] => Order opened successfully!
2026-01-05 15:09:24,515 | INFO | tester | [Trade.py:148 - order_open() ] => Order opened successfully!
2026-01-05 15:09:24,515 | INFO | tester | [Trade.py:148 - order_open() ] => Order opened successfully!
2026-01-05 15:09:24,515 | INFO | tester | [Trade.py:148 - order_open() ] => Order opened successfully!
positions in a simulator = 2:
(TradePosition(ticket=113127357728313068862, time=1767622158, time_msc=1767622158000, time_update=1767622158, time_update_msc=1767622158000, type=0, magic=112233, identifier=113127357728313068862, reason=3, volume=0.01, price_open=1.16792, sl=1.1669200000000002, tp=1.1694200000000001, price_current=1.16792, swap=0, profit=0, symbol='EURUSD', comment='Market Buy', external_id=''),
TradePosition(ticket=113127357728890995262, time=1767622158, time_msc=1767622158000, time_update=1767622158, time_update_msc=1767622158000, type=1, magic=112233, identifier=113127357728890995262, reason=3, volume=0.01, price_open=1.16792, sl=1.16892, tp=1.16642, price_current=1.16792, swap=0, profit=0, symbol='EURUSD', comment='Market Sell', external_id=''))
orders in a simulator = 4:
(TradeOrder(ticket=113127357729019468800, time_setup=1767622158, time_setup_msc=1767622158000, time_done=0, time_done_msc=0, time_expiration=0, type=2, type_time=0, type_filling=1, state=1, magic=112233, position_id=0, position_by_id=0, reason=3, volume_initial=0.01, volume_current=0.01, price_open=1.16592, sl=1.1649200000000002, tp=1.16792, price_current=1.16592, price_stoplimit=0, symbol='EURUSD', comment='Buy Limit', external_id=''),
TradeOrder(ticket=113127357729019468835, time_setup=1767622158, time_setup_msc=1767622158000, time_done=0, time_done_msc=0, time_expiration=0, type=3, type_time=0, type_filling=1, state=1, magic=112233, position_id=0, position_by_id=0, reason=3, volume_initial=0.01, volume_current=0.01, price_open=1.16992, sl=1.17092, tp=1.16792, price_current=1.16992, price_stoplimit=0, symbol='EURUSD', comment='Sell Limit', external_id=''),
TradeOrder(ticket=113127357729019468836, time_setup=1767622158, time_setup_msc=1767622158000, time_done=0, time_done_msc=0, time_expiration=0, type=4, type_time=0, type_filling=1, state=1, magic=112233, position_id=0, position_by_id=0, reason=3, volume_initial=0.01, volume_current=0.01, price_open=1.1694200000000001, sl=1.1684200000000002, tp=1.17242, price_current=1.1694200000000001, price_stoplimit=0, symbol='EURUSD', comment='Buy Stop', external_id=''),
TradeOrder(ticket=113127357729019468803, time_setup=1767622158, time_setup_msc=1767622158000, time_done=0, time_done_msc=0, time_expiration=0, type=5, type_time=0, type_filling=1, state=1, magic=112233, position_id=0, position_by_id=0, reason=3, volume_initial=0.01, volume_current=0.01, price_open=1.16642, sl=1.16742, tp=1.1634200000000001, price_current=1.16642, price_stoplimit=0, symbol='EURUSD', comment='Sell Stop', external_id='')) Outputs (MetaTrader 5 Mode):
(venv) C:\Users\Omega Joctan\OneDrive\Documents\PyMetaTester>python test.py --mt5
2026-01-05 15:09:29,171 | INFO | simulator | [Trade.py:85 - position_open() ] => Position Opened successfully!
2026-01-05 15:09:30,270 | INFO | simulator | [Trade.py:85 - position_open() ] => Position Opened successfully!
2026-01-05 15:09:31,110 | INFO | simulator | [Trade.py:148 - order_open() ] => Order opened successfully!
2026-01-05 15:09:31,711 | INFO | simulator | [Trade.py:148 - order_open() ] => Order opened successfully!
2026-01-05 15:09:33,000 | INFO | simulator | [Trade.py:148 - order_open() ] => Order opened successfully!
2026-01-05 15:09:33,952 | INFO | simulator | [Trade.py:148 - order_open() ] => Order opened successfully!
positions in a simulator = 2:
(TradePosition(ticket=1393244663, time=1767622166, time_msc=1767622166713, time_update=1767622166, time_update_msc=1767622166713, type=0, magic=112233, identifier=1393244663, reason=3, volume=0.01, price_open=1.16791, sl=1.1669100000000001, tp=1.16941, price_current=1.16791, swap=0.0, profit=0.0, symbol='EURUSD', comment='Market Buy', external_id=''),
TradePosition(ticket=1393244666, time=1767622167, time_msc=1767622167817, time_update=1767622167, time_update_msc=1767622167817, type=1, magic=112233, identifier=1393244666, reason=3, volume=0.01, price_open=1.16791, sl=1.16891, tp=1.16641, price_current=1.16791, swap=0.0, profit=0.0, symbol='EURUSD', comment='Market Sell', external_id=''))
orders in a simulator = 4:
(TradeOrder(ticket=1393244672, time_setup=1767622168, time_setup_msc=1767622168661, time_done=0, time_done_msc=0, time_expiration=0, type=2, type_time=0, type_filling=2, state=1, magic=112233, position_id=0, position_by_id=0, reason=3, volume_initial=0.01, volume_current=0.01, price_open=1.16591, sl=1.16491, tp=1.16791, price_current=1.16791, price_stoplimit=0.0, symbol='EURUSD', comment='Buy Limit', external_id=''),
TradeOrder(ticket=1393244676, time_setup=1767622169, time_setup_msc=1767622169494, time_done=0, time_done_msc=0, time_expiration=0, type=3, type_time=0, type_filling=2, state=1, magic=112233, position_id=0, position_by_id=0, reason=3, volume_initial=0.01, volume_current=0.01, price_open=1.16991, sl=1.1709100000000001, tp=1.16791, price_current=1.16791, price_stoplimit=0.0, symbol='EURUSD', comment='Sell Limit', external_id=''),
TradeOrder(ticket=1393244679, time_setup=1767622170, time_setup_msc=1767622170093, time_done=0, time_done_msc=0, time_expiration=0, type=4, type_time=0, type_filling=2, state=1, magic=112233, position_id=0, position_by_id=0, reason=3, volume_initial=0.01, volume_current=0.01, price_open=1.16941, sl=1.16841, tp=1.17241, price_current=1.16791, price_stoplimit=0.0, symbol='EURUSD', comment='Buy Stop', external_id=''),
TradeOrder(ticket=1393244687, time_setup=1767622171, time_setup_msc=1767622171748, time_done=0, time_done_msc=0, time_expiration=0, type=5, type_time=0, type_filling=2, state=1, magic=112233, position_id=0, position_by_id=0, reason=3, volume_initial=0.01, volume_current=0.01, price_open=1.16641, sl=1.16741, tp=1.16341, price_current=1.16791, price_stoplimit=0.0, symbol='EURUSD', comment='Sell Stop', external_id=''))
Managing Orders and Positions in a Simulator
With positions stored within arrays, containers in a simulator class, we can perform actions on them, such as detecting certain behaviors/conditions, modifying, or even closing them.
01: Closing Positions
Let's open two distinct positions and close one.
m_trade.buy(volume=lotsize, symbol=symbol, price=ask, comment="buy pos") m_trade.sell(volume=lotsize, symbol=symbol, price=bid, comment="sell pos") print(f"positions in a simulator = {sim.positions_total()}:\n",sim.positions_get()) positions = sim.positions_get() for pos in positions: if pos.symbol == symbol and pos.type == sim.mt5_instance.POSITION_TYPE_BUY: # close a buy position m_trade.position_close(ticket=pos.ticket, deviation=10) print("positions remaining: ", sim.positions_get())
Output:
2026-01-05 15:54:50,114 | INFO | tester | [Trade.py:85 - position_open() ] => Position Opened successfully! 2026-01-05 15:54:50,114 | INFO | tester | [Trade.py:85 - position_open() ] => Position Opened successfully! positions in a simulator = 2: (TradePosition(ticket=113127532167305337632, time=1767624887, time_msc=1767624887000, time_update=1767624887, time_update_msc=1767624887000, type=0, magic=112233, identifier=113127532167305337632, reason=3, volume=0.01, price_open=1.16743, sl=0.0, tp=0.0, price_current=1.16743, swap=0, profit=0, symbol='EURUSD', comment='buy pos', external_id=''), TradePosition(ticket=113127532167305337651, time=1767624887, time_msc=1767624887000, time_update=1767624887, time_update_msc=1767624887000, type=1, magic=112233, identifier=113127532167305337651, reason=3, volume=0.01, price_open=1.16743, sl=0.0, tp=0.0, price_current=1.16743, swap=0, profit=0, symbol='EURUSD', comment='sell pos', external_id='')) 2026-01-05 15:54:50,114 | INFO | tester | [Trade.py:397 - position_close() ] => Position 113127532167305337632 closed successfully! positions remaining: (TradePosition(ticket=113127532167305337651, time=1767624887, time_msc=1767624887000, time_update=1767624887, time_update_msc=1767624887000, type=1, magic=112233, identifier=113127532167305337651, reason=3, volume=0.01, price_open=1.16743, sl=0.0, tp=0.0, price_current=1.16743, swap=0, profit=0, symbol='EURUSD', comment='sell pos', external_id=''),)
A buy position was closed exclusively!
02: Position Modifications
Similarly to closing positions, we have to select one before sending a modification request to it.
m_trade.buy(volume=lotsize, symbol=symbol, price=ask, comment="buy pos") m_trade.sell(volume=lotsize, symbol=symbol, price=bid, comment="sell pos") print(f"positions in a simulator = {sim.positions_total()}:\n",sim.positions_get()) positions = sim.positions_get() for pos in positions: if pos.sl == 0: if pos.type == sim.mt5_instance.POSITION_TYPE_BUY: m_trade.position_modify(ticket=pos.ticket, sl=pos.price_open - 100 * symbol_info.point, tp=pos.tp) if pos.type == sim.mt5_instance.POSITION_TYPE_SELL: m_trade.position_modify(ticket=pos.ticket, sl=pos.price_open + 100 * symbol_info.point, tp=pos.tp) print("positions after modification\n: ", sim.positions_get())
Outputs.
(venv) C:\Users\Omega Joctan\OneDrive\Documents\PyMetaTester>python test.py 2026-01-05 17:19:11,604 | INFO | tester | [Trade.py:85 - position_open() ] => Position Opened successfully! 2026-01-05 17:19:11,604 | INFO | tester | [Trade.py:85 - position_open() ] => Position Opened successfully! positions in a simulator = 2: (TradePosition(ticket=113127856102580262436, time=1767629948, time_msc=1767629948000, time_update=1767629948, time_update_msc=1767629948000, type=0, magic=112233, identifier=113127856102580262436, reason=3, volume=0.01, price_open=1.16789, sl=0.0, tp=0.0, price_current=1.16789, swap=0, profit=0, symbol='EURUSD', comment='buy pos', external_id=''), TradePosition(ticket=113127856102710534416, time=1767629948, time_msc=1767629948000, time_update=1767629948, time_update_msc=1767629948000, type=1, magic=112233, identifier=113127856102710534416, reason=3, volume=0.01, price_open=1.16789, sl=0.0, tp=0.0, price_current=1.16789, swap=0, profit=0, symbol='EURUSD', comment='sell pos', external_id='')) 2026-01-05 17:19:11,604 | INFO | tester | [Trade.py:469 - position_modify() ] => Position 113127856102580262436 modified successfully! 2026-01-05 17:19:11,606 | INFO | tester | [Trade.py:469 - position_modify() ] => Position 113127856102710534416 modified successfully! positions after modification : (TradePosition(ticket=113127856102580262436, time=1767629948, time_msc=1767629948000, time_update=1767629948, time_update_msc=1767629948000, type=0, magic=112233, identifier=113127856102580262436, reason=3, volume=0.01, price_open=1.16789, sl=1.1668900000000002, tp=0.0, price_current=1.16789, swap=0, profit=0, symbol='EURUSD', comment='buy pos', external_id=''), TradePosition(ticket=113127856102710534416, time=1767629948, time_msc=1767629948000, time_update=1767629948, time_update_msc=1767629948000, type=1, magic=112233, identifier=113127856102710534416, reason=3, volume=0.01, price_open=1.16789, sl=1.16889, tp=0.0, price_current=1.16789, swap=0, profit=0, symbol='EURUSD', comment='sell pos', external_id=''))
03: Working With Pending Orders
Below is how you can modify and delete pending orders.
m_trade.buy_stop(symbol=symbol, volume=symbol_info.volume_min, price=ask+500*symbol_info.point) for order in sim.orders_get(): print("order curr price: ", order.price_open) m_trade.order_modify(ticket=order.ticket, price=order.price_open+10*symbol_info.point, sl=order.sl, tp=order.tp) print("order moved 10 points upward", order.price_open) if m_trade.order_delete(ticket=order.ticket) is None: continue print("orders remaining: ", sim.orders_total())
Outputs.
(venv) C:\Users\Omega Joctan\OneDrive\Documents\PyMetaTester>python test.py 2026-01-05 17:43:15,677 | INFO | tester | [Trade.py:148 - order_open() ] => Order opened successfully! order curr price: 1.17281 2026-01-05 17:43:15,677 | INFO | tester | [Trade.py:536 - order_modify() ] => Order 113127948523388723206 modified successfully! order moved 10 points upward 1.17291 2026-01-05 17:43:15,677 | INFO | tester | [Trade.py:431 - order_delete() ] => Order 113127948523388723206 deleted successfully! orders remaining: 0
What's Next?
The ability to use tick data from a particular period in the past and use it simulate a trading operation means that we are a few steps away from finalizing our custom simulator. Right now we have everything needed to create a loop that runs the simulation through all available ticks in specified time range. This is what we call strategy testing or a complete simulation.
In this article, we discussed the most important aspect of any trading simulator, that is, sending trading requests and managing them. In the next one, we are going to put it all together and run our very first strategy testing action in Python, Stay tuned!
Peace Out.
Share your thoughts and help to improve this project on GitHub: https://github.com/MegaJoctan/PyMetaTester
Attachments Table
| Filename | Description & Usage |
|---|---|
| Trade/Trade.py | Contains the CTrade class, Class providing an easy way for trade operations execution. |
| config.py | A Python configuration file where the most useful variables for reusability throughout the project are defined. |
| utils.py | A utility Python file which contains simple functions to help with various tasks (helpers). |
| simulator.py | It has a class named Simulator. Our core simulator logic is in one place. |
| test.py | A file used for testing all the code and functions discussed in this post. |
| error_description.py | It has functions for converting all MetaTrader 5 error codes into human-readable messages. |
| requirements.txt | Contains all Python dependencies and their versions, used in this project. |
Warning: All rights to these materials are reserved by MetaQuotes Ltd. Copying or reprinting of these materials in whole or in part is prohibited.
This article was written by a user of the site and reflects their personal views. MetaQuotes Ltd is not responsible for the accuracy of the information presented, nor for any consequences resulting from the use of the solutions, strategies or recommendations described.
Developing a multi-currency Expert Advisor (Part 24): Adding a new strategy (II)
Creating Custom Indicators in MQL5 (Part 5): WaveTrend Crossover Evolution Using Canvas for Fog Gradients, Signal Bubbles, and Risk Management
Features of Experts Advisors
Price Action Analysis Toolkit (Part 55): Designing a CPI Mini-Candle Overlay for Intra-bar Pressure
- Free trading apps
- Over 8,000 signals for copying
- Economic news for exploring financial markets
You agree to website policy and terms of use